Skip to content

深入Zustand系列- 为什么选择Zustand?

不同于vue3的Pinia的几乎一统天下,React状态管理在社区中一直没有一个让人一致推崇的方案,Redux样板代码太多、Context API 有性能问题、redux-saga复杂的流程管控让人头大。

Zustand 的出现改变了这一切。它用极简的 API 和出色的性能,成为了 React 应用状态管理的一个重磅的选手。

什么是 Zustand?

Zustand 是一个基于 Hooks 的 React 状态管理库,其核心思想非常简单:将状态逻辑封装成一个自定义 Hook 里,让状态的读取和更新变得更加的直观。

它有几个突出的特点:

  • API 极简 - 通常只需要一个 create 函数就能搞定整个 store
  • 零样板代码 - 不像 Redux 那样需要定义一堆 Actions、Reducers、Dispatchers
  • 不依赖 Context - 避免了 Context Provider 带来的性能问题
  • 精确渲染 - 组件可以选择性订阅状态的某个部分,只有相关状态变化时才重新渲染
  • 体积小巧 - 压缩后只有 1KB 左右
  • 框架无关 - 核心逻辑不依赖 React,也可以在其他环境使用

为什么选择 Zustand?

Zustand的出现主要是为了解决两个日常开发中非常常见的问题:如何替换掉Redux的 boilerplate 代码,以及如何解决 Context API 的性能问题。

对比Redux

尽管 Redux 是一个强大且成熟的状态管理库,社区也在此基础上诞生了好几种方案,大型应用中表现出色,但它在日常开发中往往存在一些诟病:

  • 过多的样板代码 (Boilerplate): 即使实现一个简单的功能,也需要创建 Actions, Action Creators, Reducers, Constants 等多个部分,代码分散且冗余。

  • 复杂的心智模型: 开发者需要严格遵守单向数据流,并理解 dispatch, action, reducer, store 之间的协作关系,逻辑是分散的。

  • 间接的更新逻辑: 你不能直接修改状态,而是需要 dispatch 一个 action,然后由 reducer 来处理这个 action 并返回一个新的状态。

尽管Redux推出了Redux Toolkit (RTK),通过更加简洁的api一定程度上减少了样板代码,但其配置步骤依旧繁琐。我们通过一个简单的例子来说明这个问题:

javascript
// store/counterSlice.js
import { createSlice } from '@reduxjs/toolkit';

const initialState = {
  value: 0,
};

// createSlice 会自动生成 action creators 和 action types
export const counterSlice = createSlice({
  name: 'counter',  // slice 的名字
  initialState,
  // reducers 字段定义了如何更新状态
  reducers: {
    increment: (state) => {
      // RTK 内部使用 Immer,允许我们“直接修改”状态
      state.value += 1;
    },
    decrement: (state) => {
      state.value -= 1;
    },
    incrementByAmount: (state, action) => {
      state.value += action.payload; // payload 是 action 携带的数据
    },
  },
});

// 导出自动生成的 action creators
export const { increment, decrement, incrementByAmount } = counterSlice.actions;

// 导出 reducer
export default counterSlice.reducer;
javascript
// store/store.js
import { configureStore } from '@reduxjs/toolkit';
import counterReducer from './counterSlice'; // 导入上一步的 reducer

export const store = configureStore({
  reducer: {
    counter: counterReducer, // 将 counter slice 添加到 store
    // 未来可以添加其他 slice,如 user: userReducer
  },
});
javascript
// main.jsx
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
import { store } from './store/store';
import { Provider } from 'react-redux';

ReactDOM.createRoot(document.getElementById('root')).render(
  <React.StrictMode>
    <Provider store={store}> {/* 提供 store */}
      <App />
    </Provider>
  </React.StrictMode>,
);
javascript
// components/Counter.jsx
import React from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { increment, decrement } from '../store/counterSlice'; // 导入 actions

function Counter() {
  // 使用 useSelector 从 store 中读取状态
  const count = useSelector((state) => state.counter.value);
  // 获取 dispatch 函数
  const dispatch = useDispatch();

  return (
    <div>
      <h2>Redux Counter</h2>
      <span>Count: {count}</span>
      <div>
        {/* 点击时,dispatch 一个 action */}
        <button onClick={() => dispatch(increment())}>
          Increment
        </button>
        <button onClick={() => dispatch(decrement())}>
          Decrement
        </button>
      </div>
    </div>
  );
}

看到这一堆代码,真是头疼,

用 Zustand 就不用这么麻烦了:

Zustand 将 createSlice 的 initialState 和 reducers 合二为一,状态和用于更新状态的函数合并在一个地方创建。

javascript
// store/counterStore.js
import { create } from 'zustand';

// create 函数创建了一个 store hook
export const useCounterStore = create((set) => ({
  // 1. 定义状态 (State)
  value: 0,

  // 2. 定义更新状态的函数 (Actions)
  // 这些函数会接收 set 函数作为参数来安全地更新状态
  increment: () => set((state) => ({ value: state.value + 1 })),
  
  decrement: () => set(() => ({ value: state.value - 1 })),

  // 对于需要参数的 action,直接在函数中接收即可
  incrementByAmount: (amount) => set((state) => ({ value: state.value + amount })),
}));

在组件中使用

javascript
// components/Counter.jsx
import React from 'react';
import { useCounterStore } from '../store/counterStore'; // 导入 store hook

function Counter() {
  // 从 store hook 中获取状态和 actions
  // 方法一:一次性获取所有需要的
  const { value, increment, decrement } = useCounterStore();

  // 方法二:使用选择器 (selector) 来获取特定状态,性能更好
  // 当只有 count 变化时,组件才会重新渲染
  const count = useCounterStore((state) => state.value);
  // 如果只需要 actions,可以这样获取以避免不必要的重渲染
  // const increment = useCounterStore((state) => state.increment);
  // const decrement = useCounterStore((state) => state.decrement);

  return (
    <div>
      <h2>Zustand Counter</h2>
      {/* 使用从 store 中获取的状态 */}
      <span>Count: {count}</span>
      <div>
        {/* 直接调用从 store 中获取的 action 函数 */}
        <button onClick={increment}>
          Increment
        </button>
        <button onClick={decrement}>
          Decrement
        </button>
      </div>
    </div>
  );
}

export default Counter;

看出来了吗?Zustand以一个更加符合hooks的理念的方式实现了同样的目标,同时减少了大量样板代码,直接一键引入useCounterStore就行。

Context API 的重渲染问题

Context API 有个让人头疼的问题:当 Provider 的 value 变了,所有用这个 Context 的组件都会重新渲染,不管它们关不关心变化的那部分数据。

比如说,你有个全局 Context 管理用户信息和主题。UserDisplay 只显示用户名,ThemeToggler 负责切换主题。按理说切换主题时,显示用户名的组件不应该重新渲染吧?但现实却啪啪打脸。。。让我们通过具体的应用代码去验证一下。

Context 应用

** 创建 AppContext.js**

javascript
// AppContext.js
import { createContext } from 'react';
export const AppContext = createContext(null);

** 在 App.jsx 中提供状态**

jsx
// App.jsx
import React, { useState, useMemo } from 'react';
import { AppContext } from './AppContext';
import UserDisplay from './UserDisplay';
import ThemeToggler from './ThemeToggler';

function App() {
  const [user, setUser] = useState({ name: 'Bob' });
  const [theme, setTheme] = useState('light');

  // 当 theme 变化时, contextValue 会生成一个新对象, 导致所有消费者重渲染
  const contextValue = { user, theme, setTheme };

  return (
    <AppContext.Provider value={contextValue}>
      <div className={theme}>
        <h1>Context API Example</h1>
        <UserDisplay />
        <ThemeToggler />
      </div>
    </AppContext.Provider>
  );
}

两个消费组件

jsx
// UserDisplay.jsx
import React, { useContext } from 'react';
import { AppContext } from './AppContext';

function UserDisplay() {
  const { user } = useContext(AppContext);
  
  // 我们在这里加一个 log 来观察渲染
  console.log('UserDisplay re-rendered!');
  
  return <p>User: {user.name}</p>;
}

// ThemeToggler.jsx
import React, { useContext } from 'react';
import { AppContext } from './AppContext';

function ThemeToggler() {
  const { theme, setTheme } = useContext(AppContext);

  console.log('ThemeToggler re-rendered!');

  const toggleTheme = () => setTheme(theme === 'light' ? 'dark' : 'light');

  return <button onClick={toggleTheme}>Toggle Theme</button>;
}

点击 "Toggle Theme" 按钮时,控制台会同时打印 "UserDisplay re-rendered!" 和 "ThemeToggler re-rendered!"。

看到问题了吗?UserDisplay 根本不关心主题变化,但因为 AppContextvalue 对象变了,UserDisplay 也会重新渲染。尽管我们可以通过拆分contextReact.memo或者使用 useMemouseCallback的方式来避免这个问题,但是这样做会增加代码的复杂度以及增加业务复杂度。

这个时候zustand可以解决这个问题,因为zustand的状态是全局的,组件可以订阅状态的变化,只有当状态发生变化时,组件才会重新渲染。

Zustand 方案

先创建一个 globalStore.js

javascript
// globalStore.js
import { create } from 'zustand';

const useGlobalStore = create((set) => ({
  user: { name: 'Bob' },
  theme: 'light',
  toggleTheme: () => set((state) => ({ 
    theme: state.theme === 'light' ? 'dark' : 'light' 
  })),
}));

export default useGlobalStore;

然后重构组件,App.jsx 不再需要管理状态或提供 Provider:

jsx
// App.jsx
import React from 'react';
import useGlobalStore from './globalStore';
import UserDisplay from './UserDisplay';
import ThemeToggler from './ThemeToggler';

function App() {
  // 从 store 中获取 theme 来应用样式
  const theme = useGlobalStore((state) => state.theme);
  
  return (
    <div className={theme}>
      <h1>Zustand Example</h1>
      <UserDisplay />
      <ThemeToggler />
    </div>
  );
}

// UserDisplay.jsx (使用选择器订阅 user)
import React from 'react';
import useGlobalStore from './globalStore';

function UserDisplay() {
  // 关键点: 使用选择器 (selector) 只订阅 user 状态
  // Zustand 会比较 user 对象的前后引用,如果没变,则不重渲染
  const user = useGlobalStore((state) => state.user);

  console.log('UserDisplay re-rendered!');

  return <p>User: {user.name}</p>;
}

// ThemeToggler.jsx (订阅 theme 和 toggleTheme)
import React from 'react';
import useGlobalStore from './globalStore';

function ThemeToggler() {
  // 可以一次性解构,也可以分开订阅
  const { theme, toggleTheme } = useGlobalStore((state) => ({
    theme: state.theme,
    toggleTheme: state.toggleTheme,
  }));

  console.log('ThemeToggler re-rendered!');

  return <button onClick={toggleTheme}>Toggle Theme</button>;
}

点击 "Toggle Theme" 按钮时,控制台只会打印 "ThemeToggler re-rendered!"(还有 App 组件,因为它订阅了 theme 来设置样式)。UserDisplay 终于不会无辜躺枪了!

优势很明显:

  • 精确更新 - 组件通过选择器函数(state => state.user)告诉 Zustand 自己只关心哪部分数据。Zustand 会对选择器结果进行浅比较,只有结果真的变了才触发重渲染
  • 性能优化 - 彻底避免了不必要的渲染,在复杂应用中性能提升相当明显

如何使用 Zustand?

说了这么多,我们应该如何使用 Zustand ?

基础用法

用 Zustand 很简单,基本就二步就搞定,让我们做个简单的计数器来演示

创建 Store

新建个 store.js 文件,用 create 函数定义状态和操作方法:

javascript
// src/store.js
import { create } from 'zustand';

// create 函数接收一个回调,参数是 set 函数,用来更新状态
const useCounterStore = create((set) => ({
  // 定义状态
  count: 0,

  // 定义操作方法
  // 每个方法都调用 set 来更新状态
  increase: () => set((state) => ({ count: state.count + 1 })),
  decrease: () => set((state) => ({ count: state.count - 1 })),
  reset: () => set({ count: 0 }),
}));

export default useCounterStore;

在组件中使用

在需要状态的组件里,像用普通 Hook 一样调用就行:

jsx
// src/CounterComponent.jsx
import React from 'react';
import useCounterStore from './store';

function CounterComponent() {
  // 从 store 获取状态和方法
  const { count, increase, decrease } = useCounterStore();

  return (
    <div>
      <h1>Count: {count}</h1>
      <button onClick={increase}>Increase</button>
      <button onClick={decrease}>Decrease</button>
    </div>
  );
}

// 另一个组件,演示性能优化
function DisplayCount() {
  // 用选择器只订阅 count 的变化
  // 如果 store 里有其他状态变了,这个组件不会重渲染
  const count = useCounterStore((state) => state.count);

  console.log('DisplayCount re-rendered');

  return <p>Current count is: {count}</p>;
}

异步操作

在 Zustand 中处理异步操作非常直观,你不需要像 Redux 那样引入 redux-thunkredux-saga 等中间件。可以直接在 action 中使用 async/await

假设我们要从一个 API 获取用户数据并存到 store 中:

javascript
// store/userStore.js
import { create } from 'zustand';

const useUserStore = create((set) => ({
  user: null,
  loading: false,
  error: null,

  // 定义一个异步 action
  fetchUser: async (userId) => {
    set({ loading: true, error: null });

    try {
      const response = await fetch(`https://api.example.com/users/${userId}`);
      if (!response.ok) {
        throw new Error('Failed to fetch user');
      }
      const user = await response.json();
      set({ user, loading: false });
    } catch (error) {
      set({ error: error.message, loading: false });
    }
  },
}));

export default useUserStore;

在组件中使用这个异步 action:

jsx
// components/UserProfile.jsx
import React, { useEffect } from 'react';
import useUserStore from '../store/userStore';

function UserProfile({ userId }) {
  const { user, loading, error, fetchUser } = useUserStore();

  useEffect(() => {
    // 组件加载时调用异步 action
    fetchUser(userId);
  }, [userId, fetchUser]);

  if (loading) return <p>Loading...</p>;
  if (error) return <p>Error: {error}</p>;

  return (
    <div>
      <h1>{user?.name}</h1>
      <p>Email: {user?.email}</p>
    </div>
  );
}

在非 React 环境中使用

Zustand 的核心是框架无关的,这意味着你可以在任何 JavaScript 环境中使用它,比如 Vue 、 Svelte甚至是jQuery 项目。我们可以直接使用 store 实例上的方法:

  • getState(): 获取当前状态。
  • setState(): 更新状态。
  • subscribe(): 订阅状态变化。
javascript
// utils/vanilla-logic.js
import { useCounterStore } from '../store/counterStore';

// 读取状态
const initialCount = useCounterStore.getState().value;
console.log('Initial count:', initialCount); // 输出: Initial count: 0

// 更新状态
useCounterStore.setState({ value: 10 });
console.log('Count after setState:', useCounterStore.getState().value); // 输出: 10

// 也可以使用 action
useCounterStore.getState().increment();
console.log('Count after increment:', useCounterStore.getState().value); // 输出: 11

// 订阅状态变化
const unsubscribe = useCounterStore.subscribe(
  (state) => {
    console.log('State changed!', state.value);
  }
);

// 触发一次更新
useCounterStore.getState().increment(); // 控制台会打印: State changed! 12

// 不再需要时,取消订阅
unsubscribe();

中间件 (Middleware)

Zustand 支持中间件来扩展其功能,常见的有:

  • persist: 将状态持久化到 localStoragesessionStorage
  • devtools: 连接到 Redux DevTools 浏览器扩展,方便调试。
  • immer: 使用 Immer 来更安全地处理嵌套状态的更新。
javascript
import { create } from 'zustand';
import { persist, devtools } from 'zustand/middleware';

const useSettingsStore = create(
  // 使用 devtools 包裹
  devtools(
    // 使用 persist 包裹
    persist(
      (set) => ({
        theme: 'light',
        notifications: true,
        toggleTheme: () => set((state) => ({ theme: state.theme === 'light' ? 'dark' : 'light' })),
        setNotifications: (enabled) => set({ notifications: enabled }),
      }),
      {
        // persist 的配置
        name: 'app-settings', // localStorage 中的 key
        // storage: createJSONStorage(() => sessionStorage), // 可以指定 storage 类型
      }
    ),
    {
      // devtools 的配置
      name: 'AppSettingsStore', // DevTools 中显示的名称
    }
  )
);

瞬时更新 (Transient Updates)

瞬时更新 (Transient Updates) 是一种让你能够订阅(subscribe) store 的状态变化,但在变化发生时不触发任何 React 组件重新渲染的机制。它通过绕过 React 的渲染循环,让你能够将状态直接同步到非 React 的部分,例如 DOM、Canvas、或第三方库。

如果一个状态变化得非常频繁(例如,每秒 60 次),会发生什么?

  • 鼠标移动坐标
  • 拖拽元素的位置
  • 视频/音频的当前播放时间

如果这些高频变化的状态都通过 useStore 连接到组件,就会导致组件以极高的频率重新渲染,从而引发严重的性能问题,导致页面卡顿、掉帧。这个时候瞬时更新就派上用场了。

javascript
// src/store/mouseStore.js
import { create } from 'zustand';

// 创建 store
export const useMouseStore = create((set) => ({
  x: 0,
  y: 0,
}));
// 我们可以直接导出 store 的 `setState` 方法。
export const setMouseState = useMouseStore.setState;
javascript
// src/components/MouseTracker.jsx
import React, { useRef, useEffect } from 'react';
import { useMouseStore, setMouseState } from '../store/mouseStore'; // 引入 store

function MouseTracker() {
  const positionRef = useRef(null);

  // 使用 useEffect 来设置和清理副作用
  useEffect(() => {
    // A. 添加一个全局的鼠标移动事件监听器
    const handleMouseMove = (event) => {
      // 当鼠标移动时,只做一件事:更新 Zustand store 的状态
      setMouseState({ x: event.clientX, y: event.clientY });
    };
    window.addEventListener('mousemove', handleMouseMove);

    // B. 订阅 store 的变化(瞬时更新的核心)
    // store.subscribe() 会返回一个用于取消订阅的函数
    const unsubscribe = useMouseStore.subscribe(
      // C. 订阅的回调函数
      (state) => {
        // 在回调函数中,我们直接操作 DOM!
        // 这一步完全绕过了 React 的渲染流程。
        if (positionRef.current) {
          positionRef.current.innerText = `当前坐标: X=${state.x}, Y=${state.y}`;
        }
      }
    );

    // D. 返回一个清理函数
    // 这个函数会在组件被销毁(卸载)时执行
    return () => {
      // 清理事件监听器
      window.removeEventListener('mousemove', handleMouseMove);
      // 取消 store 的订阅,防止内存泄漏
      unsubscribe();
    };
  }, []); // 空依赖数组 [] 意味着这个 effect 只在组件挂载时运行一次

  return <div ref={positionRef}>请移动鼠标...</div>;
}

export default MouseTracker;

应用组件

JavaScript
import React from 'react';
import MouseTracker from './components/MouseTracker';

function App() {
  return (
    <div style={{ padding: '20px' }}>
      <h1>Zustand 瞬时更新示例</h1>
      <p>下面这个坐标的更新不会触发 React 组件的重新渲染。</p>
      <MouseTracker />
    </div>
  );
}

export default App;

现在运行你的应用,你会看到 div 中的坐标随着鼠标移动而实时变化,但如果你打开 React DevTools,你会发现 组件的渲染次数始终是 1,而这个就是瞬时更新。

Zustand 缺点

在技术领域,没有银弹。Zustand 的简洁和灵活是其最大的优点,但这些优点在某些场景下也会成为其缺点和潜在的弊端。

缺乏规范,易“野蛮生长”

在大型项目或多人协作的团队中,zustand的灵活、自由可能导致混乱,比如在下面的场景中

  1. 场景A: 开发者A 可能喜欢将所有异步逻辑(如 API 请求)直接放在 action 中。
  2. 场景B: 开发者B 可能习惯将异步逻辑抽离到单独的 service 文件中,action 只负责调用。
  3. 场景C: 开发者C 可能在一个 action 中混入多个不相关的状态更新。

如果没有团队内部的统一约定,store 会变得难以预测和维护。相比之下,Redux 严格的 action -> reducer 流程本身就是一种强规范,保证了代码风格的一致性。

单一 Store 非常容易膨胀

Zustand 的 create API 非常简单,这使得开发者倾向于将越来越多的状态和 action 添加到同一个 store 中。尽管 Zustand 核心是单一 store,但可以通过“切片模式 (Slice Pattern)”来模拟模块化,将一个大的 store 拆分成多个逻辑相关的部分。

javascript
// counterSlice.js
export const createCounterSlice = (set, get) => ({
  count: 0,
  increase: () => set((state) => ({ count: state.count + 1 })),
});

// userSlice.js
export const createUserSlice = (set, get) => ({
  user: null,
  login: (userData) => set({ user: userData }),
});

// useBoundStore.js (将所有 slice 组合起来)
import { create } from 'zustand';
import { createCounterSlice } from './counterSlice';
import { createUserSlice } from './userSlice';

const useBoundStore = create((...a) => ({
  ...createCounterSlice(...a),
  ...createUserSlice(...a),
}));

export default useBoundStore;

复杂异步支持度不足

Redux 拥有一个极其庞大和成熟的生态系统,特别是用于处理复杂副作用(side effects)的中间件。这在非常复杂的异步场景中非常有用,比如:

  • 需要取消、节流、防抖的 API 请求。
  • 需要监听一个 action,然后触发另一个或多个 action(如 WebSocket 消息处理)。
  • 长轮询或复杂的业务流程管理。

Zustand 本身没有提供类似的复杂副作用管理方案,一般直接使用 async/await,这对于简单场景足够,但对于上述复杂场景则显得力不从心。

最后

Zustand 并非要完全取代 Redux。Redux 严格的规范、强大的开发者工具以及繁荣的社区在大型、多人协作的复杂项目中依然具有不可替代的价值。

Zustand 的快速发展,正是因为它精准地解决了 Redux 在中小型项目和快速开发场景下的核心痛点:用最少的代码、最直观的方式,完成状态管理的核心任务,同时内置了无需手动优化的性能保障。 对于大多数开发者和项目而言,这是一种更轻快、更现代的选择。