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
((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 (
{theme}
setTheme(theme === 'light' ? 'dark' : 'light')}>切换
);
};
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
= { [P in K]: T[P] };
type Many
= T | readonly T[];
export function useSelector
(paths: Many
) {
const prev = useRef
>({} as Pick
);
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 (
{theme}
setTheme(theme === 'light' ? 'dark' : 'light')}>切换
);
};
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
((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
>({} as StoreApi
);
export const StoreProvider = ({ children }) => {
const storeRef = useRef
>();
if (!storeRef.current) {
storeRef.current = createStore((set) => ({
theme: 'light',
lang: 'zh-CN',
setLang: (lang) => set({ lang }),
setTheme: (theme) => set({ theme })
}));
}
return
{children}
;
};
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.
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.