npm react-redux 9.4.0-alpha.2
v9.4.0-alpha.2

7 hours ago

This alpha release fixes two bugs in useSignalSelector: stale values rendered in the gap between a dispatch and the store notification, and tracking proxies leaking out of values a selector held onto from an earlier run. It also cuts the cost of spreading or enumerating objects inside selectors, and adds test coverage for using useSignalSelector with RTK Query's hooks. The stock Provider and useSelector are unchanged.

npm install react-redux@alpha

This is still an alpha. Please try it out and give us feedback! The alpha.0 notes cover what useSignalSelector is and how to opt in.

Changelog

Renders between a dispatch and the store notification now see current state

Once a useSignalSelector hook had its full dependency graph built, its getSnapshot returned a cached result that only updated when the store notified subscribers. Normally that happens synchronously inside dispatch(), but there are several cases where React renders the component before that notification arrives:

  • RTK's configureStore includes the autoBatchEnhancer, which delays notifications for batched actions (like RTK Query's pending and fulfilled actions) until the next animation frame
  • a parent re-renders for its own reasons in that gap
  • a useSelector component and a useSignalSelector component in the same tree render in the same pass
  • an action dispatched from a layout effect, which React picks up in its post-commit snapshot check

In all of these, useSignalSelector could render the previous value, and a useSelector and a useSignalSelector reading the same field could disagree within one render.

getSnapshot now checks whether store.getState() has moved since the cached result was computed. If it has, it re-runs the selector against the current state. useSignalSelector also now passes React a new getSnapshot when the selector reference changes, matching useSelector, so React's post-commit consistency check runs in the same cases it does for useSelector.

Values held from an earlier selector run are unwrapped

The render-time fallback path above runs the selector against raw state, and we assumed its result could never contain tracking proxies. That assumption was wrong. A selector that holds onto a value from an earlier tracked run, via a closure or a ref, can return a proxy from that earlier run.

RTK Query does exactly this: its query hooks keep the last result so they can show the previous data while a new arg loads. With a stable selectFromResult, the component received a proxy of the previous data during that window instead of the raw state object. Results from that path are now unwrapped the same way as the main path.

Spreading and enumerating objects is cheaper

Spreading an object in a selector ({ ...state.user }), or using Object.keys/values/entries, Object.assign, JSON.stringify, or for...in, previously recorded a dependency on the object's key list plus one dependency per field. Reading every field of an immutably-updated object is equivalent to depending on the object's identity, so a full enumeration of a nested object now collapses into a single identity dependency on that object.

Partial enumerations still track precisely: Object.keys(obj).length, or reading only some fields after enumerating, keeps field-level dependencies. The root state object and arrays are excluded, since their tracking already works differently.

This mostly shows up with RTK Query, whose selectors spread each cache entry. In our RTK Query benchmark, useSignalSelector time per dispatch dropped by about 20-30%.

RTK Query compatibility

We've added an interop test suite that runs RTK Query's hooks on top of useSignalSelector, via a custom createApi:

import {
  buildCreateApi,
  coreModule,
  reactHooksModule,
} from '@reduxjs/toolkit/query/react'
import { useDispatch, useSignalSelector, useStore } from 'react-redux'

export const createApi = buildCreateApi(
  coreModule(),
  reactHooksModule({
    hooks: { useDispatch, useSelector: useSignalSelector, useStore },
  }),
)

The tests cover query phases, selectFromResult, mutations and invalidation, skip, arg changes, and render counts, which match useSelector exactly. The types line up too: useSignalSelector satisfies the hooks.useSelector option type with no casts.

Writing those tests turned up two bugs on the RTK side, both fixed in RTK 2.13.0. useQueryState passed a new inline selector to useSelector on every render, which forced useSignalSelector to re-run the full tracked selector on every render. It also let isSuccess flip on unrelated re-renders during a refetch after an error. With 2.13.0, useSignalSelector selector runs per dispatch in our RTK Query benchmark showed a ~30% drop vs alpha.2. We recommend 2.13.0+ if you try this setup.

Performance

10 s per scenario, this release vs 9.3.0, Chrome, react-dom/profiling, RTK 2.12.

Scenario Script Blocked Notes
tree-view -54% -77%
many-components-many-slices -31% -54%
realistic-slice-count -27% -80%
deeptree-nested-hooks -24% -61%
entity-list -22% -45%
rapid-dispatch -21% -65%
multi-selector-component -20% -82%
price-ticker -19% -58% stock drops frames
selective-update -15% -64%
forms -12% -80%
entity-list-array -9% -62%
many-components-same-slice +3% +96% flat; work moved into dispatch
rtkq-separate-queries +4% +21%
one-component-many-slices +28% +32% 20,000 top-level keys
derived-selectors +53% +144%

Render counts match 9.3.0 within noise, except where stock drops frames under load.

rtkq-separate-queries moved from +9% in alpha.1 to +4%. derived-selectors and one-component-many-slices remain the two scenarios where useSignalSelector is slower than useSelector. Their numbers vary a lot between runs, and before/after runs of this release's changes showed no measurable difference for either.

What's Changed

Full Changelog: v9.4.0-alpha.1...v9.4.0-alpha.2

Don't miss a new react-redux release

NewReleases is sending notifications on new releases.