1. Architecture Overview
Redux is a predictable state container for JavaScript applications. At its core, it implements a minimal, framework-agnostic state machine: a single store holds an immutable state tree, plain-object actions describe state transitions, and pure reducer functions compute the next state. The entire public API surface consists of seven functions and two utilities, totaling under 700 lines of TypeScript source.
Redux is not an application framework — it is a coordination primitive. Its value comes from the strict constraints it enforces (immutability, unidirectional data flow, pure reducers) and the ecosystem those constraints enable (time-travel debugging, middleware pipelines, hot reloading, server-side rendering).
The Redux team now recommends Redux Toolkit (@reduxjs/toolkit) as the standard way to write Redux logic. Redux Toolkit wraps this core package with configureStore, createSlice, createAsyncThunk, and other utilities. This guide documents the core redux package internals that Redux Toolkit builds upon.
Architecture Diagram
┌──────────────────────┐
│ Application │
│ │
│ dispatch(action) ────┤
│ │
│ ◄── subscribe() ─────┤
│ ◄── getState() ──────┤
└───────────┬───────────┘
│
┌───────────▼───────────┐
│ Store │
│ │
│ ┌─────────────────┐ │
│ │ Middleware │ │
│ │ Chain │ │
│ │ (applyMiddle- │ │
│ │ ware) │ │
│ └────────┬────────┘ │
│ │ │
│ ┌────────▼────────┐ │
│ │ Root Reducer │ │
│ │ (combine- │ │
│ │ Reducers) │ │
│ └────────┬────────┘ │
│ │ │
│ ┌────────▼────────┐ │
│ │ State Tree │ │
│ │ (immutable) │ │
│ └─────────────────┘ │
│ │
│ Listeners (Map) │
└───────────────────────┘
Component Inventory
| Component | Source Path | Role |
|---|---|---|
createStore |
src/createStore.ts |
Store factory — holds state, manages dispatch/subscribe/getState lifecycle |
combineReducers |
src/combineReducers.ts |
Merges multiple slice reducers into a single root reducer |
applyMiddleware |
src/applyMiddleware.ts |
Store enhancer that composes a middleware chain around dispatch |
bindActionCreators |
src/bindActionCreators.ts |
Wraps action creators with dispatch for direct invocation |
compose |
src/compose.ts |
Right-to-left function composition utility |
isAction |
src/utils/isAction.ts |
Type guard — checks plain object with string type |
isPlainObject |
src/utils/isPlainObject.ts |
Prototype-chain check for plain objects |
actionTypes |
src/utils/actionTypes.ts |
Internal action types (@@redux/INIT, REPLACE, PROBE_UNKNOWN_ACTION) |
warning |
src/utils/warning.ts |
Development-only console.error with breakpoint trick |
kindOf |
src/utils/kindOf.ts |
Human-readable type name for error messages (dev only) |
formatProdErrorMessage |
src/utils/formatProdErrorMessage.ts |
Minified error codes with URL links for production |
symbol-observable |
src/utils/symbol-observable.ts |
TC39 Symbol.observable polyfill |
Technology Stack
| Layer | Technology |
|---|---|
| Language | TypeScript 5.x (strict mode, exactOptionalPropertyTypes) |
| Build | tsup (esbuild-based, 4 output formats) |
| Test | Vitest (runtime + type-level tests via --typecheck) |
| Lint | ESLint with @typescript-eslint |
| Format | Prettier (no semicolons, single quotes, trailing comma: none) |
| Package Manager | Yarn 4.4.1 (PnP disabled, nodeLinker: node-modules) |
| CI/CD | Netlify (website), npm publish |
| Documentation Site | Docusaurus 3.6.3 (in website/) |
Data Flow
- Application calls
store.dispatch(action)with a plain object - If middleware is applied, the action passes through the middleware chain (each middleware can inspect, modify, delay, or replace the action)
- The innermost middleware calls the original
store.dispatch, which:- Validates the action is a plain object with a string
type - Calls
currentReducer(currentState, action)synchronously - Replaces
currentStatewith the return value - Iterates all registered listeners (Map-based, snapshot before dispatch)
- Validates the action is a plain object with a string
- The dispatched action object is returned to the caller
Note
The listener collection uses a
Map<number, ListenerCallback>with numeric IDs, not an array. Subscriptions and unsubscriptions during a dispatch take effect on the next dispatch, not the current one — the listener list is snapshotted via shallow copy before iteration.
2. System Requirements
Runtime Requirements
| Requirement | Detail |
|---|---|
| JavaScript Engine | Any ES2015+ environment (all modern browsers, Node.js 14+) |
| Required Globals | Map, Object.getPrototypeOf, Symbol (for observable interop) |
| Dependencies | None — zero runtime dependencies |
Development Requirements
| Requirement | Version |
|---|---|
| Node.js | 14+ (20 for website build) |
| Yarn | 4.4.1 (ships via .yarnrc.yml → .yarn/releases/yarn-4.4.1.cjs) |
| TypeScript | 5.8.2+ |
Build Output Sizes
The library is extremely small. The sideEffects: false flag enables complete tree-shaking for ESM consumers.
3. Configuration Reference
Redux has no configuration files. All configuration is code-level, passed to createStore at instantiation.
createStore Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
reducer |
Reducer<S, A> |
Yes | Root reducer function: (state, action) => newState |
preloadedState |
S |
No | Initial state for hydration (SSR) or restored sessions |
enhancer |
StoreEnhancer |
No | Store enhancer (e.g., applyMiddleware(...), composed enhancers) |
Important
If
preloadedStateis omitted and the second argument is a function, Redux treats it as theenhancer. This enables the two-argument form:createStore(reducer, enhancer).
combineReducers Behavior
| Aspect | Detail |
|---|---|
| Input | Object mapping keys to reducer functions |
| Validation | Calls each reducer with undefined state + @@redux/INIT action; throws if any returns undefined |
| Probing | Calls each reducer with undefined state + random action type; throws if any returns undefined |
| Unknown keys | In development, logs a warning if state contains keys with no matching reducer |
| Reference equality | Returns the same state object if no slice reducer returns a new value |
applyMiddleware Behavior
| Aspect | Detail |
|---|---|
| Input | Variadic list of middleware functions |
| Middleware API | Each middleware receives { getState, dispatch } — note: dispatch is the enhanced dispatch, not the original |
| Chain construction | compose(...middlewares.map(m => m(middlewareAPI)))(store.dispatch) |
| Return | A StoreEnhancer that wraps createStore |
Environment Variables
| Variable | Scope | Effect |
|---|---|---|
NODE_ENV |
Build-time | production: strips dev warnings, uses formatProdErrorMessage with numeric codes. development: enables kindOf() detailed type names, combineReducers shape warnings, console.error warnings. |
Build Configuration
The build is configured via tsup.config.ts:
| Output | Format | Minified | NODE_ENV |
File |
|---|---|---|---|---|
| Standard ESM | esm |
No | Embedded checks | dist/redux.mjs |
| Legacy ESM | esm (ES2017 target) |
No | Embedded checks | dist/redux.legacy-esm.js |
| Browser ESM | esm |
Yes | production (hardcoded) |
dist/redux.browser.mjs |
| CJS | cjs |
No | Embedded checks | dist/cjs/redux.cjs |
| Type Declarations | — | — | — | dist/redux.d.mts |
All builds use sourcemaps. The mangleErrorsTransform Babel plugin replaces verbose error strings with numeric codes (mapped in errors.json) for production builds.
TypeScript Configuration
Three tsconfig files serve different purposes:
| Config | Extends | Purpose |
|---|---|---|
tsconfig.base.json |
— | Shared settings: strict, exactOptionalPropertyTypes, isolatedModules, path aliases (@internal/* → ./src/*, redux → ./src/index.ts) |
tsconfig.build.json |
base |
Library build: emitDeclarationOnly, outputs to dist/ |
tsconfig.test.json |
base |
Type-level tests: noEmit, includes test/typescript/ |
Note
The
reduxpath alias intsconfig.base.jsonis annotated// @remap-prod-remove-line— it is stripped during production builds so tests can import from the source directly while published types point to the built output.
4. Deployment & Scaling
Publishing
The package is published to npm as redux. The publish lifecycle:
prepublishOnly: clean → format check → lint → testprepack: clean → tsup build (4 output formats + types)- npm publish uploads
dist/andsrc/(per"files"inpackage.json)
Release Management
Releases use release-it (configured in .release-it.json):
{
"hooks": {
"after:bump": "yarn && git add -u"
},
"git": {
"tagName": "v${version}"
}
}
Git tags follow the v{version} format (e.g., v5.0.1).
Website Deployment
The documentation website deploys to Netlify:
- Build base:
website/ - Build command:
cd ../docs && yarn && cd ../website && yarn build && cp _redirects ./build - Publish directory:
website/build - Skip condition:
git diff --quiet HEAD^ HEAD -- ../docs/ .(only rebuilds if docs or website changed) - Node version: 20
- Caching:
node_modules/.cache,website/node_modules/.cache,.yarn/.cache
Bundle Size Considerations
Redux is designed to be as small as possible:
- Zero runtime dependencies
sideEffects: falseenables tree-shaking- Production builds use minified error codes (see
errors.json) - The
composefunction inlines — if onlycreateStoreis imported,compose,bindActionCreators, etc. are tree-shaken away
Tip
The browser-ready ESM build (
dist/redux.browser.mjs) is pre-minified withNODE_ENV=productionhardcoded. Use it for direct<script type="module">inclusion without a build step.
5. Operations
Development Workflow
# Install dependencies
yarn
# Run tests (runtime + type-level)
yarn test
# Run tests in watch mode
yarn test:watch
# Run tests with coverage
yarn test:cov
# Type-check the type-level tests only
yarn type-tests
# Lint
yarn lint
# Format
yarn format
# Build all outputs
yarn build
# Lint examples
yarn examples:lint
Error Code System
Redux uses a Babel plugin (scripts/mangleErrors.mts) that replaces verbose error strings with numeric codes in production builds. The mapping lives in errors.json:
| Code | Error |
|---|---|
| 0 | Passing several store enhancers to createStore() |
| 1 | Expected enhancer to be a function |
| 2 | Expected root reducer to be a function |
| 3 | Cannot call getState() while reducer is executing |
| 4 | Expected listener to be a function |
| 5 | Cannot call subscribe() while reducer is executing |
| 6 | Cannot unsubscribe while reducer is executing |
| 7 | Actions must be plain objects |
| 8 | Actions may not have undefined type |
| 9 | Reducers may not dispatch actions |
| 10 | Expected nextReducer to be a function |
| 11 | Expected observer to be an object |
| 12 | Slice reducer returned undefined during initialization |
| 13 | Slice reducer returned undefined when probed |
| 14 | Slice reducer returned undefined for action |
| 15 | Dispatching while constructing middleware |
| 16 | bindActionCreators expected object or function |
| 17 | Action type must be a string |
In production, users see: Minified Redux error #7; visit https://redux.js.org/Errors?code=7 for the full message
Note
The error code system mirrors React's approach. The
errors.jsonfile is the source of truth for the mapping between codes and full messages.
Monitoring and Debugging
Redux is designed for observability through Redux DevTools:
- Time-travel debugging: replay actions, jump to any previous state
- Action inspection: view every dispatched action and its payload
- State diff: see what changed between dispatches
- Import/export: save and load state snapshots
Integration via the DevTools Extension Compose:
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose
const store = createStore(reducer, composeEnhancers(applyMiddleware(thunk)))
Tip
Redux Toolkit's
configureStoreenables DevTools automatically in development. With the corereduxpackage, you must compose the DevTools enhancer manually.
6. Performance
State Reference Equality
combineReducers returns the same state object reference if no slice reducer returns a new value. This enables efficient change detection in UI libraries:
// Inside combineReducers:
hasChanged = hasChanged || nextStateForKey !== previousStateForKey
// ...
hasChanged = hasChanged || finalReducerKeys.length !== Object.keys(state).length
return hasChanged ? nextState : state
Listener Implementation
Listeners use a Map<number, ListenerCallback> with numeric IDs (auto-incrementing counter). This is more efficient than array-based implementations for frequent subscribe/unsubscribe patterns. The listener map is copy-on-write: ensureCanMutateNextListeners() creates a shallow copy only when needed.
Middleware Chain
The middleware chain is composed once at store creation via compose. Each dispatch passes through the chain with zero additional allocation — middleware functions are pre-composed into a single function reference.
7. Security
Action Validation
Redux enforces strict action validation in the dispatch function:
- Plain object check:
isPlainObject(action)— rejects class instances, arrays, Promises, functions - Type existence check:
typeof action.type === 'undefined'→ error - Type string check:
typeof action.type !== 'string'→ error (new in Redux 5.x) - Re-entrancy guard:
isDispatchingflag prevents dispatch during reducer execution
Serialization Safety
For SSR, Redux state must be serialized to HTML. The recommended pattern escapes < to prevent XSS:
JSON.stringify(preloadedState).replace(/</g, '\\x3c')
Caution
Failing to escape serialized state enables XSS attacks via crafted state values containing
</script>tags. Always sanitize when embedding state in HTML.
Supply Chain
- Zero runtime dependencies (the library depends on nothing at runtime)
- All devDependencies are well-known packages from the JS ecosystem
- The
sideEffects: falsedeclaration ensures no code runs on import alone
Observable Protocol
The Symbol.observable interop uses a defensive check:
const $$observable = (() =>
(typeof Symbol === 'function' && Symbol.observable) || '@@observable')()
This falls back to a string key if Symbol.observable is not available, maintaining compatibility without polyfill requirements.
8. Developer Internals
Repository Structure
redux/
├── src/ # Core library source (TypeScript)
│ ├── index.ts # Public API barrel export
│ ├── createStore.ts # Store factory (createStore, legacy_createStore)
│ ├── combineReducers.ts # Reducer composition
│ ├── applyMiddleware.ts # Middleware store enhancer
│ ├── bindActionCreators.ts # Action creator dispatch binding
│ ├── compose.ts # Right-to-left function composition
│ ├── types/ # TypeScript type definitions
│ │ ├── actions.ts # Action, UnknownAction, AnyAction, ActionCreator
│ │ ├── middleware.ts # Middleware, MiddlewareAPI
│ │ ├── reducers.ts # Reducer, ReducersMapObject, derived types
│ │ └── store.ts # Store, Dispatch, StoreEnhancer, Observable
│ └── utils/ # Internal utilities
│ ├── actionTypes.ts # @@redux/INIT, REPLACE, PROBE_UNKNOWN_ACTION
│ ├── formatProdErrorMessage.ts # Minified error codes
│ ├── isAction.ts # Action type guard
│ ├── isPlainObject.ts # Plain object check
│ ├── kindOf.ts # Dev-mode type name utility
│ ├── symbol-observable.ts # Symbol.observable polyfill
│ └── warning.ts # Dev-mode console.error wrapper
├── test/ # Test suite
│ ├── *.spec.ts # Runtime tests (one per core module)
│ ├── helpers/ # Test utilities (mock reducers, actions, middleware)
│ ├── utils/ # Tests for utility functions
│ └── typescript/ # Type-level tests (*.test-d.ts)
├── examples/ # 10 example applications
│ ├── async/ # Async data fetching with redux-thunk
│ ├── counter/ # React + RTK counter (JS)
│ ├── counter-ts/ # React + RTK counter (TypeScript)
│ ├── counter-vanilla/ # Vanilla JS counter (no React)
│ ├── real-world/ # GitHub API browser with pagination
│ ├── shopping-cart/ # Shopping cart with optimistic updates
│ ├── todomvc/ # TodoMVC with reselect
│ ├── todos/ # Basic todo list
│ ├── todos-with-undo/ # Todos with redux-undo
│ ├── tree-view/ # Dynamic tree with normalized state
│ └── universal/ # Server-side rendering (Express + React)
├── website/ # Docusaurus documentation site
├── docs/ # Markdown documentation source
├── scripts/ # Build scripts
│ └── mangleErrors.mts # Babel plugin for error code extraction
├── errors.json # Error code → message mapping
├── tsup.config.ts # Build configuration (4 output formats)
├── tsconfig.base.json # Shared TypeScript settings
├── tsconfig.build.json # Build-specific TypeScript config
├── tsconfig.test.json # Type-test TypeScript config
├── vitest.config.mts # Test runner configuration
├── package.json # Package metadata (v5.0.1)
└── netlify.toml # Website deployment config
Module Reference
createStore.ts — Store Factory
The central module. Creates the store object with getState, dispatch, subscribe, replaceReducer, and [Symbol.observable] methods.
Internal state:
| Variable | Type | Purpose |
|---|---|---|
currentReducer |
Reducer |
The active root reducer |
currentState |
S | PreloadedState | undefined |
The current state tree |
currentListeners |
Map<number, ListenerCallback> | null |
Active listener map (set to null after unsubscribe to allow GC) |
nextListeners |
Map<number, ListenerCallback> |
Next listener map (copy-on-write) |
listenerIdCounter |
number |
Auto-incrementing listener ID |
isDispatching |
boolean |
Re-entrancy guard |
Key behaviors:
- On creation, dispatches
{ type: '@@redux/INIT{random}' }to populate initial state replaceReducerdispatches{ type: '@@redux/REPLACE{random}' }to re-initialize- Random suffixes on internal action types prevent reducers from matching them
dispatchreturns the dispatched action object (middleware may return something else)subscribereturns anunsubscribefunction; calling it twice is a no-op- The
@@observablemethod returns a minimal observable that callsobserver.next(getState())on every state change
createStore vs legacy_createStore: Both are exported. createStore carries a @deprecated JSDoc annotation; legacy_createStore is identical in behavior but without the deprecation marker. legacy_createStore delegates to createStore internally.
combineReducers.ts — Reducer Composition
Merges an object of slice reducers into a single root reducer.
Validation (at construction time):
- Filters out non-function values from the input object
- Warns (dev only) if any key maps to
undefined - Calls
assertReducerShape— invokes each reducer with:undefinedstate +@@redux/INITaction → must not returnundefinedundefinedstate +@@redux/PROBE_UNKNOWN_ACTION{random}→ must not returnundefined
Runtime (on each dispatch):
- Iterates
finalReducerKeys, calls each slice reducer with its slice of state + action - Checks if any slice returned a new value (reference inequality)
- Also checks if the number of state keys changed
- Returns new state object only if something changed; otherwise returns the same reference
Dev-mode warnings:
- Unexpected keys in state that don't match any reducer
- Unexpected state shape (not a plain object)
applyMiddleware.ts — Middleware Enhancer
Returns a StoreEnhancer that intercepts createStore to wrap dispatch.
Composition:
const middlewareAPI: MiddlewareAPI = {
getState: store.getState,
dispatch: (action, ...args) => dispatch(action, ...args)
}
const chain = middlewares.map(middleware => middleware(middlewareAPI))
dispatch = compose<typeof dispatch>(...chain)(store.dispatch)
The dispatch reference in middlewareAPI is a closure — it always points to the latest composed dispatch, not the original. This allows middleware to dispatch through the full chain.
Warning
Calling
dispatchduring middleware construction (before the chain is composed) throws: "Dispatching while constructing your middleware is not allowed." The initialdispatchis a throwing stub that gets replaced once composition completes.
bindActionCreators.ts — Dispatch Binding
Accepts a single action creator or an object of action creators and a dispatch function. Returns wrapped versions that call dispatch(actionCreator(...args)) automatically.
Edge cases:
- Non-function values in the input object are silently skipped
- Passing neither a function nor an object throws with a descriptive error including
kindOf()type name - The bound function preserves
thiscontext via.apply(this, args)
compose.ts — Function Composition
Composes functions from right to left. Special cases:
| Input | Output |
|---|---|
| 0 functions | Identity function: (arg) => arg |
| 1 function | Returns that function directly |
| N functions | funcs.reduce((a, b) => (...args) => a(b(...args))) |
Heavily overloaded in TypeScript (up to 4 functions with full type inference, then rest-args fallback).
src/utils/ — Internal Utilities
| Module | Description |
|---|---|
actionTypes.ts |
Three internal action types with random suffixes: INIT (fired on store creation), REPLACE (fired on replaceReducer), PROBE_UNKNOWN_ACTION (used by assertReducerShape to verify reducers handle unknown actions) |
isPlainObject.ts |
Walks the prototype chain to check if an object's prototype is Object.prototype or null. Rejects class instances, arrays, Dates, etc. |
isAction.ts |
Combines isPlainObject with a check for 'type' in action and typeof type === 'string' |
kindOf.ts |
In development, returns a human-readable type name (e.g., 'array', 'date', 'promise', 'weakmap'). In production, returns raw typeof. Used only for error messages. |
warning.ts |
Calls console.error(message), then throw new Error(message) inside a try/catch. The throw enables "break on all exceptions" in browser DevTools without actually propagating. |
formatProdErrorMessage.ts |
Returns a message directing users to redux.js.org/Errors?code=N. Used by the mangleErrors Babel plugin to replace verbose error strings in production builds. |
symbol-observable.ts |
Returns Symbol.observable if available, otherwise the string '@@observable'. Used as a method key on the store for observable interop. |
Type System Architecture
Redux 5.0.1 exports a comprehensive type system from src/types/:
types/actions.ts
| Type | Description |
|---|---|
Action<T = string> |
{ type: T } — base action interface |
UnknownAction |
{ type: string; [extraProps: string]: unknown } — replaces AnyAction |
AnyAction |
Legacy alias for UnknownAction (deprecated) |
ActionCreator<A> |
(...args: any[]) => A |
ActionCreatorsMapObject<A> |
{ [key: string]: ActionCreator<A> } |
types/store.ts
| Type | Description |
|---|---|
Store<S, A, StateExt> |
The store interface with getState, dispatch, subscribe, replaceReducer, [Symbol.observable] |
Dispatch<A> |
(action: A) => A |
Unsubscribe |
() => void |
StoreEnhancer<Ext, StateExt> |
(createStore) => (reducer, preloadedState) => Store |
StoreCreator |
The createStore function type |
StoreEnhancerStoreCreator |
Return type of StoreEnhancer |
Observable<T> / Observer<T> |
TC39 Observable interop types |
ListenerCallback |
() => void |
UnknownIfNonSpecific<T> |
Utility type: resolves to unknown if T is {} |
types/reducers.ts
| Type | Description |
|---|---|
Reducer<S, A, PreloadedState> |
(state: S | PreloadedState | undefined, action: A) => S |
ReducersMapObject<S, A> |
{ [K in keyof S]: Reducer<S[K], A> } |
StateFromReducersMapObject<M> |
Derives the combined state type from a reducers map |
ActionFromReducersMapObject<M> |
Union of all action types accepted by reducers in the map |
PreloadedStateShapeFromReducersMapObject<M> |
Partial<> of the derived state (for preloaded state) |
types/middleware.ts
| Type | Description |
|---|---|
MiddlewareAPI<D, S> |
{ dispatch: D; getState: () => S } |
Middleware<Ext, S, D> |
(api: MiddlewareAPI) => (next: D) => (action: any) => any |
Code Patterns and Conventions
Formatting (from .prettierrc.json):
| Setting | Value |
|---|---|
| Semicolons | false |
| Quotes | Single |
| Tab width | 2 |
| Trailing commas | None |
| Arrow parens | Avoid |
Error handling pattern: All public functions validate inputs and throw Error with descriptive messages (dev) or coded messages (prod). The kindOf() utility provides type names in error strings.
Copy-on-write listeners: The listener Map is shallow-copied before mutation via ensureCanMutateNextListeners(). This prevents bugs when subscribers add/remove listeners during notification.
Internal action types use random suffixes:
INIT: `@@redux/INIT${randomString()}`
REPLACE: `@@redux/REPLACE${randomString()}`
PROBE_UNKNOWN_ACTION: () => `@@redux/PROBE_UNKNOWN_ACTION${randomString()}`
This ensures no reducer accidentally handles these internal actions. PROBE_UNKNOWN_ACTION is a function (not a string) because it must generate a unique value on each call during assertReducerShape.
Testing Strategy
Runtime tests (test/*.spec.ts):
| Test File | Coverage |
|---|---|
createStore.spec.ts |
Store creation, dispatch, subscribe, getState, replaceReducer, observable, error paths |
combineReducers.spec.ts |
Reducer combination, shape validation, warning behavior |
applyMiddleware.spec.ts |
Middleware chain composition, dispatch wrapping |
bindActionCreators.spec.ts |
Single and map binding, error cases |
compose.spec.ts |
0-arg, 1-arg, multi-arg composition |
Utility tests (test/utils/*.spec.ts):
| Test File | Coverage |
|---|---|
isAction.spec.ts |
Plain object + string type validation |
isPlainObject.spec.ts |
Prototype chain checks |
warning.spec.ts |
Console.error behavior |
formatProdErrorMessage.spec.ts |
Error code formatting |
Type-level tests (test/typescript/*.test-d.ts):
9 type-test files using Vitest's expectTypeOf assertions:
| Test File | Coverage |
|---|---|
store.test-d.ts |
Store type inference, enhancer composition |
dispatch.test-d.ts |
Dispatch type narrowing |
reducers.test-d.ts |
Reducer type derivation |
middleware.test-d.ts |
Middleware type chain |
compose.test-d.ts |
Compose overload resolution |
enhancers.test-d.ts |
Store enhancer type composition |
actions.test-d.ts |
Action type constraints |
actionCreators.test-d.ts |
Action creator type inference |
injectedDispatch.test-d.ts |
Enhanced dispatch types from middleware |
Test helpers (test/helpers/):
| File | Contents |
|---|---|
actionCreators.ts |
addTodo, addTodoAsync, addTodoIfEmpty, unknownAction, dispatchInMiddle, getStateInMiddle, subscribeInMiddle, unsubscribeInMiddle, throwError, noop |
actionTypes.ts |
ADD_TODO, DISPATCH_IN_MIDDLE, GET_STATE_IN_MIDDLE, SUBSCRIBE_IN_MIDDLE, UNSUBSCRIBE_IN_MIDDLE, THROW_ERROR, UNKNOWN_ACTION |
middleware.ts |
thunk (minimal thunk middleware for tests) |
reducers.ts |
reducerNoop, reducerA–reducerC (with state shapes), combined reducers |
Running tests:
yarn test # Full suite (runtime + typecheck)
yarn test:watch # Watch mode
yarn test:cov # With coverage
yarn type-tests # Type-level tests only (tsc --noEmit)
Example Applications
The examples/ directory contains 10 example apps demonstrating different Redux patterns:
| Example | Stack | Pattern Demonstrated |
|---|---|---|
counter |
React + RTK | Basic configureStore + createSlice (JS) |
counter-ts |
React + RTK | Same as counter, in TypeScript |
counter-vanilla |
Vanilla JS | Redux without any framework; raw DOM manipulation |
todos |
React + Redux | Basic todo list with combineReducers |
todos-with-undo |
React + Redux + redux-undo | Undo/redo history |
todomvc |
React + Redux + reselect | Full TodoMVC spec with memoized selectors |
async |
React + Redux + redux-thunk | Async data fetching from Reddit API |
shopping-cart |
React + Redux + redux-thunk | Shopping cart with mock API and optimistic updates |
real-world |
React + Redux + custom middleware | GitHub API with pagination, caching, normalization (normalizr) |
tree-view |
React + Redux | Dynamic tree with normalized flat state |
universal |
Express + React + Redux | Server-side rendering with state hydration |
Note
The
counterandcounter-tsexamples use Redux Toolkit (@reduxjs/toolkit), not the corereduxpackage directly. All other examples use core Redux APIs (createStore,combineReducers,applyMiddleware).
Potential Improvements
Based on source analysis and community issues:
- Documentation consolidation — GitHub issue #4789 discusses merging Redux, RTK, and React-Redux docs into a unified site
- Outdated tutorial content — Issue #4480 notes tutorials use legacy
ReactDOM.render()instead ofcreateRoot(). Issue #4375 reports broken reactions in the Essentials tutorial - Reselect docs drift — Issue #4866 flags outdated Reselect references in the selectors guide
- Next.js guidance — Issue #4713 requests per-route state documentation for Next.js
- Type improvements — Issue #3455 reports
ActionCreatorgeneric losing argument types; issue #4602 reports conflicts betweenAppThunkandcreateAsyncThunk.withTypes - Normalizr replacement — Issue #4699 notes normalizr is no longer maintained; the docs still reference it
Contributing
Code style:
yarn format # Auto-format with Prettier
yarn format:check # Check formatting without modifying
yarn lint # ESLint
Pre-publish checks: prepublishOnly runs format check, lint, and full test suite.
Git conventions:
- Tags:
v{version}(e.g.,v5.0.1) .git-blame-ignore-revsexists for large formatting commits.editorconfigand.prettierrc.jsonenforce consistent formatting
License: MIT
9. Integration Points
Observable Protocol
Redux stores implement the TC39 Observable proposal via Symbol.observable (or the @@observable string fallback). This enables interop with:
- RxJS:
from(store).pipe(...) - most.js:
from(store).map(...) - Any library implementing the Observable consumer protocol
The observable emits the full state tree on every dispatch via observer.next(getState()).
Store Enhancer Protocol
Store enhancers wrap createStore to add capabilities. The enhancer signature:
type StoreEnhancer = (createStore: StoreCreator) =>
(reducer: Reducer, preloadedState?: any) => Store
Examples: applyMiddleware, Redux DevTools Extension, persistence enhancers. Enhancers compose via compose:
const enhancer = compose(
applyMiddleware(thunk, logger),
devToolsEnhancer,
persistEnhancer
)
Middleware Protocol
Middleware functions follow a curried three-layer pattern:
type Middleware = (api: { dispatch, getState }) =>
(next: Dispatch) =>
(action: Action) => any
- Outer function: receives
{ dispatch, getState }— called once at store creation - Middle function: receives
next— the next middleware's dispatch (or the store's base dispatch for the last middleware) - Inner function: receives
action— called on every dispatch
React Integration
Via react-redux, Redux integrates with React through:
<Provider store={store}>— context-based store injectionuseSelector(selector)— subscribe to state slices with reference equalityuseDispatch()— access the store's dispatch functionconnect(mapState, mapDispatch)— HOC-based integration (legacy pattern)