简单来说,Zustand 是一个轻量级、高性能且易于上手的 React 状态管理库。

🤔 为什么我们需要 Zustand?

在 Zustand 出现之前,我们通常面临两种选择,但它们都有各自的痛点:

  • Redux: 功能强大,但过于复杂。你需要编写大量的“样板代码”,比如 actionreducerdispatch 等,开发效率较低。
  • React Context + useState: 虽然简单,但在处理复杂或频繁更新的状态时,容易导致不必要的组件重渲染,影响性能。而且,当组件层级很深时,数据传递(Props Drilling)会变得非常麻烦。

Zustand 的出现,就是为了完美解决这些问题,它在简洁和功能之间取得了极佳的平衡。

✨ Zustand 的核心优势

Zustand 之所以能成为当前最受欢迎的状态管理方案之一,主要归功于以下几个特点:

  1. 极致轻量
    它的核心包体积非常小(压缩后仅 1-2KB),并且没有任何外部依赖,几乎不会给你的项目增加负担。

  2. 极简的 API
    它的学习成本极低。你只需要掌握一个核心的 create 函数,就可以创建出一个全局的状态仓库(Store)。

  3. 无需 Provider
    这是它最方便的特点之一。你不需要像使用 Context 那样,在应用顶层用 <Provider> 组件包裹一切。你可以在任何地方直接导入并使用 Store,摆脱了组件树的嵌套限制。

  4. 出色的性能
    Zustand 支持细粒度的状态订阅。这意味着组件可以只订阅它关心的那部分状态。当状态更新时,只有真正使用了该状态的组件才会重新渲染,避免了性能浪费。

  5. 灵活且强大
    它原生支持异步操作,并且通过中间件(Middleware)可以轻松实现状态持久化(如保存到 localStorage)、连接开发工具(DevTools)等高级功能。

  6. 完美的 TypeScript 支持
    它对 TypeScript 非常友好,能够提供出色的类型推导,让你在享受开发便利的同时,也能获得类型安全带来的保障。

💻 核心用法:三步上手

让我们通过一个经典的“计数器”例子,看看 Zustand 用起来有多简单。

第一步:创建 Store

我们通常在 src/store 目录下创建一个状态文件,比如 counterStore.ts

// src/store/counterStore.ts
import { create } from 'zustand'

// 1. 定义状态的类型
interface CounterState {
  count: number
  increment: () => void
  decrement: () => void
}

// 2. 使用 create 函数创建 store
const useCounterStore = create<CounterState>((set) => ({
  // 初始状态
  count: 0,
  // 更新状态的方法
  increment: () => set((state) => ({ count: state.count + 1 })),
  decrement: () => set((state) => ({ count: state.count - 1 })),
}))

export default useCounterStore
第二步:在组件中使用
// Counter.tsx
import useCounterStore from './store/counterStore'

function Counter() {
  // 从 store 中获取状态和方法
  const count = useCounterStore((state) => state.count)
  const increment = useCounterStore((state) => state.increment)
  const decrement = useCounterStore((state) => state.decrement)

  return (
    <div>
      <h1>当前计数: {count}</h1>
      <button onClick={increment}>+1</button>
      <button onClick={decrement}>-1</button>
    </div>
  )
}

在任何组件中,你都可以直接导入并使用这个 store,它本身就是一个 Hook。


一种非常典型的“跨组件通信”模式,通常被称为“发布-订阅”“事件总线”模式

// 事件派发

<Button
  className="nodrag"
  type="text"
  icon={<EditOutlined style={{ fontSize: 24 }} />}
  onClick={() =>
    window.dispatchEvent(
      new CustomEvent("OPEN_CONFIG_MODAL", {
        detail: { id, data },
      }),
    )
  }
/>

// 事件监听

useEffect(() => {
  const handleOpenModal = (e: any) => {
    setActiveNode(e.detail);
    setModalVisible(true);
  };
  // 对于不需要保留状态、不需要 UI 响应;事件总线是最简洁的。
  window.addEventListener("OPEN_CONFIG_MODAL", handleOpenModal);
  return () => {
    window.removeEventListener("OPEN_CONFIG_MODAL", handleOpenModal);
  };
}, []);

下面我为你详细拆解 window.dispatchEvent 和 new CustomEvent 的含义及用法:

1. new CustomEvent(...):制造一个“自定义信号”

浏览器原生自带很多事件,比如 click(点击)、scroll(滚动)、load(加载)。但在复杂的业务中,这些不够用,我们需要定义自己的业务事件,比如这里的 'OPEN_CONFIG_MODAL'

CustomEvent 就是用来创建这种自定义事件对象的构造函数。

  • 第一个参数 'OPEN_CONFIG_MODAL'
    这是事件的名字(类型)。就像给快递贴个标签,告诉接收方这是什么类型的包裹。
  • 第二个参数 { detail: { id, data } }
    这是事件的配置项
    • detail 是 CustomEvent 的核心属性,专门用来携带数据
    • 在这里,我们将 { id, data } 放入 detail 中。这意味着,当其他组件接收到这个事件时,可以通过 event.detail 拿到这两个变量。
2. window.dispatchEvent(...):在全局“广播”信号

dispatchEvent 是一个标准的方法,用于触发某个元素上的事件。

  • 为什么要加 window.
    window 代表浏览器的全局窗口对象。当你调用 window.dispatchEvent 时,意味着你是在全局范围内触发这个事件。
  • 效果
    这就像拿着大喇叭在广场上喊话。无论你的代码在哪个组件里(哪怕是嵌套很深的子组件),只要在这个页面里,任何监听了 'OPEN_CONFIG_MODAL' 的地方都能收到通知。

3、dispatchEvent 依然存在“更新机制”上的性能劣势:

A. 无法利用 React 的“批处理” (Batching)

  • Jotai/State 模式:当你修改状态时(例如 setCount(c => c + 1)),React 知道这是一个状态更新。如果你在同一个事件循环中修改了 10 次状态,React 会自动把它们合并(批处理)成一次渲染。
  • Event Bus 模式dispatchEvent 触发的是回调函数。如果你在回调里执行 setState,这往往发生在 React 的合成事件系统之外
    • 如果你连续触发 10 次 dispatchEvent,可能会导致 10 次独立的 setState 调用,进而触发 10 次组件重渲染。这会造成严重的性能浪费(即使你只监听了这一个事件)。

B. 丢失“并发模式”的优先级调度

  • Jotai/State 模式:React 18 引入了并发模式。React 知道哪些更新是紧急的(如点击),哪些是不紧急的(如后台数据刷新)。React 可以暂停、中断或重排这些更新。
  • Event Bus 模式dispatchEvent 是浏览器原生的同步机制(稍后会详细解释)。React 无法感知这个事件的优先级。一旦事件触发,回调必须立即执行,这可能会阻塞主线程,导致高优先级的 UI 更新(如输入框打字)被卡住。

在某些场景下“弃用” Jotai/Zustand 而选择“原始”的 dispatchEvent,通常是因为以下 3 个不得不妥协的现实原因

1. “依赖地狱”与“包体积”的博弈

场景: 我正在写一个基础组件库(比如 Button、Icon),或者一个微前端的子应用

  • 如果用 Jotai/Zustand:
    这就意味着我的基础组件库强依赖了 Jotai/Zustand。
    • 如果你(使用者)的项目里没用这两个库,你就得为了用我一个按钮,额外安装它们的依赖。这叫“依赖污染”
    • 如果主应用用了 Jotai,子应用用了 Zustand,你的项目里就会同时存在两套状态管理库,包体积直接翻倍。
  • 如果用 dispatchEvent
    这是浏览器原生 API,零依赖,零体积。无论你把组件放到 React、Vue 甚至原生 jQuery 项目里,它都能跑。
    • 结论: 为了极致的通用性解耦,我宁愿牺牲一点类型安全和性能。
 2. 不需要保留状态不需要 UI 响应的“即抛型”操作(Fire-and-Forget)。
3. 跨越“框架边界”的通信
你的需求 推荐方案 理由
核心业务数据
(用户信息、购物车、表单值)
Jotai / Zustand 需要类型安全、响应式更新、高性能渲染。这是它们的主场。
全局 UI 状态
(侧边栏展开、暗黑模式)
Zustand 集中管理,方便调试,避免 Props Drilling。
基础组件/工具库
(Button, Toast, Notification)
dispatchEvent 避免强依赖,保持库的纯净和轻量。
非数据交互
(播放声音、埋点、第三方SDK通知)
dispatchEvent 既然是“发完不管”的事,就别占用状态库的资源了。
跨框架/微前端
(React 调 Vue)
dispatchEvent 唯一能打通“任督二脉”的方案。

最佳实践是: 用 Jotai 管理数据,用 dispatchEvent 处理突发的、非数据驱动的交互动作

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐