Jesse A. Green / notebook

Live demo: the DocCompiler guide library as a static, read-only snapshot (October 2026). No backend, no accounts, nothing to submit.

Redux User Guide

Generated May 2026 from reduxjs/redux at 0d8fae6

AI-generated documentation. The library is checked by sampling (three repositories so far); this guide was not individually reviewed. Verify against the project before relying on it.

1. Quick Start

Redux is a predictable state container for JavaScript applications. It centralizes your application's state and logic in a single store, enforces unidirectional data flow through plain-object actions and pure reducer functions, and notifies subscribers when the state tree changes. The result: applications that behave consistently across client, server, and native environments, and are straightforward to test and debug.

Key features:

  • Single source of truth — one store holds the entire application state
  • State is read-only — changes happen only through dispatched actions
  • Pure reducer functions compute the next state predictably
  • Middleware pipeline for async logic, logging, and side effects
  • Time-travel debugging via Redux DevTools
  • Framework-agnostic — works with React, Angular, Vue, or vanilla JS
  • First-class TypeScript support with comprehensive type exports
  • Observable interoperability (TC39 Symbol.observable)
  • Tiny footprint — under 2KB minified with zero runtime dependencies

Important

The Redux team recommends Redux Toolkit (@reduxjs/toolkit) for all new projects. Redux Toolkit wraps the core redux package with simplified APIs, built-in best practices, and pre-configured middleware. The core redux package documented here is the foundation that Redux Toolkit builds upon.

Prerequisites: Node.js 14+ and a package manager (npm, yarn, or pnpm).

Install and create your first store:

npm install redux
import { legacy_createStore as createStore } from 'redux'

// Define a reducer
function counterReducer(state = { value: 0 }, action) {
  switch (action.type) {
    case 'counter/incremented':
      return { value: state.value + 1 }
    case 'counter/decremented':
      return { value: state.value - 1 }
    default:
      return state
  }
}

// Create the store
const store = createStore(counterReducer)

// Subscribe to state changes
store.subscribe(() => console.log(store.getState()))

// Dispatch actions
store.dispatch({ type: 'counter/incremented' })
// { value: 1 }
store.dispatch({ type: 'counter/incremented' })
// { value: 2 }
store.dispatch({ type: 'counter/decremented' })
// { value: 1 }

Tip

If you are starting a new project, skip the core redux package and use Redux Toolkit directly: npm install @reduxjs/toolkit. It includes everything documented here plus configureStore, createSlice, createAsyncThunk, and more.

What to explore next:

  • Configuration section below for middleware and enhancer setup
  • Common Tasks for real-world patterns like async data fetching and SSR
  • Admin Guide for architecture, module reference, and build system internals

2. Installation

System Requirements

Requirement Minimum
Node.js 14+
TypeScript (optional) 4.7+ (5.x recommended)
Package Manager npm, yarn, or pnpm

Install Methods

npm

npm install redux

yarn

yarn add redux

pnpm

pnpm add redux

Package Exports

Redux 5.0.1 ships multiple module formats for broad compatibility:

Format Entry Point Use Case
ESM dist/redux.mjs Modern bundlers (Vite, Rollup, esbuild)
CJS dist/cjs/redux.cjs Node.js require()
Legacy ESM dist/redux.legacy-esm.js Webpack 4 ("module" field)
Browser ESM dist/redux.browser.mjs <script type="module"> (minified, production)
TypeScript dist/redux.d.mts Type definitions

The exports field in package.json routes imports automatically:

{
  "exports": {
    ".": {
      "types": "./dist/redux.d.mts",
      "import": "./dist/redux.mjs",
      "default": "./dist/cjs/redux.cjs"
    }
  }
}

Warning

The createStore function is marked @deprecated in the source. It still works and will not be removed, but your IDE will display a strikethrough. Import legacy_createStore instead to suppress the deprecation warning: import { legacy_createStore as createStore } from 'redux'

Verify Installation

node -e "const redux = require('redux'); console.log(typeof redux.createStore)"
function

Companion Packages

Package Purpose
@reduxjs/toolkit Official, batteries-included Redux development toolset
react-redux Official React bindings (Provider, useSelector, useDispatch)
redux-thunk Middleware for dispatching functions (included in RTK)
reselect Memoized selector library (included in RTK)

Important

Redux 5.x requires action type properties to be strings. If you are upgrading from Redux 4.x where non-string types were tolerated, you must update your action types to strings.

Install Troubleshooting

Error: Cannot find module 'redux'

Cause: Package not installed or node_modules corrupted.

Solution:

  1. Run npm install redux
  2. If already installed, delete node_modules and lock file, then reinstall

TypeScript cannot find type declarations

Cause: TypeScript version below 4.7 or moduleResolution misconfigured.

Solution:

  1. Ensure TypeScript 4.7+ is installed
  2. Set "moduleResolution": "node16" or "bundler" in tsconfig.json

Webpack 4: Cannot use import statement

Cause: Webpack 4 does not resolve the exports field.

Solution: Redux ships a "module" field pointing to dist/redux.legacy-esm.js for Webpack 4 compatibility. Verify your Webpack config resolves the module field.

createStore shows strikethrough in IDE

Cause: Intentional @deprecated annotation to encourage Redux Toolkit adoption.

Solution:

import { legacy_createStore as createStore } from 'redux'

3. Configuration

Redux has no configuration files. The store is configured entirely through code: the root reducer, optional preloaded state, and optional store enhancers.

Basic Store Creation

import { legacy_createStore as createStore } from 'redux'

const store = createStore(
  rootReducer,      // Required: root reducer function
  preloadedState,   // Optional: initial state for hydration / SSR
  enhancer          // Optional: store enhancer (e.g., applyMiddleware)
)

Adding Middleware

Middleware extends dispatch to handle side effects, logging, or async logic. Use applyMiddleware() as the store enhancer:

import { legacy_createStore as createStore, applyMiddleware } from 'redux'
import { thunk } from 'redux-thunk'

const store = createStore(rootReducer, applyMiddleware(thunk))

Note

Because middleware is potentially asynchronous, applyMiddleware should be the first (outermost) enhancer in the composition chain.

Combining Multiple Enhancers

Use compose to merge multiple store enhancers:

import { legacy_createStore as createStore, applyMiddleware, compose } from 'redux'
import { thunk } from 'redux-thunk'

const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose

const store = createStore(
  rootReducer,
  composeEnhancers(applyMiddleware(thunk))
)

Preloaded State (Server-Side Rendering)

Pass server-rendered state as the second argument:

// Server: serialize state into HTML
const preloadedState = store.getState()
const html = `<script>window.__PRELOADED_STATE__ = ${
  JSON.stringify(preloadedState).replace(/</g, '\\x3c')
}</script>`

// Client: hydrate
const store = createStore(rootReducer, window.__PRELOADED_STATE__)
delete window.__PRELOADED_STATE__

Warning

Always sanitize serialized state with .replace(/</g, '\\x3c') to prevent XSS when embedding in HTML.

Environment Variables

Redux checks process.env.NODE_ENV internally:

Value Effect
production Disables runtime warnings, type-checking assertions, and detailed error messages. Enables minified error codes with links to redux.js.org/Errors?code=N.
development (default) Enables warnings for common mistakes: unexpected state keys, undefined reducer returns, invalid action shapes.

See the Admin Guide for full build system and module format details.

4. Core Usage

Core Concepts

Redux is built on three principles:

  1. Single source of truth — the entire application state lives in one store
  2. State is read-only — the only way to change state is to dispatch an action
  3. Changes are made with pure functions — reducers take previous state and an action, return new state

Actions

Actions are plain objects with a string type property:

const incrementAction = { type: 'counter/incremented' }
const addTodoAction = { type: 'todos/added', payload: { text: 'Learn Redux' } }

Important

Actions must be plain objects with a string type property. Dispatching non-plain objects, actions with undefined type, or actions with non-string type throws an error. To dispatch functions, Promises, or other values, add the appropriate middleware.

Action creators are functions that return action objects:

const increment = () => ({ type: 'counter/incremented' })
const addTodo = (text) => ({ type: 'todos/added', payload: { text } })

store.dispatch(increment())
store.dispatch(addTodo('Learn Redux'))

Reducers

Reducers are pure functions: (state, action) => newState

function todosReducer(state = [], action) {
  switch (action.type) {
    case 'todos/added':
      return [...state, { text: action.payload.text, completed: false }]
    case 'todos/toggled':
      return state.map(todo =>
        todo.id === action.payload.id
          ? { ...todo, completed: !todo.completed }
          : todo
      )
    default:
      return state
  }
}

Reducer rules:

  • Must return the current state for any unrecognized action
  • Must return initial state when called with undefined state
  • Must never mutate the existing state — always return a new object
  • Initial state may not be undefined (use null for intentionally empty)
  • Must not call store.dispatch(), store.getState(), or store.subscribe()

Combining Reducers

Use combineReducers to split logic by state domain:

import { combineReducers } from 'redux'

const rootReducer = combineReducers({
  todos: todosReducer,
  visibilityFilter: visibilityFilterReducer
})
Resulting state shape:
{
  todos: [...],
  visibilityFilter: 'SHOW_ALL'
}

Note

Each key in the object passed to combineReducers becomes a top-level key in the state tree. Each slice reducer receives only its own slice — not the entire state tree.

The Store

The store holds the state tree and provides methods to interact with it:

Method Description
store.getState() Returns the current state tree
store.dispatch(action) Dispatches an action; returns the action
store.subscribe(listener) Registers a listener called after every dispatch; returns an unsubscribe function
store.replaceReducer(nextReducer) Replaces the root reducer (for code splitting / hot reload)
store[Symbol.observable]() Returns a minimal observable for reactive library interop

Subscribing to State Changes

const unsubscribe = store.subscribe(() => {
  console.log('State changed:', store.getState())
})

// Stop listening
unsubscribe()

Warning

Do not call store.getState(), store.subscribe(), or store.dispatch() from inside a reducer. The reducer is already executing within a dispatch call, and these operations will throw.

Binding Action Creators

bindActionCreators wraps action creators so they dispatch automatically:

import { bindActionCreators } from 'redux'

const actions = bindActionCreators(
  { increment, decrement, addTodo },
  store.dispatch
)

actions.increment()           // dispatched automatically
actions.addTodo('Learn Redux') // dispatched automatically

This is useful when passing action creators to components that should not know about dispatch.

Middleware

Middleware sits between dispatch and the reducer. It provides extension points for logging, crash reporting, async actions, and more.

Middleware signature:

const myMiddleware = store => next => action => {
  console.log('Dispatching:', action)
  const result = next(action)
  console.log('Next state:', store.getState())
  return result
}

Apply during store creation:

const store = createStore(
  rootReducer,
  applyMiddleware(myMiddleware, thunk, logger)
)

Composing Functions

compose chains single-argument functions from right to left:

import { compose } from 'redux'

const enhance = compose(f, g, h)
// equivalent to: (...args) => f(g(h(...args)))

Primarily used to compose multiple store enhancers.

Utility Functions

Function Description
isAction(value) Returns true if value is a plain object with a string type property
isPlainObject(value) Returns true if value is a plain object (prototype is Object.prototype or null)
import { isAction, isPlainObject } from 'redux'

isAction({ type: 'INCREMENT' })  // true
isAction({ type: 123 })          // false
isAction('INCREMENT')            // false

isPlainObject({})                // true
isPlainObject(new Date())        // false

TypeScript Usage

Redux 5.0.1 is written in TypeScript and ships comprehensive type definitions:

import type {
  Store, Dispatch, Action, UnknownAction, AnyAction,
  Reducer, ReducersMapObject, Middleware, MiddlewareAPI,
  StoreEnhancer, StoreCreator, StoreEnhancerStoreCreator,
  ActionCreator, ActionCreatorsMapObject,
  Unsubscribe, Observable, Observer,
  StateFromReducersMapObject, ReducerFromReducersMapObject,
  ActionFromReducer, ActionFromReducersMapObject,
  PreloadedStateShapeFromReducersMapObject
} from 'redux'

Typing a reducer:

interface CounterState {
  value: number
}

type CounterAction =
  | { type: 'counter/incremented' }
  | { type: 'counter/decremented' }

const counterReducer: Reducer<CounterState, CounterAction> = (
  state = { value: 0 },
  action
) => {
  switch (action.type) {
    case 'counter/incremented':
      return { value: state.value + 1 }
    case 'counter/decremented':
      return { value: state.value - 1 }
    default:
      return state
  }
}

Typing middleware:

import type { Middleware } from 'redux'

const loggerMiddleware: Middleware = store => next => action => {
  console.log('Dispatching:', action)
  const result = next(action)
  console.log('Next state:', store.getState())
  return result
}

Tip

Redux Toolkit's configureStore infers dispatch and getState types automatically. If using the core redux package directly, you must type the store, dispatch, and state manually.

5. Common Tasks

Task: Counter with Increment/Decrement

Goal: Create a simple counter store.

import { legacy_createStore as createStore } from 'redux'

function counterReducer(state = 0, action) {
  switch (action.type) {
    case 'INCREMENT':
      return state + 1
    case 'DECREMENT':
      return state - 1
    default:
      return state
  }
}

const store = createStore(counterReducer)
store.subscribe(() => console.log(store.getState()))

store.dispatch({ type: 'INCREMENT' })  // 1
store.dispatch({ type: 'INCREMENT' })  // 2
store.dispatch({ type: 'DECREMENT' })  // 1

Task: Todo List with Visibility Filter

Goal: Manage a list of todos with combined reducers.

import { legacy_createStore as createStore, combineReducers } from 'redux'

function todosReducer(state = [], action) {
  switch (action.type) {
    case 'ADD_TODO':
      return [...state, { id: action.id, text: action.text, completed: false }]
    case 'TOGGLE_TODO':
      return state.map(todo =>
        todo.id === action.id ? { ...todo, completed: !todo.completed } : todo
      )
    default:
      return state
  }
}

function visibilityFilterReducer(state = 'SHOW_ALL', action) {
  switch (action.type) {
    case 'SET_VISIBILITY_FILTER':
      return action.filter
    default:
      return state
  }
}

const store = createStore(combineReducers({
  todos: todosReducer,
  visibilityFilter: visibilityFilterReducer
}))

Task: Async Data Fetching with Thunk Middleware

Goal: Fetch remote data and store results.

Prerequisites: npm install redux-thunk

import { legacy_createStore as createStore, applyMiddleware } from 'redux'
import { thunk } from 'redux-thunk'

function postsReducer(state = { items: [], isFetching: false }, action) {
  switch (action.type) {
    case 'FETCH_POSTS_REQUEST':
      return { ...state, isFetching: true }
    case 'FETCH_POSTS_SUCCESS':
      return { ...state, isFetching: false, items: action.posts }
    case 'FETCH_POSTS_FAILURE':
      return { ...state, isFetching: false, error: action.error }
    default:
      return state
  }
}

const store = createStore(postsReducer, applyMiddleware(thunk))

function fetchPosts() {
  return async (dispatch) => {
    dispatch({ type: 'FETCH_POSTS_REQUEST' })
    try {
      const response = await fetch('https://jsonplaceholder.typicode.com/posts')
      const posts = await response.json()
      dispatch({ type: 'FETCH_POSTS_SUCCESS', posts })
    } catch (error) {
      dispatch({ type: 'FETCH_POSTS_FAILURE', error: error.message })
    }
  }
}

store.dispatch(fetchPosts())

Task: Logging Middleware

Goal: Log every action and resulting state.

const logger = store => next => action => {
  console.group(action.type)
  console.log('Previous state:', store.getState())
  console.log('Action:', action)
  const result = next(action)
  console.log('Next state:', store.getState())
  console.groupEnd()
  return result
}

const store = createStore(rootReducer, applyMiddleware(logger))

Task: Server-Side Rendering (Universal App)

Goal: Render state on the server and hydrate on the client.

Server:

import { legacy_createStore as createStore } from 'redux'
import { renderToString } from 'react-dom/server'
import { Provider } from 'react-redux'

function handleRequest(req, res) {
  const store = createStore(rootReducer, { counter: 42 })
  const html = renderToString(
    <Provider store={store}><App /></Provider>
  )
  const finalState = store.getState()
  res.send(`
    <div id="app">${html}</div>
    <script>
      window.__PRELOADED_STATE__ = ${JSON.stringify(finalState).replace(/</g, '\\x3c')}
    </script>
    <script src="/bundle.js"></script>
  `)
}

Client:

const store = createStore(rootReducer, window.__PRELOADED_STATE__)
delete window.__PRELOADED_STATE__

Task: Code Splitting with replaceReducer

Goal: Dynamically inject reducers for lazy-loaded routes.

import { combineReducers } from 'redux'

let currentReducers = { app: appReducer }
const store = createStore(combineReducers(currentReducers))

function injectReducer(key, reducer) {
  currentReducers[key] = reducer
  store.replaceReducer(combineReducers(currentReducers))
}

// Dynamically add on route load:
import('/routes/dashboard').then(module => {
  injectReducer('dashboard', module.dashboardReducer)
})

Task: Custom API Middleware

Goal: Intercept API call actions (pattern from the real-world example).

const CALL_API = Symbol('CALL_API')

const apiMiddleware = store => next => action => {
  const callAPI = action[CALL_API]
  if (typeof callAPI === 'undefined') {
    return next(action)
  }

  const { endpoint, types } = callAPI
  const [requestType, successType, failureType] = types

  next({ type: requestType })

  return fetch('https://api.github.com/' + endpoint)
    .then(response => response.json())
    .then(response => next({ type: successType, response }))
    .catch(error => next({ type: failureType, error: error.message }))
}

Task: Undo/Redo

Goal: Add undo/redo using redux-undo.

Prerequisites: npm install redux-undo

import { legacy_createStore as createStore } from 'redux'
import undoable from 'redux-undo'

const store = createStore(undoable(todosReducer))

// State shape: { past: [...], present: currentState, future: [...] }
store.dispatch({ type: 'ADD_TODO', text: 'Learn Redux' })
store.dispatch({ type: '@@redux-undo/UNDO' })
store.dispatch({ type: '@@redux-undo/REDO' })

// Access current state:
store.getState().present

Task: Observable / RxJS Interop

Goal: Use Redux with RxJS.

import { from } from 'rxjs'
import { map } from 'rxjs/operators'

const state$ = from(store)
state$.pipe(
  map(state => state.counter)
).subscribe(counter => console.log('Counter:', counter))

Note

Redux implements the TC39 Observable proposal (Symbol.observable). Any library supporting this protocol (RxJS, most.js) can subscribe to the store directly.

Task: Using with React

Prerequisites: npm install react-redux

import React from 'react'
import { createRoot } from 'react-dom/client'
import { Provider } from 'react-redux'
import { legacy_createStore as createStore } from 'redux'

const store = createStore(rootReducer)

createRoot(document.getElementById('root')).render(
  <Provider store={store}>
    <App />
  </Provider>
)

Then use useSelector and useDispatch hooks from react-redux in components.

Task: Conditional Data Fetching (Skip if Cached)

Goal: Only fetch data if it is not already in the store.

const fetchPostsIfNeeded = (subreddit) => (dispatch, getState) => {
  const posts = getState().postsBySubreddit[subreddit]
  if (posts && !posts.didInvalidate) {
    return  // already cached
  }
  dispatch(fetchPosts(subreddit))
}

store.dispatch(fetchPostsIfNeeded('reactjs'))

6. Troubleshooting

Diagnostic Decision Tree

Store not working?
├── Error on dispatch? → Check action is plain object with string type
├── State not updating? → Check reducer returns new object (not mutating)
├── Subscribers not firing? → Verify you are dispatching, not just calling action creator
├── Middleware not applied? → Verify applyMiddleware() passed as enhancer
└── TypeScript errors? → Check type imports and reducer annotations

Error: Actions must be plain objects

Cause: You dispatched a function, Promise, class instance, or other non-plain object. Common when using thunks without redux-thunk middleware.

Solution:

  1. If dispatching functions, install and apply redux-thunk:
    import { thunk } from 'redux-thunk'
    const store = createStore(reducer, applyMiddleware(thunk))
    
  2. If dispatching a regular action, ensure it is a plain object: { type: 'MY_ACTION' }

Prevention: Always apply redux-thunk if any action creators return functions.

Error: Actions may not have an undefined "type" property

Cause: Dispatched an object without a type property, or type is undefined. Often a misspelled constant.

Solution: Verify action creator returns an object with type set to a defined string constant.

Prevention: Use string literals directly or verified constant imports.

Error: Action "type" property must be a string

Cause: Action type is a number, Symbol, or other non-string value. Redux 5.x enforces string types.

Solution: Convert action types to strings.

Error: Reducers may not dispatch actions

Cause: A reducer called store.dispatch() during execution.

Solution: Move dispatch calls to middleware, thunks, or subscriber callbacks.

Prevention: Keep reducers pure: (state, action) => newState with no side effects.

Error: The slice reducer for key "X" returned undefined during initialization

Cause: A reducer returned undefined when called with undefined state.

Solution: Add a default parameter:

function myReducer(state = initialState, action) { ... }

Prevention: Always use default parameters. Use null for intentionally empty state, never undefined.

Error: The slice reducer for key "X" returned undefined when probed with a random type

Cause: Reducer lacks a default case returning current state.

Solution:

default:
  return state

Error: Dispatching while constructing your middleware is not allowed

Cause: Middleware called dispatch() during initial setup, before the chain is composed.

Solution: Only call dispatch inside the innermost function (the one receiving action).

Error: It looks like you are passing several store enhancers to createStore()

Cause: Multiple enhancers passed as separate arguments.

Solution:

import { compose, applyMiddleware } from 'redux'
const store = createStore(reducer, compose(applyMiddleware(thunk), devToolsEnhancer))

Error: You may not call store.getState() while the reducer is executing

Cause: Code inside a reducer called store.getState().

Solution: The reducer already receives state as its first argument. Use that directly.

Error: You may not call store.subscribe() while the reducer is executing

Cause: Subscription attempt inside a reducer.

Solution: Move subscriptions outside reducers.

Caution

All errors referencing "while the reducer is executing" indicate code running inside a dispatch cycle. Reducers must be pure — receive state and action, return new state, do nothing else.

State not updating after dispatch

Cause: Reducer mutates existing state instead of returning a new object.

Solution:

// Wrong — mutation:
state.items.push(newItem)
return state

// Correct — immutable update:
return { ...state, items: [...state.items, newItem] }

Subscribers called but UI not updating

Cause: With React-Redux, components re-render based on reference equality. If the reducer returns the same object, no re-render occurs.

Solution: Ensure reducers return new objects when state changes. combineReducers handles this — if any slice reducer returns a new value, the root object is new.

GitHub Issue #4375: Reactions update broken in tutorial

The Essentials tutorial's reactions feature does not update correctly in <SinglePostPage />. This is a documentation issue, not a core Redux bug.

GitHub Issue #3455: TypeScript ActionCreator loses argument types

The ActionCreator generic type may not preserve argument types in all cases. Use explicit function types or Redux Toolkit's createAction for better inference.

Warning

GitHub issue #4602 reports a TypeScript conflict between custom AppThunk types and createAsyncThunk.withTypes. This affects Redux Toolkit usage — see RTK documentation for workarounds.

7. FAQ

Q: Should I use Redux or Redux Toolkit?

Use Redux Toolkit for all new projects. It includes the core redux package and adds configureStore, createSlice, createAsyncThunk, and other utilities that eliminate boilerplate. The core redux package is for learning purposes or building custom abstractions.

Q: Can I use Redux without React?

Yes. Redux is framework-agnostic with zero runtime dependencies. Use it with Angular, Vue, Svelte, vanilla JavaScript, or Node.js. The store.subscribe() API works in any JavaScript environment.

Q: Is createStore being removed?

No. createStore will remain indefinitely. The @deprecated annotation is a visual IDE hint to encourage Redux Toolkit. Use legacy_createStore to import without the warning.

Q: Do I need middleware for async operations?

Yes. Redux's base dispatch only accepts plain objects. For async operations, use middleware:

Approach Library Pattern
Thunks redux-thunk Dispatch functions receiving (dispatch, getState)
Sagas redux-saga Generator functions yielding effects
Observables redux-observable RxJS epics transforming action streams
Listeners @reduxjs/toolkit createListenerMiddleware for reactive logic

Q: Can I have multiple stores?

Technically yes, but strongly discouraged. A single store enables DevTools, simplifies SSR, and makes state management predictable. Use combineReducers or RTK's combineSlices for logical separation within one store.

Q: How do I reset the entire store?

Handle a reset action in your root reducer:

const rootReducer = (state, action) => {
  if (action.type === 'RESET') state = undefined
  return appReducer(state, action)
}

Q: What is __DO_NOT_USE__ActionTypes?

An internal export containing Redux's private action types (@@redux/INIT, @@redux/REPLACE, @@redux/PROBE_UNKNOWN_ACTION). Do not reference these in application code — they are implementation details.

Q: How does sideEffects: false in package.json help?

It tells bundlers that Redux has no module-level side effects, enabling tree-shaking. Unused exports are removed from production bundles. This only works with ESM imports — CJS require() cannot be tree-shaken.

Q: Why does combineReducers warn about unexpected keys?

In development, if the state object has keys that don't match any reducer, combineReducers logs a warning. This catches typos and state shape mismatches. The warning is suppressed in production.

Q: Can I dispatch from inside a subscriber?

Yes, but with caveats. Subscriptions are snapshotted before each dispatch. If you subscribe/unsubscribe during notification, the change takes effect on the next dispatch. Nested dispatches from subscribers are allowed but can make state flow harder to follow.