Skip to main content

Someone wanted to learn this too, so Grasp built them a personal learning path.

You can keep exploring this one or create your own

Zustand Middleware: Extending Store Functionality

Hello! Welcome to your next lesson on Zustand.

In the previous modules, you've learned how to create stores, define state and actions, and implement advanced patterns like asynchronous actions and store slices. Now, we'll explore how to add powerful, cross-cutting features to your stores in a clean and reusable way.

This lesson addresses the first learning outcome in the "Middleware and DevTools" module: Describe the concept of middleware in Zustand and how it extends store functionality. We will demystify what middleware is, how it works conceptually, and see examples of how it can be used to add features like debugging and state persistence.


1. The Concept of Middleware

In software engineering, "middleware" generally refers to code that sits between two other layers of software, processing data or requests as they pass through. If you've worked with web servers like Express.js or Koa.js, you're likely familiar with middleware functions that process an incoming HTTP request before it reaches its final handler.

In Zustand, the concept is analogous. Middleware is a function that wraps your store's creator function. It intercepts state updates and other store operations to add extra functionality.

Think of it as an assembly line for your state changes:

  • Your Action: An action is called, intending to update the state.
  • Middleware Layer: Before the state is actually changed, it passes through one or more middleware functions. Each middleware can inspect the change, log it, save it, or even modify it.
  • The Store: The final, possibly augmented, state change is applied to the store.

This pattern is incredibly powerful because it allows you to keep your core business logic (the state and actions in your store) separate from cross-cutting concerns like logging, debugging, or storage persistence.

2. How Middleware Extends Functionality

Zustand provides several built-in middleware functions that you can import from zustand/middleware. Let's look at what some of the most common ones do.

The following article gives a concise overview of middleware and lists the most widely used ones.

React State Management — using Zustand

Please read the following section from the article 'React State Management — using Zustand' on Medium. It introduces the concept of middleware and lists the most common ones.

Read the section titled 'Zustand Middlewares' and the brief introduction to 'Redux DevTools' middleware. Focus on understanding that middleware is an 'advantage' that adds more features.

As you saw, middleware can be used for:

  • Debugging: Connecting your store to browser extensions like Redux DevTools.
  • State Persistence: Automatically saving your store's state to localStorage or sessionStorage and rehydrating it on page load.
  • Immutability: Integrating with libraries like Immer to make state updates safer and easier to write.

3. The "Wrapping" Pattern in Practice

Let's see how this "wrapping" actually looks in code. A middleware is a higher-order function—it takes your store's setup function as an argument and returns a new, enhanced version.

To get a feel for this, please watch this short segment from the "React state management crash course" video.

React state management crash course | Zustand

This clip from Hitesh Choudhary's crash course introduces the devtools and persist middleware, explaining their purpose and showing where they are imported from.

Watch from 07:43 to 08:40. Pay attention to how devtools and persist are imported from zustand/middleware and the brief explanation of what each one does.

Now, let's look at a clear code example that shows how to apply this wrapping pattern.

How to use Zustand

The article 'How to use Zustand' from the Refine blog provides a very clear, minimal example of applying the devtools middleware.

Read the section 'Use Middleware with Zustand'. Notice how the create function now takes devtools(...) as its argument, and the original store setup function is passed inside devtools.

The key syntax is:

import { create } from 'zustand';
import { devtools } from 'zustand/middleware';

// Your original store creator function
const storeCreator = (set) => ({
  count: 0,
  increment: () => set((state) => ({ count: state.count + 1 })),
});

// Wrap the creator with middleware
const useStore = create(devtools(storeCreator));

You can also chain or compose multiple middlewares. The order of wrapping matters, as each middleware wraps the next one in the chain. For example, to use both devtools and persist:

import { create } from 'zustand';
import { devtools, persist } from 'zustand/middleware';

const useStore = create(
  devtools( // Outer middleware
    persist( // Inner middleware
      (set) => ({
        fishes: 0,
        addFish: () => set((state) => ({ fishes: state.fishes + 1 })),
      }),
      {
        name: 'fish-storage', // unique name for persistence
      }
    )
  )
);

In this case, any action will first go through the devtools middleware (so it can be logged) and then the persist middleware (so the new state can be saved to storage).

4. Under the Hood: A Conceptual Model

To solidify your understanding, let's briefly look at how a middleware might work conceptually. It's essentially a higher-order function that gets access to the set and get functions of the store and can provide its own modified versions to your actual store creator.

Here is a simplified, conceptual implementation of a logging middleware. This is not the actual Zustand source code, but it illustrates the principle:

// A function that creates our logging middleware
const logger = (storeCreator) => {
  
  // The new function that will be passed to Zustand's `create`
  return (set, get, api) => {
    
    // Create a new, enhanced `set` function
    const loggedSet = (...args) => {
      console.log("Zustand Logger: calling set", args);
      console.log("Zustand Logger: previous state", get());
      
      // Call the original `set` function
      set(...args);
      
      console.log("Zustand Logger: new state", get());
    };

    // Call the original store creator, but give it our enhanced `loggedSet`
    return storeCreator(loggedSet, get, api);
  };
};

// How you would use it
const useCountStore = create(
  logger( // Our custom middleware wraps the creator
    (set) => ({
      count: 0,
      increment: () => set(state => ({ count: state.count + 1 })),
    })
  )
);

By understanding this pattern, you can see that middleware isn't magic. It's a functional programming pattern that leverages composition to add behavior in a modular and declarative way. This prepares you to not only use existing middleware but also to create your own, which we will cover in a later lesson.


Conclusion

In this lesson, we introduced the concept of middleware in Zustand.

Key Takeaways:

  • Middleware extends store functionality by wrapping the store creator function.
  • It's a powerful pattern for handling cross-cutting concerns like debugging, logging, and state persistence without cluttering your core store logic.
  • Zustand's built-in middleware, such as devtools and persist, are imported from zustand/middleware.
  • Middleware is applied by composing it around your store definition, for example: create(devtools(persist(...))). The order of composition is important.

Next Lesson Preview:

Now that you understand the "what" and "why" of middleware, our next lesson will be a practical deep-dive into one of the most essential middlewares: devtools. You will learn how to connect your Zustand store to the Redux DevTools browser extension for a vastly improved debugging experience, including action logging and time-travel debugging.

Can't find a good explanation? Sign up and we'll make it for you

Sign up