Zustand
在现代 React 生态中,状态管理方案经历了几轮技术更迭。传统的 Redux 虽然强大但由于样板代码冗长而饱受诟病,原生的 Context 又容易陷入性能陷阱。而 Zustand(德语中意为“状态”)凭借其极其轻量(约 1KB)、自适应优化、零样板代码的特性,成为了如今高性能前端项目(尤其是涉及高频数据交互、大屏可视化、WebGL/3D 孪生系统)的明星方案。
接下来,我们从架构设计、性能调优、项目实战以及技术选型四个维度,彻底拆解 Zustand 的硬核玩法。
一、 Zustand 的核心哲学与项目管理(Slice 模式)
Zustand 的底层是一个独立于 React 渲染树之外的纯 JS 订阅中心(Publish-Subscribe Pattern)。这意味着它既能在 React 组件内部通过 Hook 消费,也能在任何纯 TS/JS 异步函数、Web Worker、甚至是自建的事件循环中直接读写。
📂 大型项目中的目录与架构设计
在真实工程中,如果把所有状态都挤在一个 useUwbStore 文件里,后期代码会彻底失控。Zustand 官方推荐使用 Slice(切片)模式,将状态按业务领域拆分,最后合并成一个大 Store。
1. 目录结构
src/
└── store/
├── slices/
│ ├── uiSlice.ts # 负责侧边栏、弹窗、图表显隐等 UI 状态
│ └── assetSlice.ts # 负责车辆、标签、基站等高频核心资产数据
└── index.ts # 统一组装出口
2. 切片编写示例 (src/store/slices/uiSlice.ts)
import { StateCreator } from 'zustand';
export interface UISlice {
showSidebar: boolean;
toggleSidebar: () => void;
}
// 使用 StateCreator 约束切片类型
export const createUISlice: StateCreator<UISlice> = (set) => ({
showSidebar: true,
toggleSidebar: () => set((state) => ({ showSidebar: !state.showSidebar })),
});
3. 终极组装 (src/store/index.ts)
import { create } from 'zustand';
import { createUISlice, UISlice } from './slices/uiSlice';
import { createAssetSlice, AssetSlice } from './slices/assetSlice';
// 联姻合并成全局唯一的联邦 Store
export const useBoundStore = create<UISlice & AssetSlice>()((...a) => ({
...createUISlice(...a),
...createAssetSlice(...a),
}));
在组件中引入时,只需 const showSidebar = useBoundStore(state => state.showSidebar) 即可。
二、 核心炸弹:如何利用 Zustand 调高 React 应用性能?
React 核心的重绘机制是自上而下的虚拟 DOM 比对(Reconciliation)。Zustand 能够大幅提升项目帧率的核心秘密在于它提供了“精准拦截”和“绕过 React”的两套绝杀手段。
1. 原子选择器(Selector)的严格依赖检查
当你写下 const nodes = useBoundStore(state => state.nodes) 时,Zustand 在幕后为该组件注册了一个事件监听器。
只有当 state.nodes 的引用地址发生改变时(默认使用 Object.is 进行浅层比对),才会触发当前组件的 forceUpdate。
⚡ 进阶避坑:避免解构导致的“全量误伤”
// ❌ 错误示范:只要 Store 里任何风吹草动(比如 showSidebar 变了),这个组件都会疯狂重绘 const { nodes, showSidebar } = useBoundStore(); // 正确示范:利用浅层比较器(shallow),合并监听多个状态 import { useShallow } from 'zustand/react/shallow'; const { nodes, selectedId } = useBoundStore(useShallow(state => ({ nodes: state.nodes, selectedId: state.selectedId })));
2. 瞬时更新(Transient Updates)—— 彻底摆脱 React 渲染流水线
在面临高频事件(例如 WebSocket 每秒上百次推送、requestAnimationFrame 帧循环、onMouseMove 拖拽)时,任何 setState 或组件级重绘都会导致 CPU 瞬间拉满,导致丢帧卡顿。
Zustand 允许你只利用它的发布订阅中心,完全不让 React 组件感知到数据变动,而是直接用原生指针去操作 3D 节点或 DOM。
// 🚀 工业级高频优化范式
export function HighFrequencyComponent() {
const meshRef = useRef<THREE.Mesh>(null);
useEffect(() => {
// 🎯 核心:只订阅,不触发组件 return 重绘!
const unsubscribe = useBoundStore.subscribe(
(state) => state.currentPosition, // 监听位置
(currentPosition) => {
// 拿到最新数据,直接在微任务中通过原生引用修改 3D 矩阵或 DOM 属性
if (meshRef.current) {
meshRef.current.position.set(...currentPosition);
}
}
);
return unsubscribe; // 组件卸载时安全销毁监听
}, []);
return <mesh ref={meshRef} />;
}
三、 React Context vs Zustand 深度大对决
| 比较维度 | React Context | Zustand |
|---|---|---|
| 状态存储位置 | React 组件树的 Fiber 节点上(深度绑定) | React 外部的纯 JS 闭包内存空间中(解耦) |
| 重绘触发机制 | 只要 Provider 的 value 引用变了,下层所有消费者强制重绘(除非拆分 Context 或套用一层 React.memo) |
基于 Selector(选择器),只有被勾选的数据发生变化时,消费组件才重绘 |
| 高频并发承载力 | 极差。高频更新(>10Hz)会导致整棵组件树闪烁、卡顿 | 极强。支持瞬时更新(Transient Updates),轻松应对 60FPS 渲染 |
| 数据读写解耦 | 必须在 Provider 内部的组件才能用,无法在普通的 .ts 工具函数中读取 |
极度自由。在任意组件内、组件外、甚至网络请求拦截器里都能通过 store.getState() 读写 |
| 异步行为 (Async) | 需要在组件层自行包装 async/await,再更新状态 |
原生支持。Action 可以是普通的异步函数,直接在里面调 set() |
| 生态与调试 | 依赖 React DevTools,大项目难以追踪具体是哪个 Action 修改了状态 | 原生接入 Redux DevTools,支持时间旅行(Time Travel)与状态快照 |
四、 后续项目中该如何做出理性的技术选型?
在工程落地时,没有最好的技术,只有最适合场景的架构。你可以参考以下黄金决策树:
🎯 坚决选择 React Context 的场景
- 低频配置型依赖分发:例如全站的 国际化语言(i18n)、UI 主题色切换(Theme: dark/light)、用户的登录 Session 会签凭证。这些数据一旦定下来基本不变,且需要借助 React 的层级树结构(例如局部换肤)。
- 构建低依赖的独立 UI 组件库:如果你在开发一套类似 AntD 或 Shadcn 的复合独立组件(例如
Tabs、Select),为了不强迫组件库的用户去安装 Zustand,使用原生 Context 进行父子通信是最佳的纯净选择。
🚀 坚决选择 Zustand 的场景
- 高频数据流与大屏中台:项目中有 WebSocket、MQTT 等长连接源源不断灌入数据,且界面有实时渲染指标(如工业数字孪生、股票大盘、实时轨迹监控)。
- 跨越渲染边界的复杂交互:你的应用中同时存在 DOM 层(各种操作面板、侧边栏、表格)和 WebGL / Canvas 层(Three.js / Pixi.js 场景)。两边需要共享同一套状态并高频联动。
- 中大型全栈 Web 应用:模块错综复杂,存在大量的跨组件长途数据通信(A 组件的操作会间接影响树结构极其遥远的 B、C、D 组件),需要利用 Zustand 的 Slice 模式构建高内聚、低耦合的数据中台。
掌握了 Zustand 的 Slice 切片划分、Selector 浅层比对、以及利用 .subscribe() 绕过 React 的瞬时更新技巧,你基本上就已经拿到了通往 React 高性能进阶架构的钥匙。
后续如果有更复杂的联动需求(比如需要把 Zustand 的状态同步到 URL 路由,或者实现跨标签页同步),我们可以探讨如何编写或接入 Zustand 的 Middleware 中间件。你在目前的数字孪生业务中,有遇到哪些数据是需要跨组件大范围共享,或者更新频率特别夸张的吗?
太赞了!既然你决定全面拥抱 Zustand,我们直接把之前的 Context.Provider、useContext 彻底拿掉。
改用 Zustand 之后,整个数据流会变得极其干净:状态直接归 Store 管理,而你之前在 useEffect 里跑的定时器(或者未来的 WebSocket)可以直接封装成一个干净的“网络初始化 Hook”或直接塞进 Store 的 Action 里。
下面是为您重写后的全套工业级方案。
🛠️ 第一步:用 Zustand 重写全局 Store
我们将业务状态(控制显隐)和网络资产数据(Nodes/Areas/Bases)合并到了同一个规范的全局 Store 中。
新建或修改 src/store/useUwbStore.ts:
import { create } from "zustand"
import type { AreaState, UwbBaseState, UwbNodeState } from "@/types/uwb"
const FAKE_WIDTH = 500
export const positionRegistry = new Map<string, [number, number, number]>()
interface UwbStore {
// 1. 业务显隐控制状态
showNodes: boolean
showAreas: boolean
showBases: boolean
selectedNodeId: string | null
// 2. 核心实时资产数据
nodes: Record<string, UwbNodeState>
areas: Record<string, AreaState>
bases: Record<string, UwbBaseState>
// 3. UI 交互控制 Actions
toggleNodes: () => void
toggleAreas: () => void
toggleBases: () => void
selectNode: (id: string | null) => void
// 4. 数据变动与清洗 Actions
removeNode: (id: string) => void
removeArea: (id: string) => void
setNodes: (updater: (prev: Record<string, UwbNodeState>) => Record<string, UwbNodeState>) => void
initStaticData: (areas: Record<string, AreaState>, bases: Record<string, UwbBaseState>) => void
}
export const useUwbStore = create<UwbStore>((set) => ({
// --- 初始状态 ---
showNodes: true,
showAreas: true,
showBases: true,
selectedNodeId: null,
nodes: {},
areas: {},
bases: {},
// --- 显隐交互 Actions ---
toggleNodes: () => set((state) => ({ showNodes: !state.showNodes })),
toggleAreas: () => set((state) => ({ showAreas: !state.showAreas })),
toggleBases: () => set((state) => ({ showBases: !state.showBases })),
selectNode: (id) => set({ selectedNodeId: id }),
// --- 数据操作 Actions ---
removeNode: (id) => set((state) => {
const next = { ...state.nodes }
delete next[id]
return { nodes: next }
}),
removeArea: (id) => set((state) => {
const next = { ...state.areas }
delete next[id]
return { areas: next }
}),
// 允许高频部分更新 nodes
setNodes: (updater) => set((state) => ({ nodes: updater(state.nodes) })),
// 批量初始化静态区域/基站数据
initStaticData: (areas, bases) => set({ areas, bases })
}))
🔌 第二步:重写网络监听服务 Hook
之前的 UwbSocketProvider 退化成了一个不需要返回任何 JSX 节点的纯逻辑监听器。它的唯一职责就是:默默跟底层数据源连上线,并高频刷入数据给 Zustand。
创建或修改 src/hooks/useUwbSocket.ts(**注意:因为已经没有 JSX 了,后缀可以写回 .ts**):
import { useEffect, useRef } from "react"
import { useUwbStore, positionRegistry } from "@/store/useUwbStore"
import { createPositionWebSocket } from "@/api/websocket"
const FAKE_WIDTH = 500
export const useUwbSocket = () => {
const setNodes = useUwbStore((state) => state.setNodes)
const initStaticData = useUwbStore((state) => state.initStaticData)
const nodeIndexRef = useRef(1)
// 1. 初始化静态区域与基站数据 (等同于以前的另外两个 useEffect)
useEffect(() => {
const mockAreas = {
aaa: {
id: "aaa",
label: "测试",
coordinates: [[97.53, -35.83], [127.39, -35.83], [127.39, -61.75], [97.53, -61.75]],
},
bbb: {
id: "bbb",
label: "模糊定位区域1",
coordinates: [[-700, -500], [700, -500], [700, -700], [-700, -700]],
},
}
const mockBases = {
aaa: { id: "aaa", x: 0, y: 0, z: 0 },
bbb: { id: "bbb", x: 100, y: 0, z: 0 },
}
initStaticData(mockAreas, mockBases)
}, [initStaticData])
// 2. 心跳/高频更新 Mock 定时器
useEffect(() => {
const heartbeatTimer = setInterval(() => {
const nextIndex = nodeIndexRef.current
nodeIndexRef.current += 1
setNodes((prevNodes) => {
const next = { ...prevNodes }
// 高频无缝更新位置注册表 (供 useFrame 消费)
Object.keys(prevNodes).forEach((id) => {
positionRegistry.set(id, [
Math.random() * FAKE_WIDTH - FAKE_WIDTH / 2,
Math.random() * FAKE_WIDTH - FAKE_WIDTH / 2,
0.0,
])
})
// 每秒追加一个虚假的新标签
const id = `UWB_TAG_0${nextIndex}`
next[id] = { id, color: "#ff0000" } as any
return next
})
}, 1000)
return () => clearInterval(heartbeatTimer)
}, [setNodes])
// 3. 预留:未来真正的 WebSocket 接入点
/*
useEffect(() => {
const ws = createPositionWebSocket({
onMessage: (event) => {
// 收到真实数据后通过 Zustand 的 setNodes 快速洗牌
},
onClose: () => console.log("📍 位置 WebSocket 已断开"),
onError: (e) => console.error("📍 位置 WebSocket 错误", e),
})
return () => ws.close()
}, [setNodes])
*/
}
🎯 第三步:可能使用到的地方如何进行适配修改?
由于去掉了 Provider 的限制,现在你可以非常优雅地在任何地方消费它们。
1. 全局生命周期挂载点 (App.tsx)
你只需要把这个网络 Hook 挂载在顶级,它就会像守护进程一样运行:
import React from "react"
import { useUwbSocket } from "./hooks/useUwbSocket"
import { UwbMap } from "./components/UwbMap"
import Sidebar from "./components/Sidebar"
export default function App() {
// ⚡ 启动 UWB 数据中台进程,它将静默在后台更新状态中心,无需关心它的返回值
useUwbSocket()
return (
<div className="relative w-screen h-screen">
<Sidebar />
<UwbMap />
</div>
)
}
2. 3D 画布渲染控制器 (UwbMap.tsx)
这里已经不需要套 Provider 标签了,直接干净地通过 Selector 进行条件挂载:
import React from 'react'
import { useUwbStore } from "@/store/useUwbStore"
import { UwbNodes } from "./UwbNodes"
import { Areas } from "./Areas"
import { UwbBases } from "./UwbBases"
export function UwbMap() {
// 🎯 原子化精准监听:仅当对应的布尔值变化时,UwbMap 才会响应
const showNodes = useUwbStore((state) => state.showNodes)
const showAreas = useUwbStore((state) => state.showAreas)
const showBases = useUwbStore((state) => state.showBases)
return (
<group>
{showNodes && <UwbNodes />}
{showAreas && <Areas />}
{showBases && <UwbBases />}
</group>
)
}
3. UI 列表组件中的“踢出 / 删除”消费 (Sidebar.tsx / List.tsx)
你甚至可以直接拿到 Action 方法,执行数据清洗:
import React from 'react'
import { useUwbStore } from "@/store/useUwbStore"
export function UwbNodeList() {
// 拿数据
const nodes = useUwbStore((state) => state.nodes)
// 拿动作
const removeNode = useUwbStore((state) => state.removeNode)
return (
<div className="space-y-2">
{Object.values(nodes).map((node) => (
<div key={node.id} className="flex justify-between text-white text-xs">
<span>{node.id}</span>
<button
onClick={() => removeNode(node.id)} // 🎯 直接触发 Zustand 的 Action 清除
className="text-red-400 hover:text-red-500"
>
剔出
</button>
</div>
))}
</div>
)
}
4. 2D 控制台显隐切换面板 (VisibleController.tsx)
import React from "react"
import { useUwbStore } from "@/store/useUwbStore"
export const VisibleController: React.FC = () => {
const showBases = useUwbStore((state) => state.showBases)
const toggleBases = useUwbStore((state) => state.toggleBases)
return (
<div
className={`flex cursor-pointer ${showBases ? 'text-cyan-400' : 'text-slate-500'}`}
onClick={toggleBases} // 🎯 优雅地通过规范 Action 改变状态
>
<span>基站显示</span>
</div>
)
}
完美!通过这样改写,不仅消除了原本 Invalid hook call 的 React 报错隐患,还将系统的高频通信网络(数据源)、渲染决定树(R3F 画布)和 UI 控制层(2D 大屏侧边栏)彻底进行了三维解耦。
这绝对是一个好主意,而且在工业级的数字孪生大屏项目中,这属于典型的标准范式(Single Source of Truth / 单一数据源)。
这类统计指标(如车辆总数、在线/离线数、报警数)在业务上被称为 看板核心元数据 (KPI Metrics)。把它们放入 Zustand Store,不仅架构清晰,还能带来巨大的工程和性能优势:
💡 为什么这是个好主意?(核心优势)
- 避免二次计算(Performance Optimization)
如果不存统计好的数字,侧边栏组件为了拿到“报警数”,就必须每次都去遍历几百个甚至几千个nodes对象的集合去过滤计算(比如Object.values(nodes).filter(...))。在大屏高频刷新的情况下,这会白白浪费大量的 CPU 算力。 - 多终端消费(Data Sharing)
这些数据往往不止侧边栏(Sidebar)要用。可能你的顶部标题栏(Header)要显示报警滚动条,底部的全景图表(Charts)要画在线率折线图,3D 场景(Canvas)在收到严重报警时要触发滤镜。塞入 Zustand 后,所有组件都可以原子化地“按需秒提”这些数字。 - 网络层与 UI 层彻底解耦
WebSocket 收到报文后,只需要调用一行setMetrics(...),任务就完成了。它不需要知道界面长什么样,界面也不需要知道 WebSocket 什么时候来数据。
🛠️ 最佳实践:代码如何落地?
为了不破坏已有的结构,我们直接在 useUwbStore 中扩充一个 metrics 状态和对应的 Actions。
1. 升级 Store (src/store/useUwbStore.ts)
// 1. 定义清晰的指标数据结构
interface UwbMetrics {
totalVehicles: number;
onlineTags: number;
offlineTags: number;
activeAlarms: number;
}
interface UwbStore {
// ... 保留之前所有的状态(nodes, showNodes 等)
// 新增:全局统计指标
metrics: UwbMetrics;
// 新增:更新指标的 Actions
setMetrics: (metrics: Partial<UwbMetrics>) => void;
}
export const useUwbStore = create<UwbStore>((set) => ({
// ... 保留已有状态
// 默认初始数据
metrics: {
totalVehicles: 0,
onlineTags: 0,
offlineTags: 0,
activeAlarms: 0,
},
// 支持增量或者全量改写指标数据
setMetrics: (newMetrics) => set((state) => ({
metrics: { ...state.metrics, ...newMetrics }
})),
}));
2. 在网络层接收并注入 (src/hooks/useUwbSocket.ts)
当真实的 WebSocket 收到服务端的统计推送时(或者你在 Mock 阶段的定时器里),直接清洗并塞入:
import { useEffect } from "react"
import { useUwbStore } from "@/store/useUwbStore"
export const useUwbSocket = () => {
const setMetrics = useUwbStore((state) => state.setMetrics)
useEffect(() => {
// 模拟真实的 WebSocket 收到总览统计报文
const handleWsMessage = (data: any) => {
// 真实场景:data = JSON.parse(event.data)
// 假设后端每秒推送一次最新的大屏统计面板数据
setMetrics({
totalVehicles: data.totalVehicles ?? 120,
onlineTags: data.onlineTags ?? 18,
offlineTags: data.offlineTags ?? 2,
activeAlarms: data.activeAlarms ?? 3,
});
};
// --- 以下为 Mock 演示,定时模拟数据动态小幅飘动 ---
const mockTimer = setInterval(() => {
handleWsMessage({
totalVehicles: 150,
onlineTags: 18 + Math.floor(Math.random() * 3), // 模拟在线数偶尔跳动
offlineTags: Math.max(0, 2 - Math.floor(Math.random() * 2)),
activeAlarms: Math.random() > 0.7 ? 4 : 2, // 模拟偶发报警
});
}, 2000);
return () => clearInterval(mockTimer);
}, [setMetrics]);
}
3. 在 Sidebar 中精准、高性能消费
利用 Zustand 的 Selector 机制,侧边栏通过一条精准的指针指向 metrics。即使 3D 场景里的标签在以 60FPS 的超高频移动,只要这个 metrics 的总数值没变,Sidebar 就绝对不会触发毫无意义的重绘。
import React from 'react';
import { useUwbStore } from "@/store/useUwbStore";
import { Car, Radio, AlertTriangle } from "lucide-react";
export function SidebarMetricsSummary() {
// 🎯 核心护城河:精准订阅总览指标
const metrics = useUwbStore((state) => state.metrics);
return (
<div className="grid grid-cols-2 gap-2 p-2 bg-slate-950/40 border border-slate-800/40 rounded-lg">
{/* 车辆总数 */}
<div className="flex items-center justify-between p-2 bg-slate-900/60 rounded border border-slate-800/20">
<div className="flex items-center space-x-1.5 text-slate-400 text-xs">
<Car size={13} className="text-blue-400" />
<span>车辆总数</span>
</div>
<span className="font-mono font-bold text-sm text-blue-400">{metrics.totalVehicles}</span>
</div>
{/* 报警总数 */}
<div className={`flex items-center justify-between p-2 rounded border transition-colors ${
metrics.activeAlarms > 0
? 'bg-rose-950/20 border-rose-500/30'
: 'bg-slate-900/60 border-slate-800/20'
}`}>
<div className="flex items-center space-x-1.5 text-slate-400 text-xs">
<AlertTriangle size={13} className={metrics.activeAlarms > 0 ? "text-rose-500 animate-pulse" : "text-slate-500"} />
<span className={metrics.activeAlarms > 0 ? "text-rose-300 font-medium" : ""}>实时报警</span>
</div>
<span className={`font-mono font-bold text-sm ${metrics.activeAlarms > 0 ? 'text-rose-500' : 'text-slate-400'}`}>
{metrics.activeAlarms}
</span>
</div>
{/* 在线标签 */}
<div className="flex items-center justify-between p-2 bg-slate-900/60 rounded border border-slate-800/20">
<div className="flex items-center space-x-1.5 text-slate-400 text-xs">
<span className="w-1.5 h-1.5 rounded-full bg-emerald-500 inline-block animate-ping" />
<span>标签在线</span>
</div>
<span className="font-mono font-bold text-sm text-emerald-400">{metrics.onlineTags}</span>
</div>
{/* 离线标签 */}
<div className="flex items-center justify-between p-2 bg-slate-900/60 rounded border border-slate-800/20">
<div className="flex items-center space-x-1.5 text-slate-400 text-xs">
<span className="w-1.5 h-1.5 rounded-full bg-slate-600 inline-block" />
<span>标签离线</span>
</div>
<span className="font-mono font-bold text-sm text-slate-400">{metrics.offlineTags}</span>
</div>
</div>
);
}
把这类大屏全局 KPI 塞进全局统一状态库中是非常标准的工程解法。这样改之后,你的业务逻辑界限会极其分明。
浙公网安备 33010602011771号