How Zustand Supports Middleware

Learning Zustand in JavaScript Without Types Is Easier Than With Types

Hi,

Today, I’d like to talk about how Zustand supports middleware. Zustand is a tiny library with two parts: the vanilla part and the React part. You can check out the source code here:

Together, they contain just 43 lines of code. Zustand’s core in JavaScript is very small. (The TypeScript types are more complex, but that's not today’s focus.)

Understanding Zustand’s createStore

Middleware in Zustand is not a built-in feature—there’s no special logic for it in the library. Instead, it’s a capability that naturally emerges from how createStore is designed.

Here’s a simplified version of Zustand’s vanilla implementation:

export const createStore = (createState) => {
  let state;
  const listeners = new Set();
  const setState = (nextState) => {
    state = Object.assign({}, state, nextState);
    listeners.forEach((listener) => listener(state));
  };
  const getState = () => state;
  const subscribe = (listener) => {
    listeners.add(listener);
    return () => listeners.delete(listener);
  };
  const api = { setState, getState, subscribe };
  state = createState(setState, getState, api);
  return api;
};

Take a moment to look over the whole function. The important line is this one:

  state = createState(setState, getState, api);

This line is very Zustand-ish. Technically, api already includes setState and getState, so calling createState(api) could be enough. However, Zustand’s approach of passing all three arguments allows for a shorthand style like this:

createStore((set) => ({
  count: 0,
  setOne: () => set(1),
}));

In this case, set refers to api.setState. Most of the time, the second (get) and third (api) arguments are not needed.

No Middleware-Specific Code, Yet It Works

You may notice that there’s no code specifically handling middleware. That’s the beauty of it—the fact that createState receives api enables middleware. You could even say middleware support was a by-product of this design.

Here’s an example of how a logger middleware works:

const logger = (createState) => (set, get, api) => {
  const newSet = (nextState) => {
    console.log('Setting next state', nextState);
    set(nextState);
  };
  api.setState = newSet;
  return createState(newSet, get, api);
};

// Usage
const store = createStore(
  logger((set) => ({
    count: 0,
    setOne: () => set(1),
  }))
);

The logger is just a higher-order function wrapping createState. It intercepts set calls, adds logging, and passes the modified set to the rest of the store setup.

Honestly, I sometimes wonder if this should even be called "middleware"—but in Zustand’s world, that’s the term we use.

In Summary

There is no middleware-specific logic in Zustand’s library code. But because createState receives the api object (and its internal methods), we can build higher-order wrappers around it. That’s what we call middleware in Zustand.

Happy coding.

Reply

or to participate.