🐻 Multiple Stores with Zustand, Persist and Typescript
A small, typed recipe for splitting a Zustand store into slices, persisting only the parts that matter, and keeping the whole thing honest with TypeScript.
I went looking for a clean example of Zustand with multiple slices, selective persistence, and solid TypeScript types, and could not find one that felt right. Here is the setup I landed on and have been happy with in real projects.
The idea is simple: keep each concern in its own slice file, compose them into a single store, and tell the persist middleware exactly which pieces belong in localStorage. That way nothing leaks, and adding a new slice later is almost boring, which is the goal.
We'll start with a theme slice. It's the canonical example because it's the kind of state you actually want to survive a page refresh.
import { StoreSlice } from './store'; type themeType = 'light' | 'dark'; export type ThemeSlice = { theme: themeType; toggleTheme: () => void; }; export const themeSlice: StoreSlice<ThemeSlice> = (set) => ({ theme: 'light', toggleTheme: () => set((state) => ({ theme: state.theme === 'light' ? 'dark' : 'light' })) });
Next, a sidebar slice. This one is pure UI state: whether the sidebar is open right now. There is no reason to persist it, and persisting it would actually feel wrong (users expect a fresh page to start clean).
import { StoreSlice } from './store'; export type SidebarSlice = { sidebarOpen: boolean; toggleSidebar: () => void; }; export const sidebarSlice: StoreSlice<SidebarSlice> = (set) => ({ sidebarOpen: false, toggleSidebar: () => set((state) => ({ sidebarOpen: !state.sidebarOpen })) });
Now we wire the slices together in a single store and use partialize to tell
persist which keys to actually write to localStorage. This is the piece that
usually gets hand-waved in examples, and it's where the type safety really pays
off.
import create from 'zustand'; import { persist, StoreApiWithPersist } from 'zustand/middleware'; import { ThemeSlice, themeSlice } from './themeSlice'; import { SidebarSlice, sidebarSlice } from './sidebarSlice'; // Create a type for our store export type Storestate = ThemeSlice & SidebarSlice; // Create a type for our store slices export type StoreSlice<T> = ( set: SetState<StoreState>, get: GetState<StoreState> ) => T; // Create our store with the slices we created export const useStore = create( persist< StoreState, SetState<StoreState>, GetState<StoreState>, StoreApiWithPersist<StoreState> >( (set, get) => ({ ...themeSlice(set, get), ...sidebarSlice(set, get) }), { name: 'our-local-storage-key', partialize: (state) => ({ // Only persist the theme slice theme: state.theme }) } ) );
What I like about Zustand is how little it asks of you. No providers, no reducers, no ceremony, just a typed hook you can compose. The same shape scales up: add a slice file, spread it into the store, and opt into persistence only for the fields that truly belong there. Good taste in state management is mostly knowing what not to persist, and this pattern makes that decision explicit in one place.