手把手带你掌握Zustand:轻量级React状态管理利器

手把手带你掌握Zustand:轻量级React状态管理利器

一、简介

一个小巧、快速且可扩展的极简状态管理解决方案。Zustand 提供了一个基于 Hooks 的舒适 API,它不啰嗦、不强加架构约束,但有足够的约定性,使其语义明确,风格类似 Flux。

Zustand(在德语中意为"状态")是一个轻量、快速、可扩展的 React 状态管理库。它基于 Hooks API,并且非常灵活,不需要像 Redux 那样定义复杂的 Action、Reducer、Store 组合等模板代码,却仍然保留了单向数据流、不可变数据等良好的状态管理理念。

1. 特点

  • 基于不可变对象:使用不可变数据有助于调试、性能优化以及实现时间旅行等功能。
  • 单向数据流:保证数据流动可预测,逻辑更清晰。
  • 简洁的 API:只需要通过 create 方法创建 store,通过返回的 Hooks 进行状态读取和更新。
  • 无需 React Context:无需使用 包裹根组件,也能在任意组件中使用 Zustand 的 Store(类组件里也可通过其他方式获取)。
  • 无痛上手:对 Redux 原理熟悉的同学,上手几乎零门槛;初学者也能快速掌握。

2. 核心优势

  • 🚀 极简API:仅需掌握create、setState、getState三个核心方法
  • 🧩 零依赖:单文件体积仅1kB,无需额外配置
  • 高性能:自动依赖追踪,精准更新组件
  • 🧠 直观开发:类Redux单向数据流,但无繁琐模板代码
  • 🌐 灵活扩展:支持中间件生态,轻松对接调试工具

3. 适用场景

  • ✅ SPA应用开发
  • ✅ 中小型项目
  • ✅ 需要快速迭代的场景
  • ✅ 开发者希望简化状态管理

二、Zustand 的使用

我们以一个计数器(Counter)示例入手,演示三件最基础的事情:

  1. 创建一个 store 并定义状态(state)及更新它的函数(action)。
  2. 在组件中读取 store 中的状态
  3. 触发操作时更新状态,从而让 UI 重新渲染。

下面的示例演示了一个简单计数器的用法:

typescript 复制代码
import React from 'react';
import styles from './Counter.module.css';
import { create } from 'zustand';

/**
 * 1. 通过 create 函数创建一个 store
 */
const useCountStore = create((set, get, api) => ({
  value: 0,
  increment: () => set((state) => ({ value: state.value + 1 })),
  decrement: () => set((state) => ({ value: state.value - 1 })),
}));

export function Counter() {
  /**
   * 2. 通过返回的 Hooks + selector 读取想要的状态或操作函数
   */
  const value = useCountStore((state) => state.value);
  const increment = useCountStore((state) => state.increment);
  const decrement = useCountStore((state) => state.decrement);

  return (
    <div>
      <div className={styles.row}>
        <button
          className={styles.button}
          aria-label="Decrement value"
          onClick={decrement}
        >
          -
        </button>
        <span className={styles.value}>{value}</span>
        <button
          className={styles.button}
          aria-label="Increment value"
          onClick={increment}
        >
          +
        </button>
      </div>
    </div>
  );
}

运行后,可以点击加减按钮实时更新计数,使用非常直观。Zustand 用 create 创建的 store,默认会返回一个自定义的 Hook(如上例的 useCountStore),它可以在任何函数组件里直接调用来读写状态。


三、核心实现原理

Zustand 核心实现基于以下几点:

1. 创建 Store

通过 createState 函数初始化状态,并返回一个 api 对象,包含 setState、getState、subscribe、destroy 等方法。整体就是一个简易的「发布-订阅」模型:

  • setState:更新状态并通知所有订阅者。
  • getState:获取当前状态。
  • subscribe:订阅状态更新,当 state 改变时被触发。
  • destroy:销毁或清空所有监听器。

2. 在组件中通过 Hooks 订阅更新

返回的 useBoundStore 函数,内部使用 useSyncExternalStoreWithSelector 来订阅 store 的更新,并根据 selector 计算出具体想要的状态切片。当这个切片发生变化时,会触发组件重新渲染。

3. 选择器(selector)与比较函数(equalityFn)

  • selector:只选择所需的局部数据,有助于减少不必要的组件重渲染。
  • equalityFn:自定义对比逻辑,默认使用 Object.is。如果前后两次 selector 计算出的结果"相等",则避免触发渲染。

四、核心代码示例

下面是一个经过简化的核心实现示例:

typescript 复制代码
import useSyncExternalStoreExports from 'use-sync-external-store/shim/with-selector';
const { useSyncExternalStoreWithSelector } = useSyncExternalStoreExports;

/** 1. 创建 store 并返回 api */
function createStore(createState) {
  let state;
  const listeners = new Set();

  const setState = (partial, replace) => {
    const nextState = typeof partial === 'function' ? partial(state) : partial;
    if (!Object.is(nextState, state)) {
      const previousState = state;
      state =
        replace ?? typeof nextState !== 'object'
        ? nextState
        : Object.assign({}, state, nextState);
      listeners.forEach((listener) => listener(state, previousState));
    }
  };

  const getState = () => state;

  const subscribe = (listener) => {
    listeners.add(listener);
    return () => listeners.delete(listener);
  };

  const destroy = () => listeners.clear();

  const api = { setState, getState, subscribe, destroy };

  // 执行 createState 函数进行初始化
  state = createState(setState, getState, api);
  return api;
}

/** 2. 在 Hooks 中使用 subscribe/getState */
function useStore(api, selector, equalityFn) {
  return useSyncExternalStoreWithSelector(
    api.subscribe,
    api.getState,
    api.getServerState || api.getState,
    selector,
    equalityFn
  );
}

/** 3. create 函数:将 store 和 useStore 结合起来 */
export function create(createState) {
  const api = createStore(createState);
  const useBoundStore = (selector, equalityFn) =>
    useStore(api, selector, equalityFn);
  Object.assign(useBoundStore, api);
  return useBoundStore;
}

从上面可以看出,Zustand 其实就是一个单例式的状态存储 + Hooks 订阅更新的组合。


五、Zustand 中间件

Zustand 提供了各种中间件(middleware),用来增强 Store 的功能。例如配合 immer 可以更方便地写不可变数据;或者使用 devtools 中间件来与 Redux DevTools 进行调试。

例如,使用 immer 中间件的示例:

tsx 复制代码
import React from 'react';
import styles from './Counter.module.css';
import { create } from 'zustand';
import { immer } from 'zustand/middleware/immer';

const useCountStore = create(
  immer((set) => ({
    value: 0,
    increment: () => set((state) => ({ value: state.value + 1 })),
    decrement: () => set((state) => ({ value: state.value - 1 })),

    // 示例:管理一个 todos 列表
    todos: [
      { id: '1', title: 'Learn Zustand', done: false },
      { id: '2', title: 'Learn Jotai', done: false },
      { id: '3', title: 'Learn Valtio', done: false },
    ],
    complete: (todoId) => {
      set((state) => {
        const todo = state.todos.find((t) => t.id === todoId);
        if (todo) todo.done = true; // Immer 会自动帮我们处理不可变数据
      });
    },
  }))
);

export function Counter() {
  const value = useCountStore((state) => state.value);
  const increment = useCountStore((state) => state.increment);
  const decrement = useCountStore((state) => state.decrement);

  const todos = useCountStore((state) => state.todos);
  const complete = useCountStore((state) => state.complete);

  return (
    <div>
      <div className={styles.row}>
        <button
          className={styles.button}
          aria-label="Decrement value"
          onClick={decrement}
          >
          -
        </button>
        <span className={styles.value}>{value}</span>
        <button
          className={styles.button}
          aria-label="Increment value"
          onClick={increment}
          >
          +
        </button>
      </div>
      <div className={styles.row}>
        <ul>
          {todos.map(({ id, title, done }) => (
      <li
        className={`${styles.todoItem} ${done ? styles.done : ''}`}
        key={id}
        >
        {title}
        <button
          className={styles.todoButton}
          onClick={() => complete(id)}
          >
          ✔️
        </button>
      </li>
    ))}
        </ul>
      </div>
    </div>
  );
}

这种写法可以用"可变"的方式直接修改状态,真正保存时则自动帮你处理不可变数据,开发体验更佳。


六、自定义中间件

Zustand 中间件没有像 Redux 那样固定的形式和层级,而是一个「增强器」:本质上就是对传入的 createState 做一层包装,仍然需要返回一个新的 createState 函数。可以随时组合或嵌套多个中间件(洋葱模型)。

下面以一个「日志中间件」(logger) 为例,每次 state 更新时都在控制台打印前后状态信息:

tsx 复制代码
import React from 'react';
import styles from './Counter.module.css';
import { create } from 'zustand';

const logger = (createState) => (set, get, api) =>
  createState((...args) => {
    console.group('setState');
    console.info('before:', get());
    set(...args);
    console.info('after: ', get());
    console.groupEnd();
  }, get, api);

const useCountStore = create(
  logger((set) => ({
    value: 0,
    increment: () => set((state) => ({ value: state.value + 1 })),
    decrement: () => set((state) => ({ value: state.value - 1 })),
  }))
);

export function Counter() {
  const value = useCountStore((state) => state.value);
  const increment = useCountStore((state) => state.increment);
  const decrement = useCountStore((state) => state.decrement);

  return (
    <div>
      <div className={styles.row}>
        <button className={styles.button} onClick={decrement}>
          -
        </button>
        <span className={styles.value}>{value}</span>
        <button className={styles.button} onClick={increment}>
          +
        </button>
      </div>
    </div>
  );
}

想要再叠加更多中间件时,只要多层函数嵌套即可。例如:

tsx 复制代码
import { devtools } from 'zustand/middleware';
import { immer } from 'zustand/middleware/immer';
// ...

const useCountStore = create(
  immer(
    logger(
      devtools((set) => ({
        value: 0,
        increment: () => set((state) => ({ value: state.value + 1 })),
        decrement: () => set((state) => ({ value: state.value - 1 })),
      }))
    )
  )
);

其调用顺序是:immer => logger => devtools,然后执行到真实的 setState。


七、与 Redux/Redux Toolkit 的对比

如果只看最基础的 Redux,你会觉得要写大量模板代码:Action、Reducer、combineReducers...... 通常实际项目会使用 Redux Toolkit 来简化这一切,并内置 immer、reselect 等实用库。

下表对比了 Redux Toolkit 与 Zustand 的主要差异(这里只列举一些核心思路):

对比点 Zustand Redux Toolkit
单向数据流
不可变数据 是 (可手动或使用 immer middleware) 是 (内置 immer)
React Hooks 支持 原生支持,无需 Context 官方提供 react-redux Hooks
是否需要 Provider 否(也可配合 Context,但并非必须) 通常需要
学习曲线与概念 简洁明了,API 少,无需额外处理异步 有较多概念:store, slice, thunk, saga 等。Redux Toolkit 虽然简化了流程,但整体概念依然相对多元。
处理副作用 可直接在 action 函数里写异步逻辑 reducer 必须保持纯函数,一般会配合 redux-thunk 或 redux-saga 等中间件处理异步
特点与社区 轻量、易上手,体积小;灵活性高,不强制统一的 action 派发模式;社区生态也在不断发展中 社区成熟,配套中间件与工具链完备;支持 devtools 与时间旅行调试;大型应用有更丰富完善的第三方生态

简单来说:Zustand 更适合快速开发或中小型项目,或者你想让状态管理更"松耦合"一些;而 Redux Toolkit 则拥有最成熟的生态、Devtools 支持、大规模项目实践经验,也让数据流更加可追踪、可调试。

如果你需要埋点或对动作(Action)逐条记录,Redux 拥有天然的派发机制可以在中间件轻松拦截;而在 Zustand 里,你需要在每个更新函数里手动埋点。


八、总结

  • Zustand 的核心思路:在一个闭包里保存应用状态,通过发布订阅模式和 useSyncExternalStore(带 Selector)管理组件更新。对比传统 Redux,它省去了定义动作与拆分 Reducer 等冗长过程,但仍然保有单向数据流和不可变数据所带来的可维护性和可预测性。
  • 可插拔中间件:Zustand 中间件不拘泥于某种形式,易于自定义。你可以轻松编写 Logger、权限校验或数据持久化等中间件,并与其他官方中间件(如 devtools, immer)堆叠使用。

结语

Zustand 以其"极简却不失灵活"的特性,成为了许多开发者在 React 项目中管理状态的首选。它的用法和原理都相当直观,并且拥有良好的性能表现。对于想快速开发、并希望在未来随时可扩展的项目而言,Zustand 不失为一种优秀的选择。

祝你写码愉快,保持好"Zustand"~(好状态)!

引用
相关推荐
getapi19 分钟前
Flutter 强制横屏
前端·javascript·flutter
MrsBaek35 分钟前
【前端笔记】CSS预处理语言 LESS
前端·css·笔记
gqkmiss38 分钟前
ChromeOS 135 版本更新
前端·chrome·浏览器·chromeos
小陈同学呦1 小时前
聊聊CSS选择器
前端·css·面试
山野春茶1 小时前
js基础回顾/事件委托
前端
LaughingZhu2 小时前
PH热榜 | 2025-04-09
前端·数据库·人工智能·mysql·开源
枫super2 小时前
Day-03 前端 Web-Vue & Axios 基础
前端·javascript·vue.js
程序猿chen3 小时前
Vue.js组件安全工程化演进:从防御体系构建到安全性能融合
前端·vue.js·安全·面试·前端框架·跳槽·安全架构
你也来冲浪吗3 小时前
MD编辑器用法讲解
前端
小小小小宇3 小时前
十万字总结所有React hooks(含简单原理)
前端