'Rerender and useDeepCompareEffect don't seem to work together well
The answer to this question may very well be "don't do that", but I'd like to understand the behavior better.
I have a simple tree component that I want to rerender if the node list provided changes. The main use case is when the node list comes from a async call to the server. This is the relevant component file, minus the imports:
const TreeViewContainer = styled.div.attrs({
'data-testid': 'tws-tree',
role: 'tree',
})`
padding: 0;
`;
type TreeViewAction = {
type: 'refresh' | 'toggle';
payload: ITreeNode | ITreeNode[];
};
const treeViewReducer = (state: ITreeNode[], action: TreeViewAction) => {
switch (action.type) {
// Toggle open/closed on the parent (the node passed in) and visibility on that node's children.
case 'toggle': {
const incomingNode = action.payload as ITreeNode;
const isOpen = !incomingNode.open || false;
const newNode = { ...incomingNode, open: isOpen };
const childNodes = state.filter((n) => n.parentId === incomingNode.nodeId).slice(0);
childNodes.forEach((node) => (node.visible = isOpen));
return state.map((node) => {
if (node.nodeId === newNode.nodeId) {
return newNode;
} else {
return childNodes.find((n) => n.nodeId === node.nodeId) || node;
}
});
}
case 'refresh': {
return cloneDeep(action.payload) as ITreeNode[];
}
default:
return state;
}
};
export function TreeView(props: ITreeView) {
const { nodes, onNodeSelected, renderFactory } = props;
const [nodeList, dispatch] = useReducer(treeViewReducer, nodes);
useDeepCompareEffect(() => {
dispatch({ type: 'refresh', payload: nodes });
}, [nodes]);
const handleNodeClick = (node: ITreeNode) => {
if (node.hasChildren) {
dispatch({ type: 'toggle', payload: node });
} else {
onNodeSelected(node);
}
};
return (
<TreeViewContainer>
{nodeList
?.filter((n) => n.visible)
.map((node) => {
return (
<TreeNode key={node.nodeId} {...node} onClick={handleNodeClick}>
{renderFactory(node)}
</TreeNode>
);
})}
</TreeViewContainer>
);
}
This works fine in actual practice, and tests fine from the point of view of the component that renders the tree from a server-side call. This is probably where I should stop, but I have a lower-level test that broke once I refactored the tree to use useDeepCompareEffect
:
test('rerenders the tree if the nodelist changes', () => {
const nodeList: ITreeNode[] = [
{
nodeId: 'root',
parentId: undefined,
visible: true,
open: true,
hasChildren: true,
depth: 1,
},
];
const { rerender } = render(<TreeView nodes={nodeList} onNodeSelected={jest.fn()} renderFactory={testRenderFactory} />);
expect(screen.getAllByRole('treeitem').length).toBe(1);
nodeList.push({
nodeId: 'child',
parentId: 'root',
visible: true,
depth: 2,
});
rerender(<TreeView nodes={nodeList} onNodeSelected={jest.fn()} renderFactory={testRenderFactory} />);
expect(screen.getAllByRole('treeitem').length).toBe(2);
});
I'd like to know why this test is failing. Looking at the implementation of useDeepCompareEffect
, it seems to me that it should see that change.
Debugging and console logging tell me that though the incoming nodelist changes, the effect isn't called (dispatch({ type: 'refresh', payload: nodes });
). The assertion fails because only one treeitem is ever rendered in this test.
Solution 1:[1]
I took my own advice -- "Don't do that."
Here's a better test:
test('rerenders the tree if the nodelist changes', () => {
function RefreshHarness() {
const [nodes, setNodes] = useState<ITreeNode[]>([
{
nodeId: 'root',
parentId: undefined,
visible: true,
open: true,
hasChildren: true,
depth: 1,
},
]);
const handleClick = () => {
setNodes([
...nodes,
{
nodeId: 'child',
parentId: 'root',
visible: true,
depth: 2,
},
]);
};
return (
<div>
<Button onClick={handleClick}>Test Me</Button>
<TreeView nodes={nodes} onNodeSelected={jest.fn()} renderFactory={testRenderFactory} />
</div>
);
}
render(<RefreshHarness />);
expect(screen.getAllByRole('treeitem')).toHaveLength(1);
userEvent.click(screen.getByRole('button', { name: /test me/i }));
expect(screen.getAllByRole('treeitem')).toHaveLength(2);
});
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
Solution | Source |
---|---|
Solution 1 | Stuart |