First look at Zustand state management for Orcasound
The Orcasound Next prototype has three contexts that should be migrated to Zustand stores:
DataContext
- global filter state set by UI
- resulting array of filtered data
- array of new objects derived from filtered data
- summary metrics about filtered data
LayoutContext
- UI states that persist across page routes
NowPlayingContext
- global audio player controls
- Web Audio API analyser node
Migrating these is actually fairly straightforward, it’s mostly like this:
- Install –
npm install zustand - Add stores in
/stores/exampleStore.tsx - Set up store: ```ts import { create } from “zustand”;`
type ExampleStore = { myState: boolean; setMyState: (value: boolean) => void; };
export const useExampleStore = create
But there are a couple of gotchas I am coming across, as below.
___
3. **Selector-based subscriptions**
Context can trigger unnecessary global re-renders when a local component changes the state. Zustand behaves the same way unless you specifically target one slice of state.
For example if I have a LayoutContext provider, I typically access global state from any nested component within `<LayoutContext>{children}</LayoutContext>` like this:
```ts
const { alertOpen, setAlertOpen } = useLayout()
But every time I use setAlertOpen, I am at risk of triggering re-renders to everything inside the LayoutContext provider.
With Zustand, the default approach does the same thing:
const { alertOpen, setAlertOpen } = useLayoutStore()
This subscribes to the entire store, so the component re-renders if there is a change to any unrelated state in that store (e.g. activeMobileTab, drawerContent).
To avoid this, we need to subscribe only to specific slices from the store like this:
const alertOpen = useLayoutStore((state) => state.alertOpen);
const setAlertOpen = useLayoutStore((state) => state.setAlertOpen);
This is more verbose, but we can also export a custom hook for each slice like this, to make it more streamlined:
export const useAlertOpen = () => useLayoutStore((s) => s.alertOpen);
export const useSetAlertOpen = () => useLayoutStore((s) => s.setAlertOpen);
In components:
const alertOpen = useAlertOpen();
const setAlertOpen = useSetAlertOpen();
- Custom state setters
React has a built-in SetStateAction in the useState hook that can take either a value (e.g. ‘true’) or a function (e.g. (prev) => !prev). Zustand does not have this – each state setter needs custom handling for different inputs.
So you can make your Zustand state setter functions look like React ones:
setAlertOpen(true);
But you have to define that behavior explicitly in the Zustand store:
setAlertOpen: (value: boolean) => set({ alertOpen: value })
And where React allows you to pass a function like this:
setAlertOpen(prev => !prev);
In Zustand you might define a specific action for that:
toggleAlertOpen()
In store:
toggleAlertOpen: () => set((state) => ({ alertOpen: !state.alertOpen }))
Or, make the state setter responsive to either a value or function:
setAlertOpen: (input) =>
set((state) => ({
alertOpen:
typeof input === "function" ? input(state.alertOpen) : input,
}))
Anyway, that’s progress for now – let me know any thoughts!