Zustand Best Practices: Optimization, Persistence, Debugging, and Multi‑Instance Management
This article shares practical insights and best‑practice techniques for using the Zustand state‑management library in React, covering component re‑render optimization, selective persistence, debugging with dev‑tools, handling multiple store instances, custom selectors, and a Vite plugin for automatic selector injection.
In this tutorial the author explains how to use the zustand state‑management library in a React project, focusing on reducing unnecessary re‑renders, persisting selected state fields, debugging, and supporting multiple independent store instances.
Store definition
import { create } from 'zustand';
interface State { theme: string; lang: string; }
interface Action { setTheme: (theme: string) => void; setLang: (lang: string) => void; }
const useConfigStore = create<State & Action>((set) => ({
theme: 'light',
lang: 'zh-CN',
setLang: (lang) => set({ lang }),
setTheme: (theme) => set({ theme })
}));
export default useConfigStore;Basic component usage (causes unnecessary renders)
import useConfigStore from './store';
const Theme = () => {
const { theme, setTheme } = useConfigStore();
console.log('theme render');
return (
<div>
<div>{theme}</div>
<button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>切换</button>
</div>
);
};
export default Theme;Changing theme also triggers a re‑render of a component that only reads lang, because the whole store object is returned.
Solution 1 – select individual values
const theme = useConfigStore(state => state.theme);
const setTheme = useConfigStore(state => state.setTheme);
// component now only re‑renders when theme changesThis works because zustand shallow‑compares the selected values.
Solution 2 – object selector (needs shallow comparison)
const { theme, setTheme } = useConfigStore(state => ({
theme: state.theme,
setTheme: state.setTheme,
}));Returning a new object each render breaks the optimisation; the library sees a new reference and re‑renders.
To fix this, useShallow from zustand/react/shallow can be used:
import { useShallow } from 'zustand/react/shallow';
const { theme, setTheme } = useConfigStore(
useShallow(state => ({ theme: state.theme, setTheme: state.setTheme }))
);Solution 3 – custom useSelector hook
import { pick } from 'lodash-es';
import { useRef } from 'react';
import { shallow } from 'zustand/shallow';
type Pick<T, K extends keyof T> = { [P in K]: T[P] };
type Many<T> = T | readonly T[];
export function useSelector<S extends object, P extends keyof S>(paths: Many<P>) {
const prev = useRef<Pick<S, P>>({} as Pick<S, P>);
return (state: S) => {
if (state) {
const next = pick(state, paths);
return shallow(prev.current, next) ? prev.current : (prev.current = next);
}
return prev.current;
};
}Usage:
import useConfigStore from './store';
import { useSelector } from './use-selector';
const Theme = () => {
const { theme, setTheme } = useConfigStore(useSelector(['theme', 'setTheme']));
console.log('theme render');
return (
<div>
<div>{theme}</div>
<button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>切换</button>
</div>
);
};
export default Theme;Vite plugin that injects useSelector automatically
import generate from '@babel/generator';
import parse from '@babel/parser';
import traverse from '@babel/traverse';
import * as t from '@babel/types';
export default function zustand() {
return {
name: 'zustand',
transform(src, id) {
if (!/\.tsx?$/.test(id)) {
return { code: src, map: null };
}
const ast = parse.parse(src, { sourceType: 'module' });
let flag = false;
traverse.default(ast, {
VariableDeclarator(path) {
if (path.node?.init?.callee?.name === 'useStore') {
const keys = path.node.id.properties.map(o => o.value.name);
path.node.init.arguments = [
t.callExpression(
t.identifier('useSelector'),
[t.arrayExpression(keys.map(o => t.stringLiteral(o)))]
)
];
flag = true;
}
}
});
if (flag) {
if (!src.includes('useSelector')) {
ast.program.body.unshift(
t.importDeclaration(
[t.importSpecifier(t.identifier('useSelector'), t.identifier('useSelector'))],
t.stringLiteral('useSelector')
)
);
}
const { code } = generate.default(ast);
return { code, map: null };
}
return { code: src, map: null };
}
};
}The plugin parses .tsx files, finds calls to useStore, extracts the accessed keys, and rewrites the call to useStore(useSelector([...keys])), adding an import if necessary.
Selective persistence
import { create } from 'zustand';
import { createJSONStorage, persist } from 'zustand/middleware';
interface State { theme: string; lang: string; }
interface Action { setTheme: (theme: string) => void; setLang: (lang: string) => void; }
const useConfigStore = create(
persist<State & Action>((set) => ({
theme: 'light',
lang: 'zh-CN',
setLang: (lang) => set({ lang }),
setTheme: (theme) => set({ theme })
}), {
name: 'config',
storage: createJSONStorage(() => localStorage),
// to persist only specific fields use `partialize`
})
);
export default useConfigStore;When only part of the state should survive page reloads, the partialize helper can be used to pick specific keys.
Debugging with Redux DevTools
import { devtools } from 'zustand/middleware';
const useStore = create(devtools((set) => ({ /* … */ })));The dev‑tools middleware enables time‑travel debugging and shows action names; a third argument to set can label the action.
Multiple store instances
import React, { createContext, useRef, useContext } from 'react';
import { createStore, StoreApi, useStore as useExternalStore } from 'zustand';
export const StoreContext = createContext<StoreApi<State & Action>>({} as StoreApi<State & Action>);
export const StoreProvider = ({ children }) => {
const storeRef = useRef<StoreApi<State & Action>>();
if (!storeRef.current) {
storeRef.current = createStore((set) => ({
theme: 'light',
lang: 'zh-CN',
setLang: (lang) => set({ lang }),
setTheme: (theme) => set({ theme })
}));
}
return <StoreContext.Provider value={storeRef.current}>{children}</StoreContext.Provider>;
};
export const useStore = (selector) => {
const store = useContext(StoreContext);
// @ts-ignore
return useExternalStore(store, selector);
};Components can now obtain isolated stores via the context, preventing data from leaking between instances.
Finally, the author invites feedback on these practices.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Rare Earth Juejin Tech Community
Juejin, a tech community that helps developers grow.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
