redux review
Redux 5.0.1 is the framework-independent core behind a reducer store. Code dispatches plain actions with string types, reducers calculate the next immutable state, subscribers run after dispatch, and middleware can wrap the dispatch path. React bindings, async conventions, selector memoization, query caching, and persistence live elsewhere. Version 5.0.1 accepts Object.create(null) values in isPlainObject and restores nullable state in the Store.getState TypeScript type after a 5.0 regression. Our full browser import was 3.4 KB minified and 1.4 KB gzipped, with bundled declarations.
Redux 5.0.1 added 1.4 KB gzipped in our browser build and installed as one 1 MB package with zero audit findings. Keep core Redux for existing stores and framework-neutral reducers; start new React Redux code with the maintainers' Toolkit package.
We installed it
| Install | ✓ · 0.3s | 1 package on disk · 1 MB |
| Import | ✓ | ESM import works · require() works · CommonJS package with exports map |
| Browser | 1.4 KB | gzipped (3.4 KB minified), bundled with esbuild |
| Types | ✓ | TypeScript types bundled |
| Known vulns | 0 | 0 critical · 0 high · 0 moderate · 0 low (npm audit) |
Answers from our run
Does redux install cleanly?
Yes. In a fresh container with an empty cache, npm install redux finished in 0.3s, leaving 1 package and 1 MB on disk. npm audit reported no known vulnerabilities.
How much does redux add to a browser bundle?
1.4 KB gzipped (3.4 KB minified) when the whole package is bundled for the browser with esbuild. Importing only part of it is usually smaller.
Does redux work with both ESM and CommonJS?
Yes. Both import 'redux' and require('redux') worked in Node 22 in our run. The package is published as CommonJS with an exports map.
Does redux include TypeScript types?
Yes, type declarations ship inside the package, so no @types install is needed.
redux or @reduxjs/toolkit: which should you use?
@reduxjs/toolkit: Choose it for new Redux applications that need slices, configured middleware, DevTools, development checks, and RTK Query. Redux 5.0.1 added 1.4 KB gzipped in our browser build and installed as one 1 MB package with zero audit findings.
When should you not use redux?
You are starting a React application; the repository tells new projects to install @reduxjs/toolkit and react-redux rather than wire core Redux by hand
Discussed on
- hnGuerrilla Public Service Redux (2017)421 points
- hnBuild Yourself a Redux395 points
- hnBuild your front end in React, then let ChatGPT be your Redux reducer395 points
- hnThe SaaS CTO Security Checklist Redux395 points
- hnThings to learn in React before using Redux370 points
Use it if
- An existing Redux or Redux Toolkit application needs the core store contracts it already depends on
- A browser, Node process, worker, or test tool needs the same reducer and action model without React
- State transitions must be inspectable as serializable events for logging, replay, or deterministic tests
- You are implementing or teaching middleware, enhancers, reducer composition, and subscriptions at the core level
- You are starting a React application; the repository tells new projects to install @reduxjs/toolkit and react-redux rather than wire core Redux by hand
- Remote-data fetching and cache invalidation are the main problem; Redux core has no request deduplication, retry policy, stale time, or cache lifecycle
- Only a few nearby components share simple state; React context, Zustand, or Jotai can avoid action objects, reducer plumbing, and a global provider
- You expect createStore to be the preferred setup function; its declaration is deprecated, and configureStore is the maintained recommendation
- You need thunk dispatch, mutation checks, DevTools setup, memoized selectors, or persistence in this one package; core Redux supplies none of those features
Setup reality
We installed Redux 5.0.1 in a fresh Node 22 sandbox in 0.3 seconds. It left one package and 1 MB on disk. Redux declares zero direct dependencies and zero peer dependencies, and its own unpacked files measured 372 KB. npm audit found zero known vulnerabilities. The package metadata is CommonJS with an exports map; require() and ESM import both worked. TypeScript declarations are included. Our complete esbuild import produced 3.4 KB minified and 1.4 KB gzipped.
There are no credentials or required config files. For new React work, the project's documented install is @reduxjs/toolkit plus react-redux. configureStore includes core Redux, thunk, DevTools wiring, and development checks. Direct createStore calls still execute, but TypeScript marks that name deprecated. legacy_createStore exposes the same runtime function without the editor warning for code that deliberately owns all middleware and enhancer setup.
Redux 5 requires every action.type to be a string. Custom middleware sees both action and next as unknown in TypeScript, so narrow with isAction() or a typed action creator before reading fields. UnknownAction replaces AnyAction as the safer default, and the former PreloadedState type is gone. The package also stopped shipping its old UMD files. These are migration details even though the 5.0.1 runtime patch itself only changes plain-object detection and a getState type.
A reducer must return its old state for unknown actions and cannot mutate that existing value. subscribe() receives no state argument and runs after each dispatch, so listeners call getState() and compare the slice they care about. Middleware order changes which dispatched values a layer observes. The 1.4 KB gzipped core contains no async request policy, query cache, persistence, React provider, or selector cache. Add those deliberately, or use Toolkit when the list starts growing.
Patterns
Build a bare Redux store create-reducer-store
import { legacy_createStore } from 'redux'
function counter(state = { value: 0 }, action) {
if (action.type === 'counter/incremented') {
return { ...state, value: state.value + 1 }
}
return state
}
const store = legacy_createStore(counter)legacy_createStore is the same runtime function as createStore without its TypeScript deprecation marker; new app setup belongs in configureStore.
Observe completed dispatches dispatch-and-subscribe
const unsubscribe = store.subscribe(() => {
console.log(store.getState())
})
store.dispatch({ type: 'counter/incremented' })
unsubscribe()Subscribers receive no arguments and run after every dispatch; call getState() and compare the relevant branch.
Map reducers to state keys combine-state-branches
import { combineReducers } from 'redux'
const rootReducer = combineReducers({
session: sessionReducer,
todos: todosReducer,
})Each reducer receives only its own branch, although the same action can be handled independently by both branches.
Narrow an unknown middleware value write-version-five-middleware
import { isAction } from 'redux'
const logger = ({ getState }) => next => value => {
if (isAction(value)) console.log(value.type)
const result = next(value)
console.log(getState())
return result
}Redux 5 types middleware inputs as unknown; isAction verifies an action object with a string type before field access.
Apply middleware in order install-middleware
import { applyMiddleware, legacy_createStore } from 'redux'
const store = legacy_createStore(
rootReducer,
applyMiddleware(thunk, logger),
)Ordering is visible: middleware before thunk can see dispatched functions, while later layers see only values thunk forwards.
Combine custom enhancers compose-store-enhancers
import { applyMiddleware, compose, legacy_createStore } from 'redux'
const enhancer = compose(
applyMiddleware(logger),
monitorEnhancer,
)
const store = legacy_createStore(rootReducer, enhancer)Enhancers wrap store creation; configureStore handles standard middleware and DevTools composition for new Redux applications.
Hydrate the reducer tree load-preexisting-state
const preloadedState = {
session: { user: null },
todos: serverTodos,
}
const store = legacy_createStore(rootReducer, preloadedState)The object must match the reducer keys, and server rendering needs a separate store instance per request.
Inject a code-split reducer replace-root-reducer
const dynamic = {}
export function injectReducer(name, reducer) {
dynamic[name] = reducer
store.replaceReducer(combineReducers({
...staticReducers,
...dynamic,
}))
}replaceReducer preserves existing state; decide whether state for a removed dynamic key should be retained or deleted.
Create dispatching helper functions bind-action-creators
import { bindActionCreators } from 'redux'
const actions = bindActionCreators({
increment: () => ({ type: 'counter/incremented' }),
add: amount => ({ type: 'counter/added', payload: amount }),
}, store.dispatch)
actions.add(5)Do not bind on every UI render, because each call creates new function identities.
Check a message before dispatch validate-external-action
import { isAction } from 'redux'
const value = JSON.parse(message.data)
if (!isAction(value)) throw new TypeError('invalid Redux action')
store.dispatch(value)isAction validates the general shape and string type; it does not verify the payload schema for a specific action.
Add a Reselect selector memoize-derived-state
import { createSelector } from 'reselect'
const selectTodos = state => state.todos
export const selectCompleted = createSelector(
[selectTodos],
todos => todos.filter(todo => todo.done),
)Reselect is a separate package. Its cached result is reused until an input selector returns a different reference.
Use the recommended application setup start-with-redux-toolkit
import { configureStore, createSlice } from '@reduxjs/toolkit'
const counter = createSlice({
name: 'counter',
initialState: { value: 0 },
reducers: {
incremented(state) { state.value += 1 },
},
})
const store = configureStore({
reducer: { counter: counter.reducer },
})Toolkit wraps this Redux core and adds action creators, Immer-based reducers, thunk, DevTools, and development checks.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| @reduxjs/toolkit | npm | Choose it for new Redux applications that need slices, configured middleware, DevTools, development checks, and RTK Query |
| zustand | npm | Choose it when a hook-based store with selector subscriptions fits better than reducers and explicit action objects |
| jotai | npm | Choose it when independent atoms and dependency tracking match the way state is consumed |
More web frontend guides
postcss · react · react-dom · tailwindcss · htmlparser2 · tailwind-merge · the whole shelf →
How this guide is made: grounded in the library's documentation, release notes, changelog, and issue history, on a fixed rubric — not a hands-on install of every release. The 50 most-downloaded entries are additionally install-verified in clean containers. Corrections: contact the desk.

