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
  • 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;
}

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;

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 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.