Provide a general summary of the issue here
Clicking or keyboard-focusing a table cell re-renders the entire table. focusedKey is stored as React state on the collection root, and that update replaces the TableState context value, so every Row and Cell re-renders.
🤔 Expected Behavior?
Changing the focused cell should update only the previously focused and newly focused cell/row (roving tabIndex, focus styles). The collection should not rebuild. Unrelated rows and cells should not re-render.
Keyboard navigation, virtualizer persistedKeys, and screen-reader behavior should stay the same.
😯 Current Behavior
Focusing a cell (mouse click or arrow keys) calls selectionManager.setFocusedKey. In useMultipleSelectionState, focusedKey already lives on a ref for synchronous reads, but setFocusedKey also calls useState solely to force a React re-render:
https://github.com/adobe/react-spectrum/blob/main/packages/react-stately/src/selection/useMultipleSelectionState.ts
That state lives in the RAC Table root. The render then produces a new TableState object (useTableState / UNSTABLE_useFilteredTableState return a new object every time) and a new SelectionManager (memoized on selectionState identity). That object is provided as TableStateContext.
Every Row and Cell does useContext(TableStateContext), so they all re-render. Each useSelectableItem also has a useEffect that depends on manager.focusedKey, so those effects run for every item.
The collection document itself does not need to change on focus. The expensive work is the context identity change plus a full tree of collection items.
This is easy to see with React DevTools → "Highlight updates when components rerender" on any RAC Table example: one click flashes the whole table.
Related (not the same bug):
Docs example: https://react-spectrum.adobe.com/react-aria/Table.html
💁 Possible Solution
Keep the public hook API as-is (useTableState, SelectionManager.focusedKey, TableStateContext). This is an internal notification problem, not a new state model.
focusedKey is already a ref. The extra useState is only a "please re-render the root" signal. Replacing that signal with a subscription — the same pattern as CollectionBuilder and ToastQueue (useSyncExternalStore) — fits the existing architecture:
react-stately stays the state layer; the public object returned by useTableState does not change
react-aria / RAC stay the behavior and glue layers
- no new public store API, no extra dependencies
- collection construction is already segmented this way; focus was left on a root
useState
Concretely:
- Stop calling
useState from setFocusedKey (keep the ref).
- Add
subscribe on SelectionManager (or the selection state) for focus changes.
- In
useSelectableItem, subscribe with a boolean snapshot manager.focusedKey === key, so only the old and new item re-render (Object.is(false, false) skips the rest). Optionally notify only those two keys.
- Keep
TableState / TableStateContext identity stable across focus changes. Otherwise the parent still re-renders, provides a new context value, and every useContext(TableStateContext) consumer re-renders anyway.
- Subscribe the virtualizer separately for
persistedKeys, so keyboard focus can still mount an off-screen item before focus().
React Compiler will not fix this. Compiler memoization cannot skip a useContext update when the context value is a new object. TableState is a new object on every root render, TableStateContext always changes, and every Row/Cell reads that context during render (tabIndex, isFocused, selection, collection lookups). Memoizing the cell body does not help if the cell function itself re-runs because context changed.
The same focusedKey path is shared by Table, ListBox, GridList, Tree, etc. A fix probably belongs in the selection layer, not only in RAC Table.
If selectionMode !== 'none', click also updates selectedKeys. That is a separate, legitimate React update (checkboxes, aria-selected). This issue is specifically the focus update, which is observable even with selectionMode="none".
🔦 Context
We use large RAC tables (many rows × many cells). A single click currently re-renders the entire grid. That is the dominant cost of focusing a cell, even when the collection and cell contents are unchanged.
Virtualization helps when most rows are unmounted, but every visible cell still re-renders, and non-virtualized tables pay the full cost. We already isolated cell data updates so cells can subscribe to their own data; focus management undoes that by invalidating the shared table state on every click.
🖥️ Steps to Reproduce
- Open any RAC Table example, e.g. https://react-spectrum.adobe.com/react-aria/Table.html (or a Storybook table story on main).
- Open React DevTools → Profiler or enable "Highlight updates when components rerender".
- Click a cell, then another cell. Optionally set
selectionMode="none" so selection is not in play.
- Observe: the whole table (all rows and cells) re-renders on each focus change.
Expected in the profiler: two items (old + new). Actual: every Row and Cell.
Alternatively, in Storybook: react-aria-components → Table, same DevTools steps.
Version
Reproduced on main (react-aria-components@1.19.0, react-stately / react-aria from this repo). Same architecture is in current published RAC Table.
What browsers are you seeing the problem on?
Chrome
If other, please specify.
No response
What operating system are you using?
MacOS
🧢 Your Company/Team
No response
🕷 Tracking Issue
No response
Provide a general summary of the issue here
Clicking or keyboard-focusing a table cell re-renders the entire table.
focusedKeyis stored as React state on the collection root, and that update replaces theTableStatecontext value, so everyRowandCellre-renders.🤔 Expected Behavior?
Changing the focused cell should update only the previously focused and newly focused cell/row (roving
tabIndex, focus styles). The collection should not rebuild. Unrelated rows and cells should not re-render.Keyboard navigation, virtualizer
persistedKeys, and screen-reader behavior should stay the same.😯 Current Behavior
Focusing a cell (mouse click or arrow keys) calls
selectionManager.setFocusedKey. InuseMultipleSelectionState,focusedKeyalready lives on a ref for synchronous reads, butsetFocusedKeyalso callsuseStatesolely to force a React re-render:https://github.com/adobe/react-spectrum/blob/main/packages/react-stately/src/selection/useMultipleSelectionState.ts
That state lives in the RAC
Tableroot. The render then produces a newTableStateobject (useTableState/UNSTABLE_useFilteredTableStatereturn a new object every time) and a newSelectionManager(memoized onselectionStateidentity). That object is provided asTableStateContext.Every
RowandCelldoesuseContext(TableStateContext), so they all re-render. EachuseSelectableItemalso has auseEffectthat depends onmanager.focusedKey, so those effects run for every item.The collection document itself does not need to change on focus. The expensive work is the context identity change plus a full tree of collection items.
This is easy to see with React DevTools → "Highlight updates when components rerender" on any RAC Table example: one click flashes the whole table.
Related (not the same bug):
Docs example: https://react-spectrum.adobe.com/react-aria/Table.html
💁 Possible Solution
Keep the public hook API as-is (
useTableState,SelectionManager.focusedKey,TableStateContext). This is an internal notification problem, not a new state model.focusedKeyis already a ref. The extrauseStateis only a "please re-render the root" signal. Replacing that signal with a subscription — the same pattern asCollectionBuilderandToastQueue(useSyncExternalStore) — fits the existing architecture:react-statelystays the state layer; the public object returned byuseTableStatedoes not changereact-aria/ RAC stay the behavior and glue layersuseStateConcretely:
useStatefromsetFocusedKey(keep the ref).subscribeonSelectionManager(or the selection state) for focus changes.useSelectableItem, subscribe with a boolean snapshotmanager.focusedKey === key, so only the old and new item re-render (Object.is(false, false)skips the rest). Optionally notify only those two keys.TableState/TableStateContextidentity stable across focus changes. Otherwise the parent still re-renders, provides a new context value, and everyuseContext(TableStateContext)consumer re-renders anyway.persistedKeys, so keyboard focus can still mount an off-screen item beforefocus().React Compiler will not fix this. Compiler memoization cannot skip a
useContextupdate when the context value is a new object.TableStateis a new object on every root render,TableStateContextalways changes, and everyRow/Cellreads that context during render (tabIndex,isFocused, selection, collection lookups). Memoizing the cell body does not help if the cell function itself re-runs because context changed.The same
focusedKeypath is shared by Table, ListBox, GridList, Tree, etc. A fix probably belongs in the selection layer, not only in RACTable.If
selectionMode !== 'none', click also updatesselectedKeys. That is a separate, legitimate React update (checkboxes,aria-selected). This issue is specifically the focus update, which is observable even withselectionMode="none".🔦 Context
We use large RAC tables (many rows × many cells). A single click currently re-renders the entire grid. That is the dominant cost of focusing a cell, even when the collection and cell contents are unchanged.
Virtualization helps when most rows are unmounted, but every visible cell still re-renders, and non-virtualized tables pay the full cost. We already isolated cell data updates so cells can subscribe to their own data; focus management undoes that by invalidating the shared table state on every click.
🖥️ Steps to Reproduce
selectionMode="none"so selection is not in play.Expected in the profiler: two items (old + new). Actual: every
RowandCell.Alternatively, in Storybook:
react-aria-components→ Table, same DevTools steps.Version
Reproduced on
main(react-aria-components@1.19.0,react-stately/react-ariafrom this repo). Same architecture is in current published RAC Table.What browsers are you seeing the problem on?
Chrome
If other, please specify.
No response
What operating system are you using?
MacOS
🧢 Your Company/Team
No response
🕷 Tracking Issue
No response