adobe / adobe/react-spectrum

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

Open
#10,477 3 comments 1 reaction 0 assignees View on GitHub
Dominant language
TypeScript
Stars
15.9k
Forks
1.6k
Avg merge
3d 9m
Merged PRs (30d)
59

Description

### 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):

- https://github.com/adobe/react-spectrum/issues/7734 (comment already notes keyboard nav re-renders the whole table because of focus manager state)

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_

Contributor guide

Open the contributing guide

Research direction

Start with packages/react-stately/src/selection/useMultipleSelectionState.ts, then trace useSelectableItem, useTableState, UNSTABLE_useFilteredTableState, and TableStateContext. Reproduce the issue in a RAC Table example with React DevTools profiling and inspect the existing CollectionBuilder and ToastQueue subscription patterns. Done means focus changes update only the old and new item, preserve virtualizer and screen-reader behavior, and avoid rebuilding the collection or changing context identity.

Written by the indexing model from the issue text.

Assessment

Tech stack
react, typescript
Domain
accessibility, frontend, performance
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
55/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.