react-sortablejs icon indicating copy to clipboard operation
react-sortablejs copied to clipboard

setList fires when drag starts and when drag ends

Open beauchette opened this issue 4 years ago • 14 comments

Describe the bug setList is supposed to help set the state. It is fired when drag ends, with the new state. Unfortunately it is also called when the Drag starts with the actual state, so basically, there is a setState for nothing at drag start.

To Reproduce

  1. create a new react app.
  2. add react-sortablejs
  3. use the first example (functional component in my case)
  4. create a setStateWrapper that just executes setState and add a console.log with the setState parameter
  5. notice in the console, how sometimes, setStateWrapper is called sometimes 3 times

Expected behavior setList being there to help state the state, should only fire once.

Information "react": "^16.9.0", "react-sortablejs": "^2.0.11",

though, when I created a new react app to test, it was: "react": "^16.13.1", "react-sortablejs": "^2.0.11",

beauchette avatar Jun 03 '20 17:06 beauchette

Same here

corujoraphael avatar Jun 04 '20 00:06 corujoraphael

Oh that's a problem for me, because I update a db via https each time, so, even I filter out when it's the same content, there's always gonna be the problem of making 2 requests instead of one, the first time.

So I ended up using onEnd but it's dirty, well, not the React way. I had to use custom attribute to set the id (as a dbid attribute) on the sortable elements, and then read them in the onEnd function

beauchette avatar Jun 04 '20 08:06 beauchette

Any update on this ? It actually fires when the drag starts, but also twice when the drag ends on the source list with only the last call being genuine. It make it quite painful to work with.

Thanks for the library

Camsteack avatar Jun 16 '20 09:06 Camsteack

@Camsteack as I said in an earlier comment, setting state that often is not really a problem. What is a problem is to update your database through an http(s) request, so basically, I used the onEnd event, I had to add attributes to be able to get the database id from them, that's a little dirty but it worked

beauchette avatar Jun 16 '20 15:06 beauchette

I ran across the same issue and I got around it by using state to track whether or not I was actively in the process of sorting. Here's a rough example:

import { ReactSortable } from 'react-sortablejs';

const Sorter = () => {
  const [isSorting, setIsSorting] = useState(false);
  const [items, setItems] = useState(['A', 'B', 'C', 'D', 'E', 'F']);

  const updateOrder = (updatedList) => {
    if (!isSorting) return;
    setIsSorting(false);
    setItems(updatedList);
  };

  return (
    <ReactSortable
      list={items}
      onStart={() => setIsSorting(true)}
      setList={(updatedList) => updateOrder(updatedList)}>
      {items.map((item) => <div>{item}</div>)}
    </ReactSortable>
  );
};

export default Sorter;

doug-stewart avatar Jun 25 '20 23:06 doug-stewart

Same here

anynines-johannchopin avatar Jul 02 '20 10:07 anynines-johannchopin

Same here

lucaslz2020 avatar Jul 07 '20 06:07 lucaslz2020

I'm not sure why but @doug-stewart solution doesn't work for me. When isSorting is true, updateOrder fires with old updatedList, so by the time updateOrder fires with the correct sorted updatedList, isSorting is false and exits before updating the itemsState.

Instead I allow the updateOrder after onUpdate is fired, and I use a ref for isSorting, since this state is not meant to rerender the component but to know whether Sortable is sorting or not.

import { ReactSortable } from 'react-sortablejs';

const Sorter = () => {
  const isSortingRef = useRef(false);
  const [items, setItems] = useState(['A', 'B', 'C', 'D', 'E', 'F']);

  const updateOrder = (updatedList) => {
    if (!isSortingRef.current) return;
    isSortingRef.current = false
    setItems(updatedList);
  };

  return (
    <ReactSortable
      list={items}
      onUpdate={() => isSortingRef.current = true}
      setList={(updatedList) => updateOrder(updatedList)}>
      {items.map((item) => <div>{item}</div>)}
    </ReactSortable>
  );
};

export default Sorter;

aquaductape avatar Aug 28 '20 00:08 aquaductape

@aquaductape Hi, I tried your way but for me onUpdate is not working at all. Any hints on how to make it work?

riazosama avatar Jan 06 '21 18:01 riazosama

before set state check equality of new and old state by lodash. this is example code for class component. in my case items has 2 fields: id & name.

updateOrder = (newState) => {
    const prevState = this.state.list;
    newState = newState.map(item => _.pick(item, ['id', 'name']));

    if (_.isEqual(prevState, newState)) return;

    this.setState({
        list: newState,
    })
 }

samieinejad avatar Jul 02 '21 10:07 samieinejad

This is the easiest drag and drop library to use in the react ecosystem, but instead of onEnd, it uses buggy setList, which is really annoying

vaynevayne avatar Jun 02 '23 08:06 vaynevayne

Im just shocked such a great library still has this most fundamental of issues :(

makis-x avatar Aug 16 '23 08:08 makis-x

Working Solution :

import React, { FC, useState } from "react";
import { ReactSortable } from "react-sortablejs";

interface ItemType {
  id: number;
  name: string;
}

export const BasicFunction: FC = (props) => {
  const [state, setState] = useState<ItemType[]>([
    { id: 1, name: "shrek" },
    { id: 2, name: "fiona" },
  ]);
  
  // Use this custom function so that only update when changes on drag and drop
  function updateState(newState) {
   if (JSON.stringify(newState) !== JSON.stringify(state)) {
      setState(newState);
    }
  }

  return (
    <ReactSortable list={state} setList={updateState}>
      {state.map((item) => (
        <div key={item.id}>{item.name}</div>
      ))}
    </ReactSortable>
  );
};

tjthouhid avatar Nov 30 '23 16:11 tjthouhid