Skip to content

Commit 7122c20

Browse files
Pure render for tree data, fixes React Canary useMemo/useState StrictMode (#5835)
* Pure render for useTreeData * adjust some more calls * fix accumulation of build trees * better readability * fix typo * fix remaining broken tests * revert canary testing changes * fix lint * updating naming and making things a bit easier to read * make sure a new map is always generated in insert for purity * make useTreeData always use and return a new Map this avoids the case where we may inadvertantly modify the previous map directly which could be problematic in a double render * guard against empy array in remove --------- Co-authored-by: Daniel Lu <[email protected]>
1 parent 3973a77 commit 7122c20

File tree

2 files changed

+84
-64
lines changed

2 files changed

+84
-64
lines changed

packages/@react-spectrum/listbox/stories/ListBox.stories.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -940,6 +940,7 @@ export function FocusExample(args = {}) {
940940
getKey: (item) => item.name,
941941
getChildren: (item:{name:string, children?:{name:string, children?:{name:string}[]}[]}) => item.children
942942
});
943+
943944
let [dialog, setDialog] = useState(null);
944945
let ref = useRef(null);
945946
return (

packages/@react-stately/data/src/useTreeData.ts

Lines changed: 83 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
*/
1212

1313
import {Key} from '@react-types/shared';
14-
import {useMemo, useState} from 'react';
14+
import {useState} from 'react';
1515

1616
export interface TreeOptions<T extends object> {
1717
/** Initial root items in the tree. */
@@ -126,44 +126,47 @@ export function useTreeData<T extends object>(options: TreeOptions<T>): TreeData
126126
getKey = (item: any) => item.id || item.key,
127127
getChildren = (item: any) => item.children
128128
} = options;
129-
let map = useMemo(() => new Map<Key, TreeNode<T>>(), []);
130129

131130
// We only want to compute this on initial render.
132-
// eslint-disable-next-line react-hooks/exhaustive-deps
133-
let initialNodes = useMemo(() => buildTree(initialItems), []);
134-
let [items, setItems] = useState(initialNodes);
131+
let [tree, setItems] = useState<{items: TreeNode<T>[], nodeMap: Map<Key, TreeNode<T>>}>(() => buildTree(initialItems, new Map()));
132+
let {items, nodeMap} = tree;
133+
135134
let [selectedKeys, setSelectedKeys] = useState(new Set<Key>(initialSelectedKeys || []));
136135

137-
function buildTree(initialItems: T[] = [], parentKey?: Key | null) {
138-
return initialItems.map(item => {
139-
let node: TreeNode<T> = {
140-
key: getKey(item),
141-
parentKey: parentKey,
142-
value: item,
143-
children: null
144-
};
136+
function buildTree(initialItems: T[] = [], map: Map<Key, TreeNode<T>>, parentKey?: Key | null) {
137+
return {
138+
items: initialItems.map(item => {
139+
let node: TreeNode<T> = {
140+
key: getKey(item),
141+
parentKey: parentKey,
142+
value: item,
143+
children: null
144+
};
145145

146-
node.children = buildTree(getChildren(item), node.key);
147-
map.set(node.key, node);
148-
return node;
149-
});
146+
node.children = buildTree(getChildren(item), map, node.key).items;
147+
map.set(node.key, node);
148+
return node;
149+
}),
150+
nodeMap: map
151+
};
150152
}
151153

152-
function updateTree(items: TreeNode<T>[], key: Key, update: (node: TreeNode<T>) => TreeNode<T>) {
153-
let node = map.get(key);
154+
function updateTree(items: TreeNode<T>[], key: Key, update: (node: TreeNode<T>) => TreeNode<T>, originalMap: Map<Key, TreeNode<T>>) {
155+
let node = originalMap.get(key);
154156
if (!node) {
155-
return items;
157+
return {items, nodeMap: originalMap};
156158
}
159+
let map = new Map<Key, TreeNode<T>>(originalMap);
157160

158161
// Create a new node. If null, then delete the node, otherwise replace.
159162
let newNode = update(node);
160163
if (newNode == null) {
161-
deleteNode(node);
164+
deleteNode(node, map);
162165
} else {
163-
addNode(newNode);
166+
addNode(newNode, map);
164167
}
165168

166-
// Walk up the tree and update each parent to refer to the new chilren.
169+
// Walk up the tree and update each parent to refer to the new children.
167170
while (node.parentKey) {
168171
let nextParent = map.get(node.parentKey);
169172
let copy: TreeNode<T> = {
@@ -196,26 +199,29 @@ export function useTreeData<T extends object>(options: TreeOptions<T>): TreeData
196199
items = items.filter(c => c !== node);
197200
}
198201

199-
return items.map(item => {
200-
if (item === node) {
201-
return newNode;
202-
}
202+
return {
203+
items: items.map(item => {
204+
if (item === node) {
205+
return newNode;
206+
}
203207

204-
return item;
205-
});
208+
return item;
209+
}),
210+
nodeMap: map
211+
};
206212
}
207213

208-
function addNode(node: TreeNode<T>) {
214+
function addNode(node: TreeNode<T>, map: Map<Key, TreeNode<T>>) {
209215
map.set(node.key, node);
210216
for (let child of node.children) {
211-
addNode(child);
217+
addNode(child, map);
212218
}
213219
}
214220

215-
function deleteNode(node: TreeNode<T>) {
221+
function deleteNode(node: TreeNode<T>, map: Map<Key, TreeNode<T>>) {
216222
map.delete(node.key);
217223
for (let child of node.children) {
218-
deleteNode(child);
224+
deleteNode(child, map);
219225
}
220226
}
221227

@@ -224,19 +230,22 @@ export function useTreeData<T extends object>(options: TreeOptions<T>): TreeData
224230
selectedKeys,
225231
setSelectedKeys,
226232
getItem(key: Key) {
227-
return map.get(key);
233+
return nodeMap.get(key);
228234
},
229235
insert(parentKey: Key | null, index: number, ...values: T[]) {
230-
setItems(items => {
231-
let nodes = buildTree(values, parentKey);
236+
setItems(({items, nodeMap: originalMap}) => {
237+
let {items: newNodes, nodeMap: newMap} = buildTree(values, originalMap, parentKey);
232238

233239
// If parentKey is null, insert into the root.
234240
if (parentKey == null) {
235-
return [
236-
...items.slice(0, index),
237-
...nodes,
238-
...items.slice(index)
239-
];
241+
return {
242+
items: [
243+
...items.slice(0, index),
244+
...newNodes,
245+
...items.slice(index)
246+
],
247+
nodeMap: newMap
248+
};
240249
}
241250

242251
// Otherwise, update the parent node and its ancestors.
@@ -246,30 +255,30 @@ export function useTreeData<T extends object>(options: TreeOptions<T>): TreeData
246255
value: parentNode.value,
247256
children: [
248257
...parentNode.children.slice(0, index),
249-
...nodes,
258+
...newNodes,
250259
...parentNode.children.slice(index)
251260
]
252-
}));
261+
}), newMap);
253262
});
254263
},
255264
insertBefore(key: Key, ...values: T[]): void {
256-
let node = map.get(key);
265+
let node = nodeMap.get(key);
257266
if (!node) {
258267
return;
259268
}
260269

261-
let parentNode = map.get(node.parentKey);
270+
let parentNode = nodeMap.get(node.parentKey);
262271
let nodes = parentNode ? parentNode.children : items;
263272
let index = nodes.indexOf(node);
264273
this.insert(parentNode?.key, index, ...values);
265274
},
266275
insertAfter(key: Key, ...values: T[]): void {
267-
let node = map.get(key);
276+
let node = nodeMap.get(key);
268277
if (!node) {
269278
return;
270279
}
271280

272-
let parentNode = map.get(node.parentKey);
281+
let parentNode = nodeMap.get(node.parentKey);
273282
let nodes = parentNode ? parentNode.children : items;
274283
let index = nodes.indexOf(node);
275284
this.insert(parentNode?.key, index + 1, ...values);
@@ -281,7 +290,7 @@ export function useTreeData<T extends object>(options: TreeOptions<T>): TreeData
281290
if (parentKey == null) {
282291
this.insert(null, items.length, ...values);
283292
} else {
284-
let parentNode = map.get(parentKey);
293+
let parentNode = nodeMap.get(parentKey);
285294
if (!parentNode) {
286295
return;
287296
}
@@ -290,16 +299,24 @@ export function useTreeData<T extends object>(options: TreeOptions<T>): TreeData
290299
}
291300
},
292301
remove(...keys: Key[]) {
302+
if (keys.length === 0) {
303+
return;
304+
}
305+
293306
let newItems = items;
307+
let prevMap = nodeMap;
308+
let newTree;
294309
for (let key of keys) {
295-
newItems = updateTree(newItems, key, () => null);
310+
newTree = updateTree(newItems, key, () => null, prevMap);
311+
prevMap = newTree.nodeMap;
312+
newItems = newTree.items;
296313
}
297314

298-
setItems(newItems);
315+
setItems(newTree);
299316

300317
let selection = new Set(selectedKeys);
301318
for (let key of selectedKeys) {
302-
if (!map.has(key)) {
319+
if (!newTree.nodeMap.has(key)) {
303320
selection.delete(key);
304321
}
305322
}
@@ -310,13 +327,14 @@ export function useTreeData<T extends object>(options: TreeOptions<T>): TreeData
310327
this.remove(...selectedKeys);
311328
},
312329
move(key: Key, toParentKey: Key | null, index: number) {
313-
setItems(items => {
314-
let node = map.get(key);
330+
setItems(({items, nodeMap: originalMap}) => {
331+
let node = originalMap.get(key);
315332
if (!node) {
316-
return items;
333+
return {items, nodeMap: originalMap};
317334
}
318335

319-
items = updateTree(items, key, () => null);
336+
let {items: newItems, nodeMap: newMap} = updateTree(items, key, () => null, originalMap);
337+
320338

321339
const movedNode = {
322340
...node,
@@ -325,15 +343,15 @@ export function useTreeData<T extends object>(options: TreeOptions<T>): TreeData
325343

326344
// If parentKey is null, insert into the root.
327345
if (toParentKey == null) {
328-
return [
329-
...items.slice(0, index),
346+
return {items: [
347+
...newItems.slice(0, index),
330348
movedNode,
331-
...items.slice(index)
332-
];
349+
...newItems.slice(index)
350+
], nodeMap: newMap};
333351
}
334352

335353
// Otherwise, update the parent node and its ancestors.
336-
return updateTree(items, toParentKey, parentNode => ({
354+
return updateTree(newItems, toParentKey, parentNode => ({
337355
key: parentNode.key,
338356
parentKey: parentNode.parentKey,
339357
value: parentNode.value,
@@ -342,21 +360,22 @@ export function useTreeData<T extends object>(options: TreeOptions<T>): TreeData
342360
movedNode,
343361
...parentNode.children.slice(index)
344362
]
345-
}));
363+
}), newMap);
346364
});
347365
},
348366
update(oldKey: Key, newValue: T) {
349-
setItems(items => updateTree(items, oldKey, oldNode => {
367+
setItems(({items, nodeMap: originalMap}) => updateTree(items, oldKey, oldNode => {
350368
let node: TreeNode<T> = {
351369
key: oldNode.key,
352370
parentKey: oldNode.parentKey,
353371
value: newValue,
354372
children: null
355373
};
356374

357-
node.children = buildTree(getChildren(newValue), node.key);
375+
let tree = buildTree(getChildren(newValue), originalMap, node.key);
376+
node.children = tree.items;
358377
return node;
359-
}));
378+
}, originalMap));
360379
}
361380
};
362381
}

0 commit comments

Comments
 (0)