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 corereduxpackage with simplified APIs, built-in best practices, and pre-configured middleware. The corereduxpackage 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
reduxpackage and use Redux Toolkit directly:npm install @reduxjs/toolkit. It includes everything documented here plusconfigureStore,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
createStorefunction is marked@deprecatedin the source. It still works and will not be removed, but your IDE will display a strikethrough. Importlegacy_createStoreinstead 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
typeproperties 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:
- Run
npm install redux - If already installed, delete
node_modulesand lock file, then reinstall
TypeScript cannot find type declarations
Cause: TypeScript version below 4.7 or moduleResolution misconfigured.
Solution:
- Ensure TypeScript 4.7+ is installed
- Set
"moduleResolution": "node16"or"bundler"intsconfig.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,
applyMiddlewareshould 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:
- Single source of truth — the entire application state lives in one store
- State is read-only — the only way to change state is to dispatch an action
- 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
typeproperty. Dispatching non-plain objects, actions withundefinedtype, 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
undefinedstate - Must never mutate the existing state — always return a new object
- Initial state may not be
undefined(usenullfor intentionally empty) - Must not call
store.dispatch(),store.getState(), orstore.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
combineReducersbecomes 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(), orstore.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
configureStoreinfersdispatchandgetStatetypes automatically. If using the corereduxpackage 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:
- If dispatching functions, install and apply
redux-thunk:import { thunk } from 'redux-thunk' const store = createStore(reducer, applyMiddleware(thunk)) - 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
AppThunktypes andcreateAsyncThunk.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.