react-redux
react-redux is the glue between a Redux store and React components. It does not hold state itself: Redux owns the state, and this package handles the part React needs, which is subscribing to the store, working out whether the slice a component reads actually changed, and re-rendering only the components affected. You wrap the tree in a Provider that carries the store through context, then read state with useSelector and send actions with useDispatch. Under the hood it uses React's useSyncExternalStore, so it behaves correctly with concurrent rendering and server rendering instead of tearing. The older connect() higher-order component still works and is not going away, but as of 9.3.0 it is marked deprecated in the types, and the hooks are what the maintainers tell everyone to write today.
If your app runs Redux, this is the binding, it is well maintained, and 3.8 KB gzipped is a fair price. If you are choosing a state library from scratch in 2026, decide whether you want Redux at all first, because that is the real cost, not this package.
Use it if
- You already run Redux or Redux Toolkit and need React components to read from that store; this is the official binding and there is no real competitor for the job
- Your app has genuinely global, frequently updated state read by many components at different depths, and you want the Redux DevTools time-travel and action log to debug it
- You want fine-grained subscriptions: each useSelector re-renders only its own component when its slice changes, instead of re-rendering a whole context subtree
- You are on TypeScript and want pre-typed hooks: useSelector.withTypes<RootState>() and useDispatch.withTypes<AppDispatch>() give every call site the right types with one line of setup
- You do not already have Redux. Adding the store, the slices, the Provider, and this binding is far more ceremony than zustand or jotai for an app that needs a few shared values
- Most of your "global state" is really cached server data. TanStack Query or RTK Query handle fetching, caching, and invalidation, and moving that into Redux by hand is work you do not need
- You are building React Server Components. The react-server build of this package throws on every hook, so anything touching the store must be a client component and the Provider has to sit inside a client boundary with a fresh store per request
- You are not ready to memoize selectors. useSelector runs its selector after every dispatched action and compares the result by reference, so a selector returning a new object or a .filter() result re-renders the component on every action until you wrap it in createSelector or pass shallowEqual
- You are stuck on React 17 or Redux 4. Version 9 requires React 18 or later and Redux 5, so upgrading this package pulls a whole stack upgrade with it and older apps are pinned to react-redux 8
Setup reality
npm install react-redux redux (or @reduxjs/toolkit, which most people should use for the store half). The peer dependencies are strict: React 18 or 19, Redux 5, and @types/react 18 or 19 on TypeScript projects, so npm will refuse the install on an older stack rather than warn. Then you wrap the app in <Provider store={store}> exactly once, at a level above everything that reads state. Two setup details bite people. In Next.js App Router the Provider file needs 'use client' at the top and the store must be created per request inside a component rather than as a module singleton, otherwise state leaks between users on the server. And on TypeScript you should immediately export pre-typed hooks with .withTypes, because raw useSelector gives you unknown state.
Patterns
Wrap the app once with Providerprovider-setup
import { Provider } from 'react-redux';
import { createRoot } from 'react-dom/client';
import { store } from './store';
import App from './App';
createRoot(document.getElementById('root')).render(
<Provider store={store}>
<App />
</Provider>
);One Provider, above everything that reads state. Any component calling useSelector outside it throws at render time with a message about a missing store.
Read a value with useSelectorread-state-with-selector
import { useSelector } from 'react-redux';
function CartBadge() {
const count = useSelector(state => state.cart.items.length);
return <span>{count}</span>;
}Select the narrowest primitive you can. The selector runs after every dispatched action and the result is compared with reference equality, so returning a number re-renders only when that number changes.
Send actions with useDispatchdispatch-actions
import { useDispatch } from 'react-redux';
import { itemAdded } from './cartSlice';
function AddButton({ product }) {
const dispatch = useDispatch();
return (
<button onClick={() => dispatch(itemAdded(product))}>Add</button>
);
}The dispatch reference is stable for the lifetime of the store, so it is safe in a useEffect or useCallback dependency array without causing loops.
Pre-type the hooks once for TypeScripttyped-hooks
// hooks.ts
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>();.withTypes arrived in 9.1.0 and replaces the old TypedUseSelectorHook cast. Import these everywhere instead of the raw hooks, or thunks and async dispatch will not type-check.
Stop a selector that returns a new object every timeavoid-rerender-new-object
import { useSelector, shallowEqual } from 'react-redux';
// re-renders on every action: new object identity each run
const bad = useSelector(state => ({ id: state.user.id, name: state.user.name }));
// compares one level deep instead
const user = useSelector(
state => ({ id: state.user.id, name: state.user.name }),
shallowEqual
);This is the single most common react-redux bug. shallowEqual fixes objects and arrays of primitives; it does not help if the values inside are themselves newly created objects.
Derive filtered data with createSelectormemoized-derived-data
import { createSelector } from '@reduxjs/toolkit';
import { useAppSelector } from './hooks';
const selectDoneTodos = createSelector(
[state => state.todos.items],
items => items.filter(t => t.done)
);
function DoneList() {
const done = useAppSelector(selectDoneTodos);
return <ul>{done.map(t => <li key={t.id}>{t.text}</li>)}</ul>;
}Define the selector at module scope, not inside the component, or the memo cache is thrown away on every render. reselect ships inside Redux Toolkit, so there is nothing extra to install.
Pass a prop into a selectorselector-with-argument
import { useCallback } from 'react';
import { useAppSelector } from './hooks';
function TodoRow({ id }) {
const todo = useAppSelector(
useCallback(state => state.todos.byId[id], [id])
);
return <li>{todo.text}</li>;
}An inline arrow closing over a prop is a new function each render, which is fine for cheap lookups but defeats any memoization. useCallback keeps the identity stable while id is unchanged.
Provider in the Next.js App Routernextjs-app-router-provider
'use client';
import { useRef } from 'react';
import { Provider } from 'react-redux';
import { makeStore, type AppStore } from './store';
export function StoreProvider({ children }) {
const storeRef = useRef<AppStore>(undefined);
if (!storeRef.current) storeRef.current = makeStore();
return <Provider store={storeRef.current}>{children}</Provider>;
}A module-level store singleton is shared across requests on the server and leaks one user's state into another's response. Creating it in a ref gives each request its own store. The 'use client' directive is required because hooks throw in the react-server build.
Read state without subscribingread-store-imperatively
import { useAppStore, useAppDispatch } from './hooks';
function SaveButton() {
const store = useAppStore();
const dispatch = useAppDispatch();
const onClick = () => {
const draft = store.getState().editor.draft; // read at click time
dispatch(saveDraft(draft));
};
return <button onClick={onClick}>Save</button>;
}useStore does not subscribe, so the component never re-renders from it. Use it in event handlers when you need the latest value; never call getState() during render, because that read is invisible to React.
A second store in its own contextcustom-context
import { createContext } from 'react';
import {
Provider,
createSelectorHook,
createDispatchHook,
createStoreHook,
} from 'react-redux';
export const WidgetContext = createContext(null);
export const useWidgetSelector = createSelectorHook(WidgetContext);
export const useWidgetDispatch = createDispatchHook(WidgetContext);
export const useWidgetStore = createStoreHook(WidgetContext);
// <Provider context={WidgetContext} store={widgetStore}>...</Provider>Useful for an embeddable widget that must not collide with the host app's store. Regular useSelector still reads the default context, so the two trees stay independent.
Render a component under test with a storetest-with-provider
import { render } from '@testing-library/react';
import { Provider } from 'react-redux';
import { makeStore } from './store';
function renderWithStore(ui, { preloadedState } = {}) {
const store = makeStore(preloadedState);
return {
store,
...render(<Provider store={store}>{ui}</Provider>),
};
}Build a real store with preloaded state rather than mocking useSelector. A fresh store per test keeps cases isolated, and you can assert on store.getState() after interactions.
Replace a connect() component with hooksmigrate-connect-to-hooks
// before
export default connect(
state => ({ user: state.auth.user }),
{ logout }
)(Header);
// after
function Header() {
const user = useAppSelector(state => state.auth.user);
const dispatch = useAppDispatch();
return <button onClick={() => dispatch(logout())}>{user.name}</button>;
}9.3.0 marks connect as deprecated in the types, which shows as a strikethrough in editors. Behaviour is unchanged and removal is not planned, so migrate at your own pace; legacy_connect is exported if the warning is noisy.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| zustand | npm | You want shared state in a hook with no provider, no actions, and no reducer boilerplate. |
| jotai | npm | Your state is naturally lots of small independent pieces rather than one tree. |
| @tanstack/react-query | npm | The state you keep syncing is server data that needs caching, refetching, and invalidation. |