Frontend Development 13 min read

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
((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 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
= { [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.

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

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