2

I'm having a strange issue when implementing a drag-and-drop. The drag and drop has 2 levels, the Group and the Rank.

The intention is that every "overall group" (buttons at the top) maintain their own ranks for both Groups and Ranks. So if I move Type A - 1 to Rank 2 when I have Overall Group A selected, I should be able to then select Overall Group B and see Type A - 1 in rank 1.

The Group (outer) level works fine. When I change between "Overall" groups, the sorting remains tied to each respective overall group as seen below:

good example

However when changing the Rank it changes for all overall groups. Even stranger, when console logging before my setState (passed through props) I can see the props are already changed before calling! Somehow my props (and actually the state) object is being mutated before I call setState.

bad example

A couple things I tried:

  1. smacking key everywhere and making them unique, in case this is a DnD problem
  2. making copies of the props before ever passing it to a Draggable or a function that could potentially mutate it

I did make a codesandbox -- https://codesandbox.io/s/react-beautiful-dnd-nested-drag-bug-q89lot?file=/src/Wrapper.tsx:1649-2116

but functionally both Draggable's reuse a single onDragEnd which does some logic to then call setOverallGroups() which sets the groups and ranks for all groups at once

Frank
  • 735
  • 1
  • 12
  • 33

2 Answers2

1

Solution: https://codesandbox.io/s/react-beautiful-dnd-nested-drag-bug-forked-i8x5ry?file=/src/Wrapper.tsx

In short, you are shallow cloning your listsInitalState across "overall groups". You want to deep clone them, so that any changes (of any level) in one "overall groups" doesn't affect other groups.

In your Wrapper.tsx, you should use deep cloning at these lines of code:

const [overallGroups, setOverallGroups] = useState<
  Record<string, OverallGroup>
>({
  'Overall Group A': JSON.parse(JSON.stringify(listsInitalState)),
  'Overall Group B': JSON.parse(JSON.stringify(listsInitalState)),
  'Overall Group C': JSON.parse(JSON.stringify(listsInitalState)),
  'Overall Group D': JSON.parse(JSON.stringify(listsInitalState)),
  'Overall Group E': JSON.parse(JSON.stringify(listsInitalState)),
});

...instead of shallow cloning:

const [overallGroups, setOverallGroups] = useState<
  Record<string, OverallGroup>
>({
  'Overall Group A': { ...listsInitalState },
  'Overall Group B': { ...listsInitalState },
  'Overall Group C': { ...listsInitalState },
  'Overall Group D': { ...listsInitalState },
  'Overall Group E': { ...listsInitalState },
});

Note: My solution uses JSON.parse(JSON.stringify(...)) as the simplest way to do deep-cloning for simple objects. It is not very performant, however, so you can look into third-party libraries, or if your environment already supports, you can use structuredClone.

Son Nguyen
  • 1,472
  • 8
  • 16
1

Shallow copying only clone the top level only. So if you have a nested object then it will not clone them.

  const listsInitalState: OverallGroup = {
    "Type A": typeAItems,
    "Type B": typeBItems,
    Order: ["Type A", "Type B"]
  };

  const shallowCpy = {...listsInitialState};

  console.log(shallowCpy["Type A"] === listsInitialState["Type A"]) // this will be true

Therefore you should do a deep copy. Another way to do a deep copy is use the lodash cloneDeep method.

Another Solution,

Since you are using the same data for all the tabs, keep the data in single place and manage the order of the items by ids of the data objects,

const [order, setOrder] = useState({
  1: { // an unique id for each overall group
    0: { // unique id to identify each groups within overall group
      subOrder: [0,2,1,3] // order of the group with id 0
    },
    1: {
      subOrder: [0,3,1,2]
    }
  },
  // rest of the overllgroup ids
})

Then use the order to render items & content will be not mutated in this way as well as you will not clone bunch of data as well.

Dilshan
  • 2,797
  • 1
  • 8
  • 26