diff --git a/packages/x-tree-view/src/internals/plugins/useTreeViewJSXNodes/useTreeViewJSXNodes.tsx b/packages/x-tree-view/src/internals/plugins/useTreeViewJSXNodes/useTreeViewJSXNodes.tsx index 89f89081511c7..34b68bc1f3140 100644 --- a/packages/x-tree-view/src/internals/plugins/useTreeViewJSXNodes/useTreeViewJSXNodes.tsx +++ b/packages/x-tree-view/src/internals/plugins/useTreeViewJSXNodes/useTreeViewJSXNodes.tsx @@ -17,7 +17,19 @@ export const useTreeViewJSXNodes: TreeViewPlugin = setState, }) => { const insertJSXNode = useEventCallback((node: TreeViewNode) => { - setState((prevState) => ({ ...prevState, nodeMap: { ...prevState.nodeMap, [node.id]: node } })); + setState((prevState) => { + if (prevState.nodeMap[node.id] != null) { + throw new Error( + [ + 'MUI X: The Tree View component requires all items to have a unique `id` property.', + 'Alternatively, you can use the `getItemId` prop to specify a custom id for each item.', + `Tow items were provided with the same id in the \`items\` prop: "${node.id}"`, + ].join('\n'), + ); + } + + return { ...prevState, nodeMap: { ...prevState.nodeMap, [node.id]: node } }; + }); }); const removeJSXNode = useEventCallback((nodeId: string) => { diff --git a/packages/x-tree-view/src/internals/plugins/useTreeViewNodes/useTreeViewNodes.test.tsx b/packages/x-tree-view/src/internals/plugins/useTreeViewNodes/useTreeViewNodes.test.tsx new file mode 100644 index 0000000000000..af8da91e18ef4 --- /dev/null +++ b/packages/x-tree-view/src/internals/plugins/useTreeViewNodes/useTreeViewNodes.test.tsx @@ -0,0 +1,60 @@ +import * as React from 'react'; +import { expect } from 'chai'; +import { createRenderer, ErrorBoundary } from '@mui-internal/test-utils'; +import { RichTreeView } from '@mui/x-tree-view/RichTreeView'; +import { SimpleTreeView } from '@mui/x-tree-view/SimpleTreeView'; +import { TreeItem } from '@mui/x-tree-view/TreeItem'; + +describe('useTreeViewNodes', () => { + const { render } = createRenderer(); + + it('should throw an error when two items have the same ID (items prop approach)', function test() { + // TODO is this fixed? + if (!/jsdom/.test(window.navigator.userAgent)) { + // can't catch render errors in the browser for unknown reason + // tried try-catch + error boundary + window onError preventDefault + this.skip(); + } + + expect(() => + render( + + + , + ), + ).toErrorDev([ + 'MUI X: The Tree View component requires all items to have a unique `id` property.', + 'MUI X: The Tree View component requires all items to have a unique `id` property.', + 'The above error occurred in the component:', + ]); + }); + + it('should throw an error when two items have the same ID (JSX approach)', function test() { + // TODO is this fixed? + if (!/jsdom/.test(window.navigator.userAgent)) { + // can't catch render errors in the browser for unknown reason + // tried try-catch + error boundary + window onError preventDefault + this.skip(); + } + + expect(() => + render( + + + + + + , + ), + ).toErrorDev([ + 'MUI X: The Tree View component requires all items to have a unique `id` property.', + 'MUI X: The Tree View component requires all items to have a unique `id` property.', + 'The above error occurred in the component:', + ]); + }); +}); diff --git a/packages/x-tree-view/src/internals/plugins/useTreeViewNodes/useTreeViewNodes.ts b/packages/x-tree-view/src/internals/plugins/useTreeViewNodes/useTreeViewNodes.ts index 98aba1c4c71a5..43aef3ff53a2e 100644 --- a/packages/x-tree-view/src/internals/plugins/useTreeViewNodes/useTreeViewNodes.ts +++ b/packages/x-tree-view/src/internals/plugins/useTreeViewNodes/useTreeViewNodes.ts @@ -29,6 +29,7 @@ const updateState = ({ parentId: string | null, ): TreeViewNodeIdAndChildren => { const id: string = getItemId ? getItemId(item) : (item as any).id; + if (id == null) { throw new Error( [ @@ -40,6 +41,16 @@ const updateState = ({ ); } + if (nodeMap[id] != null) { + throw new Error( + [ + 'MUI X: The Tree View component requires all items to have a unique `id` property.', + 'Alternatively, you can use the `getItemId` prop to specify a custom id for each item.', + `Tow items were provided with the same id in the \`items\` prop: "${id}"`, + ].join('\n'), + ); + } + const label = getItemLabel ? getItemLabel(item) : (item as { label: string }).label; if (label == null) { throw new Error(