Skip to content

[Table] Focusing a cell re-renders the entire table (focusedKey) #10477

Description

@coolassassin

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:

  1. Stop calling useState from setFocusedKey (keep the ref).
  2. Add subscribe on SelectionManager (or the selection state) for focus changes.
  3. 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.
  4. 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.
  5. 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

  1. Open any RAC Table example, e.g. https://react-spectrum.adobe.com/react-aria/Table.html (or a Storybook table story on main).
  2. Open React DevTools → Profiler or enable "Highlight updates when components rerender".
  3. Click a cell, then another cell. Optionally set selectionMode="none" so selection is not in play.
  4. 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions