reduxjs/react-redux
 Watch   
 Star   
 Fork   
11 days ago
react-redux

v9.4.0-alpha.2

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

  • Add prototype signals+path-tracking implementation by @markerikson in #2318

Full Changelog: https://github.com/reduxjs/react-redux/compare/v9.4.0-alpha.1...v9.4.0-alpha.2

16 days ago
react-redux

v9.4.0-alpha.1

This alpha release fixes a bug where useSignalSelector stopped updating when a memoized Reselect selector was shared between components or called with the same state twice, changes how array methods called on tracked state return their elements, and adds a per-evaluation read cache plus a dev-mode warning for selectors that repeatedly read the same field in a loop. The stock Provider and useSelector are unchanged.

npm install react-redux@alpha
yarn add react-redux@alpha
pnpm add react-redux@alpha

This is still an alpha. Please try it out and give us feedback, especially on real apps using Reselect and entity adapters. The alpha.0 notes cover what useSignalSelector is and how to opt in.

Changelog

Shared and memoized selectors now update correctly

After the alpha.0 release, we got feedback asking about two cases: reusing the same memoized selector instance across components, and conditional logic inside of a selector altering what fields it reads.

In alpha.0, every useSignalSelector evaluating against the same state object received the same tracking proxy. Reselect's argsMemoize (the weakMapMemoize default, and lruMemoize) keys its cache on argument identity, so the second evaluation with that proxy was a cache hit: the input selectors never ran, no state properties were read, and the hook recorded zero dependencies. That hook then never re-rendered.

This affected more than the reported case of two components sharing one createSelector. A single component with an inline wrapper (useSignalSelector(s => selectSummary(s).total)), a parametrized selector (selectScaled(s, factor)), nested memoized inputs, and components mounted after the cache was warm all went deaf after their first update.

Every tracked evaluation now gets a fresh root proxy. The consequence is that argsMemoize misses on every tracked run and the input selectors execute each time. The result function still memoizes on the input values, so derived objects keep stable identities and are shared across components as before. Hand-rolled caches that never read state (if (cached) return cached) still cannot be tracked, but those are equally stale under useSelector.

We also reviewed conditional selectors (s => s.toggle ? s.foo : s.bar). Turns out those already worked :) Dependencies are re-recorded from scratch on every evaluation, so switching branches drops the old branch and picks up the new one.

Array methods return raw elements with identity dependencies

Once we fixed the cache bug and re-ran the benchmarks, we saw that the alpha.0 benchmark results were flawed. In particular, the derived-selectors benchmark had appeared to be a significant perf improvement over stock useSelector. However, with the cache fix in place, the correct behavior had more overhead. More hooks actually received updates, and result functions that iterated arrays of tracking proxies (selectAll, then .filter over the result) paid one proxy trap and one dependency registration per element read, per hook, per dispatch. Inside a Reselect result function that precision buys nothing, since the function re-runs on input identity anyway.

To address this, find, findLast, filter, slice, and now map return raw state objects instead of proxies, each with a single identity dependency on the element's path (items.{id:42} for arrays with a key field, items.3 otherwise). Because state is immutable, an identity dependency never misses a change to that element; it can only over-run when a field you did not read changes. Fields read off a returned element are not tracked separately. Direct index access (state.items[0].name) keeps field-level precision.

For map, callbacks that return the element itself, a primitive, or a proxy obtained elsewhere (ids.map(id => entities[id])) get precise dependencies. Callbacks that construct new objects fall back to one dependency on the whole array. forEach, reduce, and flatMap are still not overridden and depend on the whole array.

One side effect: elements returned from these methods compare correctly with === against references held outside the selector, without unwrap().

Repeated reads are memoized, with a dev warning

A selector like comments.filter(c => c.postId === post.id) reads post.id once per comment. When post is a tracking proxy, each read is a trap call. Within one evaluation that read is idempotent: the value cannot change and the dependency is already linked. Each proxy now remembers its last primitive read for the current evaluation and returns it directly on repeat.

In development, a selector that re-reads the same field 100+ times in a single evaluation logs a warning once, naming the path and suggesting hoisting the value into a local before the loop. Hoisting is still cheaper than the memo, but the memo recovers most of the difference without code changes.

Performance

Overall, the branch is still a significant perf win across most of our benchmark scenarios, and it now correctly handles additional common usage scenarios that we'd expect to see in real app code.

10 s per scenario, this release vs 9.3.0, Chrome, react-dom/profiling. "Script" is total scripting time; "blocked" is main-thread time inside dispatch().

Scenario Script Blocked Notes
tree-view -55% -76%
entity-list-array -49% -85% stock drops frames
many-components-many-slices -37% -56%
deeptree-nested-hooks -28% -63%
realistic-slice-count -26% -80%
price-ticker -21% -59%
forms -20% -81%
multi-selector-component -17% -84%
selective-update -16% -66%
rapid-dispatch -14% -65%
entity-list +2% -21% noise
many-components-same-slice +3% +93% flat; work moved into dispatch
rtkq-separate-queries +9% +25%
one-component-many-slices +19% +22% 20,000 top-level keys
derived-selectors +36% +119% see below

Render counts match 9.3.0 within noise on every scenario.

The alpha.0 notes reported derived-selectors at -87%. That number was wrong: 150 of the 200 hooks in that scenario were affected by the cache bug and never re-rendered after their first update. With the fix, every hook re-runs its input selectors through the tracking proxy on every relevant dispatch, and that is the cost shown.

one-component-many-slices improved from +36% in alpha.0 to +19%.

Bundle cost is unchanged from alpha.0: zero for apps that do not import useSignalSelector, about +7 kB min+gz for apps that do.

Docs

The useSignalSelector API page now covers array-method return values, the Reselect interaction, and the repeated-read behavior.

What's Changed

  • Add prototype signals+path-tracking implementation by @markerikson in #2318

Full Changelog: https://github.com/reduxjs/react-redux/compare/v9.4.0-alpha.0...v9.4.0-alpha.1

18 days ago
react-redux

v9.4.0-alpha.0

This alpha release adds an opt-in useSignalSelector hook and SignalProvider component that only re-run selectors whose state dependencies actually changed, plus a react-redux/signals entry point for adopting them app-wide, and restructures the hooks API docs into separate pages.

npm install react-redux@alpha
yarn add react-redux@alpha
pnpm add react-redux@alpha

This is an early alpha. The implementation is complete and heavily tested, but it has only been tried in a small number of real apps. Please try it out and give us feedback! We'd especially like to hear about performance differences after adopting useSignalSelector (render counts and dispatch timings), any differences or issues with selector behaviors (including edge cases in proxy wrapper access), and bundle sizes.

Changelog

useSignalSelector and SignalProvider

Redux is a simple event emitter, with O(n) subscriber behavior. Every dispatched action triggers a loop over all subscriber callbacks, which in turn normally call getState() and a selector to determine if that useSelector or connect needs to re-render. React-Redux moves some of the tracking to be internal to itself and optimizes some subscription aspects, but it's fundamentally still calling O(n) callbacks every time. An app with 2,000 mounted useSelector hooks runs 2,000 selector functions per dispatch, then compares 2,000 results, even when the action touched one field. That work is usually cheap per selector but it scales linearly with app size and is the dominant per-dispatch cost in large apps.

As with React, this works fine for most apps, but breaks down at large scales. I've talked with teams that had 20,000 connected components, and at that scale just running the callbacks themselves is a major perf issue.

Proxies allow tracking access to nested fields. Signals allow automatically deriving dependencies. More magic, but signals libs automatically only update the dependencies that actually rely on a given updated value.

We can't and won't change the semantics of Redux in terms of immutable updates, subscribing to the store, etc. But, we can leverage some of these techniques inside of React-Redux, invisibly to the end user.

useSignalSelector tracks which state paths each selector actually reads. Selectors run against a tracking proxy over the Redux state, which records property accesses like state.todos[3].text. On each dispatch, SignalProvider diffs the previous and next state trees and fires signals for the paths that changed. Only selectors that depend on a changed path re-run. Everything else is skipped entirely: no selector call, no equality check, no render.

Internal implementation details Internally this is a path-keyed signal graph built on the `alien-signals` propagation algorithm. Signals are created lazily as selectors touch paths and are released when the last subscriber unmounts, so memory tracks what is mounted rather than the size of the state tree. Dependency tracking is two-tiered: at mount, a hook records only which top-level state keys it reads and registers cheaply against those. It builds its full deep dependency graph the first time one of those keys changes. Hooks that never see a relevant dispatch never pay for the deep graph.

You can adopt this incrementally. SignalProvider is a drop-in replacement for Provider, and useSignalSelector has the same signature and options as useSelector and passes the entire existing useSelector test suite, so you can switch individual components:

import { SignalProvider, useSignalSelector } from 'react-redux'

function TodoText({ id }: { id: number }) {
  const text = useSignalSelector((state) => state.todos.byId[id].text)
  return <span>{text}</span>
}

Alternately, you can alias the whole app at once. The new react-redux/signals entry point exports SignalProvider as Provider and useSignalSelector as useSelector, so existing code and third-party libraries that import from react-redux pick up the new implementation without edits:

// vite.config.ts
export default defineConfig({
  resolve: {
    alias: { 'react-redux': 'react-redux/signals' },
  },
})

connect, useDispatch, useStore, and stock useSelector continue to work unchanged inside a SignalProvider. Reselect selectors work as expected, including weakMapMemoize. Zombie-child and stale-props behavior matches useSelector. SSR produces identical markup.

Performance

We benchmarked the branch against 9.3.0 across 16 scenarios in our react-redux-benchmarks suite, 10 seconds each, using React DevTools profiling builds so render counts are exact.

Total script time improved in 13 of 16 scenarios. Time spent inside dispatch() dropped 49–88% in 12 of 16. Render counts matched 9.3.0 within a few percent everywhere except the scenarios where 9.3.0 was dropping frames under load, where signals renders more often because it keeps up. Some representative numbers:

Scenario Script Time in dispatch() Avg dispatch Renders
derived-selectors -87% -70% 1.47 → 0.44 ms 347 → 30
tree-view -55% -73% 9.1 → 2.3 ms ≈
multi-selector-component -28% -88% 6.5 → 0.9 ms ≈
realistic-slice-count -28% -82% 1.15 → 0.21 ms 768 → 768
many-components-many-slices -30% -49% 8.1 → 3.7 ms ≈
price-ticker -19% -58% 24.3 → 6.2 ms 247 → 410
forms -20% -80% 0.92 → 0.18 ms ≈

The gains come from skipping work: in derived-selectors, 9.3.0 re-runs every derived selector on each dispatch and renders 347 times; signals renders 30 times, and the extra renders in 9.3.0 were all wasted.

There are costs. A tracked selector evaluation is 3–4x slower than a raw one because it runs through a proxy, so a selector that does re-run costs more than before. Each dispatch pays a diff of the previous and next state; this is proportional to how much of the tree actually changed, since unchanged subtrees are skipped by reference. Mount is a few milliseconds slower on small apps and faster on large component trees. The one scenario that regresses is a single component whose selector reads thousands of top-level state keys (+36% script), where there is no precision to recover and the diff is pure overhead.

The biggest tradeoff is bundle size. Opting in costs about 22.7 kB minified / 7.2 kB min+gzip more (32.9 kB / 10.9 kB total), which includes the alien-signals runtime. This is an intentional tradeoff - the extra logic and internal complexity requires more code. Stick with the standard useSelector as a default - useSignalSelector is specifically meant for larger apps that will have a net benefit from its performance characteristics.

The good news is that apps that keep using Provider and useSelector pay nothing for this feature. Stock React-Redux in a Vite 8 build is 10.2 kB minified / 3.7 kB min+gzip, unchanged from 9.3.0, and we have a CI check confirming the signals code tree-shakes out. Note that the raw dist/ files grew a lot because the signals code ships in the same file; bundle-size bots that measure dist/ will report a large increase that does not reflect what your app ships.

Constraints and tradeoffs

Path tracking works by observing property reads and diffing plain data. That imposes rules that stock useSelector does not:

  • State must be plain, immutably-updated objects and arrays. This is already what Redux requires, but useSelector tolerates violations that useSignalSelector does not. If a reducer mutates an object in place and returns the same reference, the diff sees no change and dependent components will not update. Map, Set, Date, and class instances are tracked by reference only: replacing one triggers updates, mutating one does not.
  • Selectors must not mutate state. In development, writing to state inside a selector (state.items.sort(), delete state.x, Object.assign(state.y, ...)) throws a TypeError with a clear message. Stock useSelector silently allows this. Use slice().sort() or toSorted().
  • Selectors receive a proxy, not the raw state. Values you return that are objects are unwrapped automatically, so useSignalSelector results are the same references stock useSelector would return and are safe to use as useEffect deps or Map keys. If you pass state objects out of the selector by some other route, unwrap() converts them back to raw state.
  • useSignalSelector(state => state) never updates. Returning the root state (or otherwise reading nothing specific from it) gives the tracker nothing to depend on. Select the fields you need.
  • Array iteration tracks one level deep. state.todos.map(t => t.text) depends on the text column across all todos, not on every property of every todo. Mapping and then reading deeper properties works, but may re-run more often than the minimal set.

Details for each are on the useSignalSelector and SignalProvider API pages.

Docs Restructure

The single "Hooks" API page has been split into separate pages for useSelector, useDispatch, and useStore, with the hooks overview page now serving as an index. New pages cover SignalProvider, useSignalSelector, and unwrap.

What's Changed

  • Add prototype signals+path-tracking implementation by @markerikson in #2318

Full Changelog: https://github.com/reduxjs/react-redux/compare/v9.3.0...v9.4.0-alpha.0

2026-05-15 23:22:00
react-redux

v9.3.0

This feature release officially marks the connect API as deprecated.

That's it. That's the release. :)

Changelog

connect deprecation

Way back in 2022, I officially marked the original Redux core createStore method as deprecated in Redux 4.2.0. As I clearly stated in that release, the goal of marking createStore as deprecated was to encourage users to migrate to modern Redux Toolkit, especially for those users who don't read our docs (such as beginners following outdated tutorials or in bootcamps, etc). The change was visual-only - adding @deprecated just marks the import with a strikethrough in an IDE, and the docblock references the "use modern Redux Toolkit" docs page. No runtime errors, no behavior changes, just an indication that the function is considered obsolete and you shouldn't use it directly any more. I also exported a legacy_createStore alias - same function, no deprecation attribute.

It's 4 years later, and I'm finally doing the same thing for connect :)

Again, nothing about connect's behavior is changing, and we do not intend to remove the connect API. But it's 2026, and hooks are the correct way to use React and React-Redux today.

We do strongly encourage users to migrate from connect to the useSelector / useDispatch hooks in general. This should result in codebases that are easier to understand and ought to improve performance slightly due to the way updates are handled.

As with before, React-Redux now exports a legacy_connect alias that does not have the deprecation attribute applied.

Trusted Publishing Fixed

We had set up trusted publishing for React-Redux a couple years ago and did some releases with that enabled, but at some point I did a follow-up release that still used the previous manual workflow, and that lost the trusted publishing provenance flag. We had recent issues requesting a new release with trusted publishing enabled again, so we've fixed that with this release.

What's Changed

Full Changelog: https://github.com/reduxjs/react-redux/compare/v9.2.0...v9.3.0

2024-12-11 07:07:12
react-redux

v9.2.0

This feature release updates the React peer dependency to work with React 19, and improves treeshakeability of our build artifacts.

Changelog

React 19 Compat

React 19 was just released! We've updated our peer dep to accept React 19, and updated our runtime and type tests to check against both React 18 and 19.

Also see Redux Toolkit v2.5.0 for the same peer dep update.

Treeshaking

We've done some nitty-gritty optimization work to ensure bundlers correctly treeshake unused parts of the bundle.

What's Changed

Full Changelog: https://github.com/reduxjs/react-redux/compare/v9.1.2...v9.2.0

2024-05-02 10:14:35
react-redux

v9.1.2

This bugfix release removes the no-longer-necessary peer dependency on react-native, and tweaks a few TS types for compat with the upcoming React 19 release.

Changes

React Native Peer Dependency Removed

We've always had an awkward peer dependency on both ReactDOM and React Native, because of the need to import the unstable_batchedUpdates API directly from each reconciler. That's part of what led to the sequence of 9.x patch releases to deal with RN compat.

As of 9.0.3, we dropped the batching imports completely, since React 18 now batches by default. That means we didn't even have any remaining imports from react-native.

Meanwhile, React 18.3 just came out, but so did React Native 0.74. RN 0.74 still requires React 18.2.

This caused NPM users to have installation failures when trying to use React-Redux:

  • React-Redux has a peer dep on RN
  • RN has a peer dep on React 18.2
  • But the latest React, 18.3 would get installed in the app
  • NPM errors with a peer dep mismatch

We no longer need to list RN as a peer dep, and dropping that also fixes the NPM installation issues as well.

What's Changed

Full Changelog: https://github.com/reduxjs/react-redux/compare/v9.1.1...v9.1.2

2024-04-15 00:32:39
react-redux

v9.1.1

This bugfix release fixes an issue with connect and React Native caused by changes to our bundling setup in v9. Nested connect calls should work correctly now.

What's Changed

Full Changelog: https://github.com/reduxjs/react-redux/compare/v9.1.0...v9.1.1

2024-01-13 01:20:40
react-redux

v9.1.0

This minor release adds a new syntax for pre-typing hooks.

.withTypes

Previously, the approach for "pre-typing" hooks with your app settings was a little varied. The result would look something like the below:

import type { TypedUseSelectorHook } from "react-redux"
import { useDispatch, useSelector, useStore } from "react-redux"
import type { AppDispatch, AppStore, RootState } from "./store"

export const useAppDispatch: () => AppDispatch = useDispatch
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector
export const useAppStore = useStore as () => AppStore

React Redux v9.1.0 adds a new .withTypes method to each of these hooks, analogous to the .withTypes method found on Redux Toolkit's createAsyncThunk.

The setup now becomes:

import { useDispatch, useSelector, useStore } from "react-redux"
import type { AppDispatch, AppStore, RootState } from "./store"

export const useAppDispatch = useDispatch.withTypes<AppDispatch>()
export const useAppSelector = useSelector.withTypes<RootState>()
export const useAppStore = useStore.withTypes<AppStore>()

What's Changed

New Contributors

Full Changelog: https://github.com/reduxjs/react-redux/compare/v9.0.4...v9.1.0

2023-12-11 23:07:38
react-redux

v9.0.4

This bugfix release updates the React Native peer dependency to be >= 0.69, to better reflect the need for React 18 compat and (hopefully) resolve issues with the npm package manager throwing peer dep errors on install.

What's Changed

Full Changelog: https://github.com/reduxjs/react-redux/compare/v9.0.3...v9.0.4

2023-12-10 23:12:14
react-redux

v9.0.3

This bugfix release drops the ReactDOM / React Native specific use of render batching, as React 18 now automatically batches, and updates the React types dependencies

Changelog

Batching Dependency Updates

React-Redux has long depended on React's unstable_batchedUpdates API to help batch renders queued by Redux updates. It also re-exported that method as a util named batch.

However, React 18 now auto-batches all queued renders in the same event loop tick, so unstable_batchedUpdates is effectively a no-op.

Using unstable_batchedUpdates has always been a pain point, because it's exported by the renderer package (ReactDOM or React Native), rather than the core react package. Our prior implementation relied on having separate batch.ts and batch.native.ts files in the codebase, and expecting React Native's bundler to find the right transpiled file at app build time. Now that we're pre-bundling artifacts in React-Redux v9, that approach has become a problem.

Given that React 18 already batches by default, there's no further need to continue using unstable_batchedUpdates internally, so we've removed our use of that and simplified the internals.

We still export a batch method, but it's effectively a no-op that just immediately runs the given callback, and we've marked it as @deprecated.

We've also updated the build artifacts and packaging, as there's no longer a need for an alternate-renderers entry point that omits batching, or a separate artifact that imports from "react-native".

What's Changed

Full Changelog: https://github.com/reduxjs/react-redux/compare/v9.0.2...v9.0.3