Skip to content

[tree view] Do not render items removed from the items prop - #23337

Open
Anexus5919 wants to merge 2 commits into
mui:masterfrom
Anexus5919:fix/tree-view-stale-items-render
Open

[tree view] Do not render items removed from the items prop#23337
Anexus5919 wants to merge 2 commits into
mui:masterfrom
Anexus5919:fix/tree-view-stale-items-render

Conversation

@Anexus5919

@Anexus5919 Anexus5919 commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

Fixes #23331

Changes

The Tree View store was synced from the root component's props inside a layout effect, so on the render where props.items changes the tree still renders the previous set of items. The store catches up after commit and schedules a second render with the correct set.

Nothing flickers on screen because a layout effect runs before paint, but the item slot does render with ids that are no longer in props.items. A slotProps.item callback that looks the item up in the user's own data, as shown in Using additional props, then receives an id it cannot resolve and throws. It also costs a wasted render pass of the subtree on every items change.

The state derived from the parameters is now computed during render, so the items rendered in that pass always match props.items.

  • MinimalTreeViewStore.applyParametersDuringRender applies the new state without notifying the subscribers, since React does not allow scheduling updates on other components while rendering. flushRenderUpdate notifies in a layout effect, which only matters for the subscribers that bailed out of that render.
  • The default treeId now comes from useId and is passed to the store through the parameters. It used to be generated by the first sync, which no longer runs on mount. This also removes a full re-render of every item right after mount.
  • @mui/x-internals/store is untouched, so no other package is affected.

Repro of the reported behavior, before the fix, logging the ids the item slot is rendered with after the child is removed:

['1', '1', '1.1', '1.1', '1', '1']

The first pass is the stale one, doubled by StrictMode. After the fix, 1.1 is gone.

Testing

Added a regression test in RichTreeView.test.tsx. It lives there rather than in the describeTreeView suites because that harness supplies its own slotProps.item, and the stale pass is only observable when the callback identity changes on each render. When it does not, RichTreeViewItem bails out of the re-render through React.memo and the test would pass without the fix.

x-tree-view and x-tree-view-pro suites pass, including the focus, keyboard navigation and memoization tests that depend on when the items state is applied.

@code-infra-dashboard

code-infra-dashboard Bot commented Aug 13, 2026

Copy link
Copy Markdown

Deploy preview

https://deploy-preview-23337--material-ui-x.netlify.app/
QR code for https://deploy-preview-23337--material-ui-x.netlify.app/

Bundle size

Bundle Parsed size Gzip size
@mui/x-data-grid 0B(0.00%) 0B(0.00%)
@mui/x-data-grid-pro 0B(0.00%) 0B(0.00%)
@mui/x-data-grid-premium 0B(0.00%) 0B(0.00%)
@mui/x-charts 0B(0.00%) 0B(0.00%)
@mui/x-charts-pro 0B(0.00%) 0B(0.00%)
@mui/x-charts-premium 0B(0.00%) 0B(0.00%)
@mui/x-date-pickers 0B(0.00%) 0B(0.00%)
@mui/x-date-pickers-pro 0B(0.00%) 0B(0.00%)
@mui/x-tree-view 🔺+575B(+0.83%) 🔺+174B(+0.85%)
@mui/x-tree-view-pro 🔺+575B(+0.43%) 🔺+166B(+0.42%)
@mui/x-scheduler 🔺+579B(+0.15%) 🔺+185B(+0.18%)
@mui/x-scheduler-premium 🔺+579B(+0.11%) 🔺+185B(+0.12%)
@mui/x-chat 0B(0.00%) 0B(0.00%)
@mui/x-license 0B(0.00%) 0B(0.00%)

Details of bundle changes


Check out the code infra dashboard for more information about this PR.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d379decd6f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

>(parameters: MinimalTreeViewParameters<R, Multiple>): MinimalTreeViewState<R, Multiple> {
return {
treeId: undefined,
treeId: createTreeViewDefaultId(),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Avoid generating counter IDs during render

When a Tree View is rendered without an explicit id in SSR, this now increments the module-global globalTreeViewDefaultId during render and serializes that value into the HTML. In a long-lived server process the counter carries over between requests, while the browser starts from 0 during hydration, so later requests can server-render mui-tree-view-2 but hydrate as mui-tree-view-1, causing hydration mismatches and inconsistent item ids/ARIA relationships. The previous post-mount generation avoided putting these counter IDs in SSR output; keep render-time ids deterministic per request, e.g. via React useId, or defer default id generation until after hydration.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch, this was a real regression. globalTreeViewDefaultId is module scoped, so a long lived server process keeps counting across requests while the browser restarts from zero, and every request after the first would hydrate with a mismatched id.

Fixed in c24ac71: the default id now comes from useId (@mui/utils/useId, same as the Data Grid) and is passed to the store through the parameters, so it is stable across server and client. createTreeViewDefaultId is gone.

@Anexus5919

Copy link
Copy Markdown
Contributor Author

@noraleonte could you take a look at this one, or point me to the right person? The store now derives its state from the props during render instead of in a layout effect, so I would like a maintainer to confirm the timing change is acceptable before this goes further.

Unrelated but worth flagging: useEventCalendar syncs its store the same way, in a layout effect:

https://github.com/mui/mui-x/blob/master/packages/x-scheduler-internals/src/use-event-calendar/useEventCalendar.ts#L26-L29

so the Scheduler state is one render behind its parameters for the same reason. Out of scope here, but I can open a separate issue if that is useful.

cc @JCQuintas

@noraleonte noraleonte left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for digging into this. The diagnosis is correct and the repro output made it easy to follow.

I don't want to take the change to the store though. store.state is also read from outside React, by apiRef, event handlers and plugins, and today it always matches what is on screen. Deriving during render gives that up: a render that React starts and then drops would leave the store describing a tree that was never shown. It would also move us away from how the Scheduler, Charts and Data Grid all work.

There is a smaller bug in the current version too. store.parameters !== storeParameters is already true on the first render for SimpleTreeView, because its constructor normalises the parameters. So applyParametersDuringRender runs on mount and the items: EMPTY_ARRAY normalisation is lost.

The route I would rather explore is to keep the store synced in an effect and give React a separate view of the state to render from, so the committed state stays untouched for everything outside React. I still need to work out the details.

Whatever we end up with has to fit these constraints:

  • no breaking change rn, and no change to existing public APIs
  • apiRef keeps returning what is on screen
  • works with virtualization and with lazy loading (and other existent features)
  • the Scheduler can use the same solution, since it has the same code

The useId part is separate and I like it. If you split it into its own PR I can review that one searately.

@Anexus5919

Anexus5919 commented Aug 14, 2026

Copy link
Copy Markdown
Contributor Author

Thanks, this is fair and I agree with all of it.

the SimpleTreeView bug is real, and a bit worse than you described: applyParametersDuringRender sits on the base class and calls the state builder directly, so it bypasses the SimpleTreeViewStore.updateStateFromParameters override entirely, not just on mount. store.parameters.items ends up undefined for every render-path update. It only avoids blowing up because shouldIgnoreItemsStateUpdate returns true for that store.

I take the point on apiRef too. I had weighed the dropped-render case as self correcting on the next render, but that ignores that store.state is a public surface through apiRef, so a handler firing in that window reads a tree that was never on screen. Your framing is the right one.

also useId is split out into #23341.

I will leave this one open and hold off on a rewrite until you have settled the direction. One idea in case it is useful: rather than a second copy of the state, keep store.state exactly as it is today, effect synced and committed, and only change what getSnapshot() returns, so React reads pendingState ?? state while everything outside React keeps reading state. The layout effect promotes pending to committed and notifies. No call site changes, no duplicated derivation, and it should port to the Scheduler unchanged. Virtualization reads only through useStore so it would pick this up for free, and lazy loading never takes the parameters path at all since shouldIgnoreItemsStateUpdate is true whenever dataSource is set.

The detail I have not solved is clearing pendingState after a render React drops. Happy to prototype it if you want, or to just implement whatver you land on.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[tree view] Stale item is being rendered in the tree view

2 participants