Redux's createStore
is at the heart of the Redux architecture. It's the fundamental API for creating a Redux store. In this deep dive, we’ll explore everything about createStore
— from its arguments to advanced usage patterns, internal logic, edge cases, and best practices.
💡 Basic Signature
createStore(
reducer: Reducer,
preloadedState?: any,
enhancer?: StoreEnhancer
): Store
-
reducer
: (Required) A pure function that returns the next state tree, given the current state and an action. -
preloadedState
: (Optional) The initial state. Useful for server-side rendering or persisting state across sessions. -
enhancer
: (Optional) A higher-order function that extends store capabilities (e.g., middleware, DevTools).
✅ Valid Usage Patterns
Redux allows four distinct ways to call createStore
:
createStore(reducer)
createStore(reducer, preloadedState)
createStore(reducer, enhancer)
createStore(reducer, preloadedState, enhancer)
So how does Redux differentiate between preloadedState
and enhancer
when only two arguments are provided?
🔍 Internal Check
Redux uses type checking to distinguish them:
if (typeof preloadedState === 'function' && typeof enhancer === 'undefined') {
enhancer = preloadedState;
preloadedState = undefined;
}
This ensures clarity even when developers omit preloadedState
and directly pass an enhancer
as the second argument.
❓What if...
The reducer is not a function?
Redux throws an error becausereducer
is the core of the store logic.preloadedState
is not an object?
It will be accepted unless the reducer breaks due to unexpected state shape. It's the reducer's responsibility to handle and validate state.
🧠 Understanding Enhancers
Enhancers are store customizers. They wrap the store creator to augment behavior. They follow the signature:
const enhancer = (createStore) => (reducer, initialState) => store;
Here’s how Redux applies an enhancer:
return enhancer(createStore)(reducer, preloadedState);
If an enhancer returns a new store, Redux will use that instead of the default.
🧪 Sample Enhancer: Logger + Performance
const loggerAndPerformanceEnhancer = (createStore) => (reducer, initialState) => {
const enhancedReducer = (state, action) => {
console.log('Previous State:', state);
const start = performance.now();
const nextState = reducer(state, action);
const duration = performance.now() - start;
console.log('Next State:', nextState);
console.log('Reducer time:', duration, 'ms');
return nextState;
};
return createStore(enhancedReducer, initialState);
};
const store = createStore(reducer, loggerAndPerformanceEnhancer);
🔄 getState()
Returns the current state held by the store.
store.getState();
📢 subscribe(listener)
Registers a callback to be invoked after every dispatch. Returns an unsubscribe function.
const unsubscribe = store.subscribe(() => {
console.log('State changed!');
});
Redux internally manages listeners by Map
.
let currentListeners: Map<number, ListenerCallback> | null = new Map()
let nextListeners = currentListeners
Why Map? It provides O(1) lookup/deletion and unique keys, avoiding array filtering.
How does nextListeners ensure safety? It creates a new Map during subscription/unsubscription to avoid mutating the list during iteration.
Check the actual code here
🚀 dispatch(action)
The only way to update the state. It forwards the action to the reducer, updates the state, and notifies subscribers.
store.dispatch({ type: 'INCREMENT' });
Internally:
try {
isDispatching = true;
currentState = currentReducer(currentState, action);
} finally {
isDispatching = false;
}
currentListeners.forEach(listener => listener());
🛠 replaceReducer(newReducer)
Allows dynamic replacement of the reducer (e.g., for code splitting).
store.replaceReducer(newRootReducer);
📡 observable()
Redux supports interop with TC39 Observables via:
store[Symbol.observable]();
Useful for integration with RxJS or any reactive libraries.
🧓 legacy_createStore
Redux Toolkit discourages using createStore
directly. However, legacy_createStore
is provided to avoid deprecation warnings. It behaves identically.
🧪 Summary of Common Issues
- Confusing
preloadedState
andenhancer
➝ always check types - Passing incorrect reducer ➝ always pass a pure function
🏁 Conclusion
The createStore
API is still a vital piece of Redux. While Redux Toolkit (RTK) is the recommended standard today, understanding createStore
is essential to mastering Redux’s internals, because RTK configureStore
also uses createStore to build the store.