Open Rihel opened 2 years ago
I didn't fully understand the issue with nesting. I've been using nesting for the feature I'm building and it works just fine, I can drag from anywhere to anywhere.
@julienben, could you please put out a code sandbox example for nested DND with a parent-child relationship between items?
https://codesandbox.io/s/react-sortablejs-nested-demo-nouiv3
Very simplified version of my tool. You have groups and items. You can nest infinitely in any way you want (except items, which do not accept children, but you can make it all groups if you need).
can i close this issue tough?
@andresin87 I'm guessing you can close but I was wondering if you had any thoughts on my nesting example above? There's only one "setState
" function, it uses DFS to know which nesting to update.
Nesting does work fine, but all lists must basically use the same setState
as @julienben pointed out.
Another way that I've recently implemented ( found with a bunch of random google searching ), was passing both a top level setList
method as well as an indexs
array down to each child list, which contains that lists index in the parent list.
This allows easily manipulating the list state by using indexs for traversal.
The child list setList looks something like this:
setList={currentList => {
setList(rootList=> {
const newRootList= [...rootList];
const _indexs = [...indexs];
const currentListIndex = _indexs.pop();
// Get ref to currentLst inside rootList.
const currentListRef= _indexs.reduce(
(arr, i) => arr[i]["children"],
tempList
);
// Replace with new updated list.
currentListRef[currentListIndex ]["children"] = currentList;
return newRootList;
});
}}
lastArr
in this case is a reference to the curennt list inside the nested structure, setting its value there updates it inside the root list.
No extra packages needed, just a clever indexs based list traversal using references.
Full examples here:
One issue I'm facing is that it stops working with React 18. Not urgent since most other deps still need to catch up to it but it'd be a shame to be stuck to React 17 because of it.
@danieliser Trying this approach, but both the parent and the child fire setList
(first parent with original list, then child with updated, the parent with its updated but not including the child's updates), so I end up missing e.g. a child moved into a folder.
The parent shouldn't really need to update, but not sure how to best stop it.
@julienben - We recently were forced to React 18 (product based on WordPress which upgraded to 18 recently), and I was pleasantly surprised our code still built & ran fine. None of the sortable/draggable stuff broke, and the nesting works fine still.
@Heilemann - Happy to try and work it out, it was a rather confusing set of code which I heavily documented to not forget what exactly is occurring.
Do you have a sandbox?
@danieliser Thank you, but I moved on to @dnd-kit. It's probably overall better suited for my needs anyway. I spent two weeks ramming my head against nested support with this library, and just couldn't ever get it to not be wonky ¯_(ツ)_/¯
@Heilemann no worries. The biggest headache I had was when I tried to be clever and prevent redrawing entire list on every update. If you don't call the parent setlist which triggers a full redraw, then no changes get saved.
That said we were looking at dnd-kit as well, but previously we weren't looking to move to react-18 until our deps themselves moved up.
I also ended up rewriting it all using Context
and hooks so that each parent creates a new context that children live in, but that really just means no passing props, the nesting setList concept still applied as each childContext calls parentContext inside it.
Some updates on our implementation, which I just simplified greatly using contexts while also being more reliable.
I'm posting this here as a full working solution, albeit not the most out of the box simple solution, but it is working great, even on React 18. This should dispell the notes in the readme that it isn't possible. .Happy to boil this into a codesandbox without our extra flavor if its really needed.
This results in infinite nesting while minimizing added setState calls to the bare minimum with no recursion penalty.
A deeply nested item can modify itself without affecting its parents. which is extremely important to performance and reliability.
<List items={[]} onChange={...} indexs={ [0, 1] } />
component, as well as a ListContext
which includes methods like addItem
, removeItem
etc.ListContext
includes both a setList
and setRootList
methods as well as indexs
array, isDragging
and setIsDragging
props handled by useControlledState
etc.ListContext
is then nestable, and when its nested it checks for a parent context, and if found passes the parent shared methods (ie setRootList
etc) into the child context, if not it generates them as the root list.useControlledState
is used to allow shared dragging status through the entire chain. We use it to add animated css borders to dropzones.setList
method which I'll outline on its own below as it is warranted to prevent confusion due to its "magic" methodology which is totally solid JS magic.The setRootList
method and a few other consts you might need for reference.
Of note the setRootList method accepts both new value for entire list, or a callback that takes the existing (unchanged) list as a first argument for modification. Nested setList calls will use the latter, only root will pass value directly.
// Used for parent traversal when nested.
const parentQueryContext = useQuery(); // our custom context.
const isRoot = typeof parentQueryContext.isRoot === 'undefined';
const setRootList = isRoot
? ( newList ) => {
/**
* The root list can accept a functional state action.
* Check for that and call it with current items if needed.
*/
const _newList =
typeof newList !== 'function'
? newList
: // Root list passes in current state to updaters.
newList( items );
// ! Warning, this is a bad idea. It results in changes not saving.
// ! if ( isEqual( items, _newList ) ) {
// * Prevents saving state during dragging as a suitable replacement.
if ( isDragging ) {
return;
}
onChange( {
...query,
items: _newList,
} );
}
: parentQueryContext.setRootList;
And the context value containing the nestable setList
method.
Biggest things to note here:
.reduce
on the indexs
array and a clone of the full list as the starting variable, we are able to recursively walk our way down to the nested list we want to modify, returning a reference to that nested list within the full list.So when moving items from one nested list to another, it makes 2 state save calls in general, one for the removal, one for the addition. No calling setState all the way back up the chain. A deeply nested item can modify itself without affecting its parents.
/**
* Generate a context to be provided to all children consumers of this query.
*/
const queryContext: QueryContextProps = {
...parentQueryContext,
isRoot,
indexs,
isDragging,
setIsDragging,
items,
setRootList,
/**
* The root setList method calls the onChange method directly.
* Nested lists will then call setRootList and pass a SetStateFunctional
* that modifies the rootList based on the current list indexs list.
*/
setList: ( newList ) => {
// Don't save state when dragging, will save a lot of calls.
if ( isDragging ) {
return;
}
// If its the root, check they aren't already equal( optional ), then pass the new list directly to the setRootList method.
if ( isRoot ) {
if ( ! isEqual( items, newList ) ) {
setRootList( newList );
}
} else {
setRootList( ( rootList ) => {
// Clone root list.
const newRootList = [ ...rootList ];
// Clone indexs to current list.
const parentIndexs = [ ...indexs ];
// Remove this lists index from cloned indexs.
const currentListIndex = parentIndexs.pop() ?? 0;
/**
* Get reference to latest array in nested structure.
*
* This clever function loops over each parent indexs,
* starting at the root, returning a reference to the
* last & current nested list within the data structure.
*
* The accumulator start value is the entire root tree.
*
* Effectively drilling down from root -> current
*
* Return reference to child list for each parent index.
*/
const directParentList = parentIndexs.reduce(
( arr, i ) => {
const nextParentGroup = arr[ i ] as GroupItem;
return nextParentGroup.query.items;
},
newRootList
);
// Get reference to current list from the parent list reference.
const closestParentGroup = directParentList[
currentListIndex
] as GroupItem;
// Prevent saving state if items are equal.
if (
! isEqual( closestParentGroup.query.items, newList )
) {
// Replaced referenced items list with updated list.
closestParentGroup.query.items = newList;
}
return newRootList;
} );
}
},
The full code is available here, fully typed in TypeScript:
Notes about our usage:
.query.items
property.And one with the isDragging
state, and you will notice only parents of the current item are sutable dropzones. A group can't drop into itself and our solution's CSS handles that nicely.
With the mobx library, the list data is all responsive, so nesting will be much easier, making it similar to Vuedraggable?