One Tip a Week: Zustand Middleware & Storage

This week’s tip of the week is about Zustand.

It comes from a PR I currently have open, pomerium/mcp-app-demo/pull/160. I originally was using the useLocalStorage custom hook but opted for Zustand’s persist middleware.

  • Before: using useLocalStorage, state persisted but changes only appeared on the next render

  • After: with Zustand, updates apply instantly and state is automatically saved to storage and rehydrated on load

Here’s a simplified version of the background jobs store from that PR:

import { create } from "zustand";
import { persist } from "zustand/middleware";
import type { BackgroundJob } from "@/lib/schemas";
import { backgroundJobSchema } from "@/lib/schemas";

export const BACKGROUND_JOBS_STORAGE_KEY = "background-jobs";

interface BackgroundJobsState {
  jobs: Record<string, BackgroundJob>;
  addJob: (job: BackgroundJob) => void;
  removeJob: (id: string) => void;
}

export const useBackgroundJobsStore = create<BackgroundJobsState>()(
  persist(
    (set) => ({
      jobs: {},
      addJob: (job) => {
        const validated = backgroundJobSchema.parse(job);
        set((state) => ({
          jobs: { ...state.jobs, [validated.id]: validated },
        }));
      },
      removeJob: (id) => {
        set((state) => {
          const { [id]: _, ...rest } = state.jobs;
          return { jobs: rest };
        });
      },
    }),
    {
      name: BACKGROUND_JOBS_STORAGE_KEY, // stored in localStorage by default
    }
  )
);

See the file in the PR for the full store with persist in action.

Since no custom storage is provided, Zustand automatically uses localStorage. It also hydrates state on load, so your background jobs reappear as soon as the app initializes.

If you want a different backend, you can pass a custom storage option, for example sessionStorage or IndexedDB via createJSONStorage.

Zustand is awesome. Simple API, instant updates, and persistence that just works.

That’s it! Short and sweet. Until the next one!