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.

Rare Earth Juejin Tech Community
Rare Earth Juejin Tech Community
Rare Earth Juejin Tech Community
Zustand Best Practices: Optimization, Persistence, Debugging, and Multi‑Instance Management

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 changes

This 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.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

optimizationTypeScriptState ManagementReactPersistenceZustand
Rare Earth Juejin Tech Community
Written by

Rare Earth Juejin Tech Community

Juejin, a tech community that helps developers grow.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.