DEV Community

Ayush Dutt Sharma
Ayush Dutt Sharma

Posted on

Deep Dive into `createStore` in Redux

Redux

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
Enter fullscreen mode Exit fullscreen mode
  • 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:

  1. createStore(reducer)
  2. createStore(reducer, preloadedState)
  3. createStore(reducer, enhancer)
  4. 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;
}
Enter fullscreen mode Exit fullscreen mode

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 because reducer 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;
Enter fullscreen mode Exit fullscreen mode

Here’s how Redux applies an enhancer:

return enhancer(createStore)(reducer, preloadedState);
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

πŸ”„ getState()

Returns the current state held by the store.

store.getState();
Enter fullscreen mode Exit fullscreen mode

πŸ“’ subscribe(listener)

Registers a callback to be invoked after every dispatch. Returns an unsubscribe function.

const unsubscribe = store.subscribe(() => {
  console.log('State changed!');
});
Enter fullscreen mode Exit fullscreen mode

Redux internally manages listeners by Map.

  let currentListeners: Map<number, ListenerCallback> | null = new Map()
  let nextListeners = currentListeners

Enter fullscreen mode Exit fullscreen mode

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' });
Enter fullscreen mode Exit fullscreen mode

Internally:

try {
  isDispatching = true;
  currentState = currentReducer(currentState, action);
} finally {
  isDispatching = false;
}

currentListeners.forEach(listener => listener());
Enter fullscreen mode Exit fullscreen mode

πŸ›  replaceReducer(newReducer)

Allows dynamic replacement of the reducer (e.g., for code splitting).

store.replaceReducer(newRootReducer);
Enter fullscreen mode Exit fullscreen mode

πŸ“‘ observable()

Redux supports interop with TC39 Observables via:

store[Symbol.observable]();
Enter fullscreen mode Exit fullscreen mode

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 and enhancer ➝ 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.

Top comments (0)