Repository navigation
perf(List): skip re-rendering unchanged list items - #3581
Merged
Merged
Conversation
The list model is rebuilt on every render of List, and it was the context value. Every useList consumer re-rendered with each render, including the ones inside items that skip their own render. The context now holds a store that keeps its identity. useList subscribes to it and still re-renders with every new model, so its consumers behave as before. The new useListSelector re-renders only when the value it reads changes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ListItemView and ListItemViewContent built a new props context on every render, so every item's content re-rendered with them. Both contexts hold only constants and are module-level now. ListItemView reads the view mode through useListSelector. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
react-aria re-renders every GridList item whenever the GridList renders, Flow's item memo or not. The items now return their last element while the loading state, onAction, the infinite scroll trigger, every entry's id and data and the list props stay equal and every item renders the same by the item memo's rule. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Contributor
🚀 Preview DeploymentPreview environments are ready:
Images:
|
Lisa18289
reviewed
Oct 27, 2026
The default list context now holds a store without a list, so both hooks return undefined again instead of throwing. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Lisa18289
reviewed
Oct 27, 2026
… statically The store behind `useListSelector` received the new model during render, so a render React throws away left an uncommitted model in it. It now gets the model in the layout effect, and `useList` reads the render-time model from the list context again. `ListItemView` read the view mode inside the button's dynamic `size`, which only runs while the button's props context still holds that `size`. Now that the read is a hook, a button whose context dropped or regained it rendered with a different hook count. `ListItemView` reads the view mode once and hands the button a static size. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…skip-unchanged # Conflicts: # packages/components/src/components/List/List.browser.test.tsx
Filled before the remote Field was wired up, the value never reached the remote form, so its required rule blocked the submit on slow runners. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
mfal
enabled auto-merge
October 27, 2026 11:33
Lisa18289
approved these changes
Oct 27, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
A re-render of a list's parent, or a refetch that hands
List.StaticDataa new array of the same items, no longer re-renders the items of a list or tile view. Three changes:Listrebuilds its model on every render, and the model is the list context's value. Every consumer re-rendered with each render, including the ones inside items that skip their own render:ListItemView, and everyButtonin an item through itssizedynamic prop. A second context now holds a store that receives the model once the list has committed. An internaluseListSelectorreads it and re-renders only when the value it reads changes;ListItemViewuses it for the view mode.useListis unchanged.ListItemViewandListItemViewContentbuild their props contexts once. Both held only constants (tunnels, class names, anOptionsButtonelement, dynamic props) but were rebuilt on every render, which handed every item's content a new props context.ListItemViewkeeps one per view mode and hands the item buttons a static size.Itemsskips rendering while nothing it shows has changed. react-aria re-renders everyGridListItemwhenever theGridListrenders, Flow's item memo or not.Itemsnow returns its last element while these stay equal:onAction, the infinite scroll trigger,accordion,tileMaxWidthItemView.rendersSameFor)The table counterpart is #3575.
It builds on #3579: without its stable
ClearPropsContext, every render ofListhands the items a new props context, and they re-render despite the changes here.Related: #3575
Effect
Measured in mStudio, dev build, React Profiler render time and number of component renders summed over one refetch, median of 7. Baseline is
nextwith #3575 and #3579; after adds this PR.List.StaticDataRefetching only the projects' or mail addresses' list query was already ~1 ms with #3575, since their loaders keep the same items.
Selecting an item in Nexus' ticket list (list view, 100 entries, selection controlled by the parent), same baseline, dev build, render time summed over all commits of one click, median of 6 real mouse clicks:
Before, the click rendered every item twice. What remains is mostly the one render of the list and each item's react-aria
Buttonre-rendering on its own state.Tests
List.browser.test.tsx(Item re-renders): counts react-aria's item renders through anItemsGridListItemview whose children function runs on each of them, and the item content's renders through a component reading the props context. Two parent re-renders render neither; a change torenderDependenciesstill renders the item.The test fails without the
Itemschange and without the stable list store (2 item renders instead of 0, verified by reverting each). A second test toggles a button's size through a nested props context inside an item, which used to change the button's hook count. The existing List tests for filters, saved settings, the date range filter and the view mode cover theuseListconsumers that still have to follow every change.NestedFormSubmit.browser.test.tsxnow waits for the Modal before typing. Typed into before the remote Field was wired up, the value never reached the remote form, and its required rule blocked the submit. The race is in the test, not in this PR: under CPU load it fails onnextas well.🤖 Generated with Claude Code