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

Persisting State with LocalStorage

Hello! Welcome to the next lesson in our series on Zustand.

In our last session, we integrated the devtools middleware to connect our store to the Redux DevTools, giving us powerful debugging capabilities. We saw how middleware wraps our store creator to add functionality and that the order of this wrapping is important.

Today, we'll tackle a very common requirement in web applications: persisting state across page reloads. You've likely implemented this manually with localStorage before. With Zustand, this process is streamlined through the persist middleware. This lesson will teach you how to apply this middleware to automatically save your store's state and rehydrate it when the user returns.


1. The Problem: State Loss on Refresh

By default, a Zustand store, like any JavaScript variable, is held in memory. When you refresh the page, the entire JavaScript context is reset, and your application's state reverts to its initial values.

The video below demonstrates this problem. A simple counter is incremented, but upon refreshing the page, the count resets to zero.

React Zustand Tutorial - 6 Persist Middleware(typescript)

This clip from the 'React Zustand Tutorial - 6' by Conny Gu clearly illustrates the problem we're about to solve.

Watch from the beginning until 01:05. Notice how the state of the 'bears' and 'cats' is lost after the page is refreshed.

2. Basic Persistence with localStorage

The persist middleware solves this problem elegantly. You wrap your store's creator function with persist, provide a unique name for the storage key, and Zustand handles the rest.

First, import persist from zustand/middleware. Then, apply it similarly to how we applied devtools.

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

const useBearStore = create(
  persist(
    (set, get) => ({
      bears: 0,
      increase: () => set({ bears: get().bears + 1 }),
    }),
    {
      name: 'bear-storage', // unique name for the storage item
    }
  )
);

The name option is the only required configuration. It defines the key under which your state will be saved in localStorage.

React Zustand Tutorial - 6 Persist Middleware(typescript)

The same video now demonstrates how to implement the persist middleware and shows the immediate effect.

Watch from 01:41 to 03:06. Pay close attention to how the store creator is wrapped. After the change, open your browser's DevTools, go to the 'Application' tab, and inspect 'Local Storage' to see the persisted state yourself.

3. Advanced Configuration

While the default behavior is useful, persist offers several options for more granular control. These are all passed in the second argument's configuration object.

Combining with devtools

A common pattern is to use persist and devtools together. As we discussed, middleware order is important. The recommended practice is to have devtools wrap persist. This ensures that all actions, including the rehydration event from persist, are captured by the Redux DevTools.

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

const useCatStore = create(
  devtools( // devtools is the outer wrapper
    persist( // persist is the inner wrapper
      (set) => ({
        cats: 0,
        addCat: () => set((state) => ({ cats: state.cats + 1 })),
      }),
      {
        name: 'cat-storage',
      }
    )
  )
);

React Zustand Tutorial - 6 Persist Middleware(typescript)

This clip demonstrates the correct order for combining persist and devtools.

Watch from 08:34 to 10:07. The key takeaway is the nesting order: devtools(persist(...)). Notice how the 'cat-store' now appears in local storage and persists across reloads.

Selective Persistence with partialize

You often don't need or want to save the entire state. For example, you might want to avoid persisting transient UI flags or sensitive information. The partialize option lets you select which parts of the state to save.

Persisting store data - Zustand

The official Zustand documentation provides clear examples for how to use the partialize option to either include or exclude specific fields from being persisted.

Read the section titled 'partialize'. You'll see two common patterns: one for explicitly picking fields to keep, and another for filtering out fields to exclude.

Here's an example of persisting only the bears count:

// ...
persist(
  (set) => ({
    bears: 0,
    color: 'red', // This will not be saved
    increase: () => set((state) => ({ bears: state.bears + 1 })),
  }),
  {
    name: 'bear-storage',
    partialize: (state) => ({ bears: state.bears }), // Only persist the 'bears' field
  }
)
// ...

The video below gives a practical demonstration of implementing this.

React Zustand Tutorial - 6 Persist Middleware(typescript)

This segment shows how to use partialize to save only a portion of the state, and also demonstrates a clever way to exclude certain keys.

Watch from 04:24 to 06:47. Note the difference in the stored object in localStorage before and after applying the partialize option.

Changing the Storage Engine

By default, persist uses localStorage. You can easily switch to sessionStorage or even more complex asynchronous storage like IndexedDB. To do this, you use the createJSONStorage helper.

React Zustand Tutorial - 6 Persist Middleware(typescript)

This clip shows how to switch the storage from the default localStorage to sessionStorage.

Watch from 03:06 to 04:24. The key is the storage property in the configuration, which uses createJSONStorage to specify the desired storage API.

Here is the code to switch to sessionStorage:

import { createJSONStorage } from 'zustand/middleware';

// ...
persist(
  (set) => ({ ... }),
  {
    name: 'session-storage-example',
    storage: createJSONStorage(() => sessionStorage), // Switch to sessionStorage
  }
)
// ...

4. Managing Persisted State

The persist middleware also attaches a small API to your store hook, allowing you to interact with the storage programmatically.

Clearing Storage

A common requirement is to clear the persisted state, for instance, when a user logs out. You can call useStore.persist.clearStorage().

It's important to understand that this function only clears the data from the storage engine (e.g., localStorage). It does not reset the current in-memory state of your application.

React Zustand Tutorial - 6 Persist Middleware(typescript)

This final clip demonstrates how to use clearStorage and highlights the crucial difference between clearing storage and resetting the in-memory state.

Watch from 06:47 to 08:34. Notice that after clicking 'Clear Storage', the value in localStorage is gone, but the UI still shows the old value until the page is refreshed. This illustrates why you might also need a separate reset action in your store.

5. Understanding Hydration

The process of reading data from storage and applying it to your store is called hydration.

Persisting store data - Zustand

The official documentation has an excellent explanation of hydration and the important distinction between synchronous and asynchronous storage.

Read the section 'Hydration and asynchronous storages'. This will clarify why using localStorage (synchronous) is straightforward, but using something like AsyncStorage (asynchronous) can introduce timing issues where your component might render before the state is fully hydrated.

When using localStorage or sessionStorage, hydration is synchronous, meaning your store is hydrated with the persisted data before your application's first render. This is typically the behavior you want.

If you ever use an asynchronous storage, you may need to handle cases where your UI renders with initial state before the persisted state has been loaded. The documentation provides patterns for this, such as creating a custom hook to track hydration status, which we can explore if it becomes relevant to your projects.


Conclusion

You have now learned how to make your Zustand stores persistent, a critical feature for creating a seamless user experience. By leveraging the persist middleware, you can avoid manual localStorage management and benefit from a clean, configurable API.

Key Takeaways:

  • The persist middleware saves store state to a storage engine and rehydrates it on page load.
  • It is implemented by wrapping the store creator: create(persist((set) => ..., { name: '...' })).
  • The name option is required and must be a unique key for the storage.
  • When used with devtools, the recommended order is devtools(persist(...)).
  • The partialize option gives you fine-grained control over which parts of the state are saved.
  • The default storage is localStorage, but it can be changed to sessionStorage or others using createJSONStorage.
  • You can programmatically clear the storage with useStore.persist.clearStorage(), but this does not reset the in-memory state.

Next Lesson Preview:

We have now used two powerful, built-in middlewares: devtools and persist. But what if you have a specific, cross-cutting concern that isn't covered by the official middleware? In the next lesson, we will learn how to create your own custom middleware to implement logic like logging state changes to the console.

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

Sign up