在React项目开发中,状态管理是贯穿始终的核心需求------从简单的组件内状态,到复杂的跨组件、跨页面全局状态,选择一款合适的状态管理工具,能极大提升开发效率、降低维护成本。Redux作为经典方案,功能强大但配置繁琐、概念繁多;Context+useReducer虽原生无依赖,却容易陷入"Provider嵌套地狱"和性能瓶颈。
而今天要分享的Zustand,正是一款专为React设计的轻量级状态管理库,以"简洁、小巧、灵活"的特点脱颖而出,成为中小型项目乃至大型项目中局部状态管理的首选。本文将结合真实项目实战,从基础用法到高级技巧,全面解析Zustand的使用方式,帮你快速上手并落地到实际开发中。
一、Zustand 核心优势:为什么选择它?
在正式讲解使用方法前,我们先明确Zustand的核心优势,理解它相比其他状态管理工具的差异化价值:
- 极致轻量:体积仅约1KB(gzip后),无任何依赖,引入项目后几乎不增加包体积负担,适合对性能要求较高的项目。
- 无需Provider包裹:不同于Redux、Context API,Zustand无需在项目入口用Provider嵌套整个应用,直接创建Store即可在任意组件中使用,简化了项目结构。
- API简洁易懂:核心API仅一个create方法,状态和方法的定义直观,上手成本极低,新手也能快速掌握。
- 灵活可扩展:支持中间件(如持久化、日志、防抖等),可根据项目需求灵活扩展功能;同时支持TypeScript完全类型推导,类型安全有保障。
- 性能优异:内置状态订阅机制,组件仅订阅自己需要的状态字段,避免不必要的重渲染,性能优于传统的Context+useReducer方案。
简单来说,Zustand完美平衡了"易用性"和"功能性",既解决了React原生状态管理的痛点,又避免了重型状态管理库的繁琐配置,是当前React生态中极具性价比的状态管理选择。
二、项目结构设计:模块化Store划分
在实际项目中,状态管理的核心是"模块化"------将不同业务场景的状态拆分到不同的Store中,避免单一Store过于庞大,提升代码的可维护性和可复用性。以下是我们项目中通用的Store目录结构:
bash
src/stores/
├── authStore.ts # 认证相关状态(登录、登出、用户信息)
├── chatStore.ts # 聊天功能状态(会话、消息、流式输出)
└── themeStore.ts # 主题相关状态(暗黑模式、主题色、布局)
这种划分方式遵循"单一职责原则":
- authStore:管理用户登录状态、token、用户信息等,供登录页、个人中心、全局权限控制等组件使用。
- chatStore:管理聊天会话列表、当前会话、消息列表、流式输出状态等,仅服务于聊天相关组件。
- themeStore:管理全局主题配置,供布局组件、设置页面等使用。
提示:Store的划分没有固定标准,核心是"按业务模块拆分",避免一个Store包含所有状态,导致后续维护困难。一般建议一个业务模块对应一个Store,若多个模块存在关联状态,可考虑跨Store通信(下文会详细讲解)。
三、Store 核心结构:接口定义与创建
Zustand的Store创建分为两步:先定义状态接口(TypeScript),再通过create方法创建Store并实现状态和方法。其中,接口定义是关键,它能保证状态的类型安全,避免开发中的类型错误。
3.1 状态接口定义(以chatStore为例)
在TypeScript项目中,我们首先需要定义Store的状态结构,包括"数据状态""UI状态"和"操作方法(Actions)",让整个Store的结构清晰可见。
TypeScript
// stores/chatStore.ts
// 先定义子类型(根据项目实际需求扩展)
interface Session {
id: string;
title: string;
lastTime: string | null;
unreadCount?: number; // 未读消息数(扩展字段)
}
interface Message {
id: string;
role: "user" | "assistant" | "system"; // 角色区分
content: string;
createdAt: string;
feedback?: "positive" | "negative" | null; // 反馈状态
}
type FeedbackValue = "positive" | "negative";
// 定义核心状态接口 - 包含所有状态和方法的类型
interface ChatState {
// ========== 数据状态(核心业务数据) ==========
sessions: Session[]; // 会话列表
currentSessionId: string | null; // 当前选中的会话ID
messages: Message[]; // 当前会话的消息列表
// ========== UI 状态(界面交互相关) ==========
isLoading: boolean; // 加载中状态(如获取会话、加载消息)
isStreaming: boolean; // 是否正在流式输出(AI回复)
deepThinkingEnabled: boolean; // 深度思考模式开关
thinkingStartAt: number | null; // 深度思考开始时间(用于计时)
inputFocusKey: number; // 输入框焦点key(用于重置输入框)
// ========== 流式相关状态(专属业务场景) ==========
streamTaskId: string | null; // 流式任务ID(用于取消、追踪)
streamAbort: (() => void) | null; // 流式请求取消方法
streamingMessageId: string | null; // 当前流式输出的消息ID
cancelRequested: boolean; // 是否请求取消生成
// ========== Actions(操作方法,修改状态的唯一途径)==========
fetchSessions: () => Promise<void>; // 获取会话列表
createSession: () => Promise<string>; // 创建新会话(返回会话ID)
deleteSession: (sessionId: string) => Promise<void>; // 删除会话
renameSession: (sessionId: string, title: string) => Promise<void>; // 重命名会话
selectSession: (sessionId: string) => Promise<void>; // 选择会话
sendMessage: (content: string) => Promise<void>; // 发送消息
cancelGeneration: () => void; // 取消流式生成
submitFeedback: (messageId: string, feedback: FeedbackValue) => Promise<void>; // 提交消息反馈
appendStreamContent: (content: string) => void; // 追加流式消息内容
appendThinkingContent: (content: string) => void; // 追加深度思考内容
}
接口定义的核心要点:
- 区分"数据状态"和"UI状态",让状态职责更清晰,便于维护。
- Actions方法需明确参数和返回值类型,尤其是异步方法(返回Promise)。
- 子类型(如Session、Message)单独定义,提高代码复用性和可读性。
3.2 Store 创建:两种核心写法
定义好接口后,通过Zustand的create方法创建Store,核心是传入一个回调函数,该函数接收set和get两个参数:
set:用于修改状态,支持两种语法(下文详细讲解)。get:用于获取当前的最新状态,常用于在Actions中依赖其他状态。
3.2.1 标准创建方式(推荐)
TypeScript
// stores/chatStore.ts
import { create } from "zustand";
import { listSessions, createSession as createSessionApi } from "@/services/sessionService";
import { listMessages, sendMessageApi } from "@/services/chatService";
import { toast } from "@/components/ui/Toast"; // 项目中的提示组件
// 导入上文定义的接口
import type { ChatState, Session, Message } from "./types";
// 创建Store,泛型指定状态接口,确保类型安全
export const useChatStore = create<ChatState>((set, get) => ({
// ========== 1. 初始状态(与接口对应,初始化所有字段) ==========
sessions: [],
currentSessionId: null,
messages: [],
isLoading: false,
isStreaming: false,
deepThinkingEnabled: false,
thinkingStartAt: null,
inputFocusKey: 0, // 初始值为0,重置时自增
streamTaskId: null,
streamAbort: null,
streamingMessageId: null,
cancelRequested: false,
// ========== 2. Actions 实现(核心逻辑) ==========
// 获取会话列表(异步Action)
fetchSessions: async () => {
// 方式1:对象语法(用于设置固定值,不依赖当前状态)
set({ isLoading: true });
try {
// 调用API获取会话数据
const data = await listSessions();
// 格式化数据(适配前端状态结构)
const formattedSessions: Session[] = data.map((item) => ({
id: item.conversationId,
title: item.title || "新对话",
lastTime: item.lastTime,
unreadCount: item.unreadCount || 0
}));
// 更新状态
set({ sessions: formattedSessions });
} catch (error) {
// 错误处理
toast.error((error as Error).message || "加载会话失败");
} finally {
// 方式2:函数语法(推荐,用于依赖当前状态)
set((state) => ({
isLoading: false,
// 可依赖当前状态做进一步处理,此处示例为固定值
inputFocusKey: state.inputFocusKey // 保持原有值
}));
}
},
// 创建新会话(异步Action)
createSession: async () => {
try {
const data = await createSessionApi();
const newSession: Session = {
id: data.conversationId,
title: "新对话",
lastTime: new Date().toISOString(),
unreadCount: 0
};
// 依赖当前会话列表,添加新会话到头部
set((state) => ({
sessions: [newSession, ...state.sessions],
currentSessionId: newSession.id,
messages: [] // 新会话默认无消息
}));
return newSession.id; // 返回新会话ID,供组件使用
} catch (error) {
toast.error((error as Error).message || "创建会话失败");
throw error; // 抛出错误,让组件处理
}
},
// 其他Actions方法(省略实现,参考上述逻辑)
deleteSession: async (sessionId: string) => { /* ... */ },
selectSession: async (sessionId: string) => { /* ... */ },
sendMessage: async (content: string) => { /* ... */ },
cancelGeneration: () => { /* ... */ },
submitFeedback: async (messageId: string, feedback: FeedbackValue) => { /* ... */ },
appendStreamContent: (content: string) => { /* ... */ },
appendThinkingContent: (content: string) => { /* ... */ }
}));
3.2.2 set() 方法的两种语法(关键细节)
set() 是Zustand修改状态的核心方法,支持两种语法,需根据场景选择:
TypeScript
// 写法1:对象语法 - 用于设置固定值,不依赖当前状态
// 适用场景:直接赋值,无需参考当前状态
set({ isLoading: true, isStreaming: false });
set({ currentSessionId: "session-123" });
// 写法2:函数语法 - 用于依赖当前状态,推荐优先使用
// 适用场景:需要基于当前状态计算新值(如计数、过滤、拼接)
set((state) => ({
count: state.count + 1, // 依赖当前count值
sessions: state.sessions.filter(s => s.id !== sessionId), // 过滤会话
messages: [...state.messages, newMessage] // 拼接消息
}));
// ⚠️ 重要注意事项:不要在函数语法中直接修改state对象
// 错误写法:直接修改state,会导致状态异常,且不触发组件重渲染
set((state) => {
state.count = state.count + 1; // ❌ 错误:直接修改原状态
return { count: state.count };
});
// 正确写法:返回新的状态对象,不修改原状态
set((state) => ({
count: state.count + 1 // ✅ 正确:返回新值
}));
为什么推荐函数语法?因为React状态是不可变的,函数语法能确保我们基于"最新的状态"计算新值,避免因异步操作导致的状态不一致问题。尤其是在异步Actions中,函数语法能有效规避竞态条件。
四、Actions 实战:异步处理与状态联动
Actions是Store的核心,负责处理业务逻辑、调用API、修改状态。实际项目中,Actions以异步为主(如调用后端API),同时需要处理状态联动(如切换会话时取消流式输出)。以下是几个高频场景的Actions实现示例。
4.1 异步Action:获取会话列表(带加载状态)
异步Action的核心逻辑:开始请求时设置加载状态,请求成功更新数据,请求失败提示错误,请求结束(无论成功失败)重置加载状态。
TypeScript
fetchSessions: async () => {
set({ isLoading: true }); // 开始加载
try {
const data = await listSessions(); // 调用后端API
// 格式化数据,适配前端状态结构
const sessions: Session[] = data.map((item) => ({
id: item.conversationId,
title: item.title || "新对话",
lastTime: item.lastTime,
unreadCount: item.unreadCount || 0
}));
// 按最后更新时间排序(最新的会话在前面)
sessions.sort((a, b) => {
const timeA = a.lastTime ? new Date(a.lastTime).getTime() : 0;
const timeB = b.lastTime ? new Date(b.lastTime).getTime() : 0;
return timeB - timeA;
});
set({ sessions }); // 更新会话列表
} catch (error) {
toast.error((error as Error).message || "加载会话失败");
} finally {
set({ isLoading: false }); // 结束加载,无论成功失败
}
}
4.2 状态联动:选择会话(依赖当前状态)
选择会话时,需要处理多个状态联动:取消当前流式输出、重置消息列表、更新当前会话ID,同时避免重复加载(当前会话已选中且有消息时,不重复请求)。
TypeScript
selectSession: async (sessionId: string) => {
if (!sessionId) return; // 边界处理:会话ID为空时不执行
const state = get(); // 获取当前最新状态
// 避免重复加载:当前已选中该会话且有消息,直接返回
if (state.currentSessionId === sessionId && state.messages.length > 0) {
return;
}
// 状态联动:如果正在流式输出,先取消
if (state.isStreaming) {
get().cancelGeneration(); // 调用当前Store的其他方法
}
// 设置加载状态和当前会话ID
set({
isLoading: true,
currentSessionId: sessionId,
messages: [] // 重置消息列表(避免残留上一个会话的消息)
});
try {
// 调用API获取该会话的消息列表
const data = await listMessages(sessionId);
// 异步竞态处理:防止用户快速切换会话,导致消息列表错乱
if (get().currentSessionId !== sessionId) {
return; // 此时已切换到其他会话,不更新消息
}
set({ messages: data }); // 更新当前会话的消息列表
} catch (error) {
toast.error((error as Error).message || "加载消息失败");
// 错误回滚:重置当前会话ID
set({ currentSessionId: null });
} finally {
set({ isLoading: false });
}
}
4.3 辅助函数:简化状态更新逻辑
对于复杂的状态更新(如"存在则更新,不存在则添加"),可以抽取辅助函数,提高代码复用性和可读性。
TypeScript
// 辅助函数:更新会话列表(存在则更新,不存在则添加)
function upsertSession(sessions: Session[], next: Session): Session[] {
const index = sessions.findIndex((session) => session.id === next.id);
const updated = [...sessions]; // 浅拷贝,避免修改原数组
if (index >= 0) {
// 存在:合并当前会话和新会话的属性(新属性覆盖旧属性)
updated[index] = { ...sessions[index], ...next };
} else {
// 不存在:添加到列表头部
updated.unshift(next);
}
// 重新排序(按最后更新时间降序)
return updated.sort((a, b) => {
const timeA = a.lastTime ? new Date(a.lastTime).getTime() : 0;
const timeB = b.lastTime ? new Date(b.lastTime).getTime() : 0;
return timeB - timeA;
});
}
// 在Actions中使用辅助函数
renameSession: async (sessionId: string, title: string) => {
try {
// 调用API修改会话标题
await renameSessionApi(sessionId, title);
// 使用辅助函数更新会话列表
set((state) => ({
sessions: upsertSession(state.sessions, {
id: sessionId,
title,
lastTime: new Date().toISOString(),
unreadCount: state.sessions.find(s => s.id === sessionId)?.unreadCount || 0
})
}));
toast.success("会话重命名成功");
} catch (error) {
toast.error((error as Error).message || "重命名会话失败");
}
}
五、组件中使用Store:高效订阅与状态访问
Zustand的组件使用方式非常简洁,通过创建Store时返回的Hook(如useChatStore),即可在任意组件中访问状态和调用方法。核心要点是"按需订阅",避免不必要的重渲染。
5.1 基本使用方式
直接通过Hook解构获取需要的状态和方法,适用于需要多个状态/方法的场景。
TypeScript
// components/chat/ChatInput.tsx
import { useChatStore } from "@/stores/chatStore";
import { useState } from "react";
export function ChatInput() {
const [value, setValue] = useState("");
// 解构获取需要的状态和方法(按需获取,避免订阅整个Store)
const {
sendMessage,
isStreaming,
cancelGeneration,
deepThinkingEnabled,
setDeepThinkingEnabled // 假设存在该方法,用于切换深度思考开关
} = useChatStore();
// 处理发送消息
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!value.trim() || isStreaming) {
return;
}
await sendMessage(value.trim());
setValue(""); // 发送成功后清空输入框
};
// 处理取消流式输出
const handleCancel = () => {
if (isStreaming) {
cancelGeneration();
}
};
return (
<form onSubmit={">
<input
type="text"
value={ setValue(e.target.value)}
placeholder="请输入消息..."
disabled={isStreaming}
/>
<button
type="button"
onClick={ setDeepThinkingEnabled(!deepThinkingEnabled)}
className={deepThinkingEnabled ? "active" : ""}
>
深度思考
<button
type="submit"
disabled={ >
{isStreaming ? "发送中..." : "发送"}
{isStreaming && (
<button type="button" onClick={
取消
)}
);
}
5.2 选择器(Selector):优化性能,避免重渲染
Zustand的Hook支持传入选择器函数,仅订阅选择器返回的状态字段,当且仅当这些字段变化时,组件才会重渲染。这是优化组件性能的关键。
TypeScript
// 推荐:使用选择器,只订阅需要的状态字段
import { useChatStore } from "@/stores/chatStore";
import { shallow } from "zustand/shallow"; // 用于浅比较
// 方式1:单个字段订阅(最简洁)
const isStreaming = useChatStore((state) => state.isStreaming);
const messages = useChatStore((state) => state.messages);
// 方式2:多个字段订阅(返回对象)
// 注意:默认是深比较,若返回对象,建议使用shallow浅比较,避免不必要的重渲染
const { messages, isLoading } = useChatStore(
(state) => ({
messages: state.messages,
isLoading: state.isLoading
}),
shallow // 浅比较:只要messages和isLoading的引用不变,就不重渲染
);
// ❌ 不推荐:订阅整个Store(任何状态变化,组件都会重渲染)
const store = useChatStore(); // 不推荐,性能较差
const { messages, isStreaming } = store;
补充说明:shallow比较的作用是"比较对象的顶层属性引用",适用于返回多个状态字段的场景。如果不使用shallow,Zustand会进行深比较,虽然更精准,但性能开销略高;如果返回的是基本类型(如boolean、string、number),则无需使用shallow。
5.3 自定义Hook封装:简化组件使用
对于频繁使用的Store,可以封装一个自定义Hook,进一步简化组件中的导入和使用。
TypeScript
// hooks/useChat.ts
import { useChatStore } from "@/stores/chatStore";
import { shallow } from "zustand/shallow";
// 封装ChatStore的Hook,统一导出需要的状态和方法
export function useChat() {
// 可在Hook内部预设常用的选择器,组件直接使用
const chatState = useChatStore(
(state) => ({
sessions: state.sessions,
currentSessionId: state.currentSessionId,
messages: state.messages,
isLoading: state.isLoading,
isStreaming: state.isStreaming,
// 方法
fetchSessions: state.fetchSessions,
createSession: state.createSession,
selectSession: state.selectSession,
sendMessage: state.sendMessage,
cancelGeneration: state.cancelGeneration
}),
shallow
);
return chatState;
}
// 组件中使用(简化导入和使用)
// import { useChat } from "@/hooks/useChat";
// function ChatComponent() {
// const { messages, sendMessage, isStreaming } = useChat();
// // ...
// }
自定义Hook的优势:统一管理Store的订阅字段,后续若需要调整订阅的状态,只需修改Hook内部,无需逐个修改组件,提升代码可维护性。
六、高级技巧:跨Store通信与持久化
在实际项目中,不可避免会遇到"一个Store需要访问另一个Store"的场景(如登出时重置聊天状态),同时需要实现状态持久化(如刷新页面后保留用户登录状态)。以下是这两个场景的实战实现。
6.1 跨Store通信:访问其他Store的状态和方法
Zustand支持在一个Store中访问另一个Store,核心是使用Store.getState()方法(注意:不是组件中的useStore Hook,而是Store本身的getState方法)。
TypeScript
// stores/authStore.ts(认证Store)
import { create } from "zustand";
import { useChatStore } from "./chatStore";
import { storage } from "@/utils/storage";
import { logoutRequest } from "@/services/authService";
interface AuthState {
user: User | null;
token: string | null;
isAuthenticated: boolean;
login: (username: string, password: string) => Promise<void>;
logout: () => Promise<void>;
}
export const useAuthStore = create<AuthState>((set, get) => ({
user: storage.getUser(), // 从本地存储初始化
token: storage.getToken(),
isAuthenticated: Boolean(storage.getToken()),
login: async (username, password) => { /* ... 登录逻辑 ... */ },
logout: async () => {
try {
await logoutRequest(); // 调用登出API
} catch (error) {
// 忽略网络错误,确保登出流程继续执行
console.warn("登出API调用失败,仍执行本地登出逻辑", error);
}
// 跨Store通信:访问ChatStore的状态和方法
const chatStore = useChatStore.getState(); // 获取ChatStore的当前状态
chatStore.cancelGeneration(); // 调用ChatStore的方法(取消流式输出)
// 跨Store通信:重置ChatStore的状态
useChatStore.setState({
sessions: [],
currentSessionId: null,
messages: [],
isLoading: false,
isStreaming: false,
streamTaskId: null,
streamAbort: null,
streamingMessageId: null,
cancelRequested: false
});
// 清除本地存储的认证信息
storage.clearAuth();
// 重置当前AuthStore的状态
set({ user: null, token: null, isAuthenticated: false });
}
}));
注意事项:
- 使用
Store.getState()获取其他Store的状态,该方法返回的是Store的当前快照,不会触发组件重渲染(仅用于Store内部逻辑)。 - 避免在Store之间形成循环依赖(如AStore导入BStore,BStore又导入AStore),可通过抽取公共逻辑到工具类解决。
6.2 状态持久化:本地存储保存状态
Zustand本身不提供持久化功能,但可以通过手动操作localStorage/sessionStorage实现,适用于需要保留状态的场景(如用户登录状态、主题设置)。
6.2.1 手动持久化实现(以authStore为例)
TypeScript
// utils/storage.ts(本地存储工具类)
export const storage = {
// 认证相关key
TOKEN_KEY: "auth_token",
USER_KEY: "auth_user",
// Token相关
getToken: () => localStorage.getItem(storage.TOKEN_KEY),
setToken: (token: string) => localStorage.setItem(storage.TOKEN_KEY, token),
clearToken: () => localStorage.removeItem(storage.TOKEN_KEY),
// 用户信息相关
getUser: (): User | null => {
const userStr = localStorage.getItem(storage.USER_KEY);
return userStr ? JSON.parse(userStr) : null;
},
setUser: (user: User) => localStorage.setItem(storage.USER_KEY, JSON.stringify(user)),
clearUser: () => localStorage.removeItem(storage.USER_KEY),
// 清除所有认证相关存储
clearAuth: () => {
storage.clearToken();
storage.clearUser();
}
};
// stores/authStore.ts(使用storage工具类实现持久化)
import { create } from "zustand";
import { storage } from "@/utils/storage";
export const useAuthStore = create<AuthState>((set, get) => ({
// 初始化状态:从本地存储读取
user: storage.getUser(),
token: storage.getToken(),
isAuthenticated: Boolean(storage.getToken()),
login: async (username, password) => {
// 调用登录API,获取用户信息和token
const { user, token } = await loginRequest(username, password);
// 保存到本地存储
storage.setToken(token);
storage.setUser(user);
// 更新Store状态
set({ user, token, isAuthenticated: true });
},
logout: async () => {
// 清除本地存储
storage.clearAuth();
// 重置Store状态
set({ user: null, token: null, isAuthenticated: false });
// 跨Store重置聊天状态(上文已实现)
}
}));
6.2.2 进阶:使用中间件实现自动持久化
如果需要更灵活的持久化(如部分状态持久化、过期时间设置),可以使用Zustand的中间件,如zustand-persist,无需手动操作localStorage,简化持久化逻辑。
TypeScript
// 安装依赖
// npm install zustand-persist
// stores/themeStore.ts(使用中间件实现自动持久化)
import { create } from "zustand";
import { persist } from "zustand-persist";
interface ThemeState {
darkMode: boolean;
themeColor: string;
toggleDarkMode: () => void;
setThemeColor: (color: string) => void;
}
// 使用persist中间件,自动持久化到localStorage
export const useThemeStore = create(
persist<ThemeState>(
(set, get) => ({
darkMode: false, // 初始状态
themeColor: "#165DFF", // 默认主题色
toggleDarkMode: () => set((state) => ({ darkMode: !state.darkMode })),
setThemeColor: (color) => set({ themeColor: color })
}),
{
name: "theme-storage", // 本地存储的key
storage: localStorage, // 存储方式(localStorage/sessionStorage)
// 可选:指定需要持久化的字段(默认所有字段)
partialize: (state) => ({ darkMode: state.darkMode, themeColor: state.themeColor })
}
)
);
优势:中间件会自动监听状态变化,同步到本地存储;页面刷新时,自动从本地存储读取状态初始化Store,无需手动编写读写逻辑。
七、ChatStore 完整实战:流式消息发送流程
聊天功能是Zustand实战的典型场景,涉及异步请求、流式输出、状态联动等多个知识点。以下是sendMessage方法的完整实现,还原真实项目中的流式聊天逻辑。
TypeScript
// stores/chatStore.ts 中的 sendMessage 方法
sendMessage: async (content: string) => {
const state = get(); // 获取当前状态
// 1. 边界处理:内容为空或正在流式输出,不执行
if (!content.trim() || state.isStreaming) {
return;
}
// 2. 创建用户消息(临时ID,后续会被后端返回的ID替换)
const userMessage: Message = {
id: `temp-${Date.now()}`,
role: "user",
content: content.trim(),
createdAt: new Date().toISOString(),
feedback: null
};
// 3. 更新状态:添加用户消息,开启流式输出状态
set((s) => ({
messages: [...s.messages, userMessage],
isStreaming: true,
thinkingStartAt: Date.now(), // 记录深度思考开始时间
cancelRequested: false
}));
try {
// 4. 创建AbortController,用于取消流式请求
const controller = new AbortController();
const { signal } = controller;
// 更新streamAbort方法,供取消生成时使用
set({
streamAbort: () => controller.abort(),
streamTaskId: null,
streamingMessageId: null
});
// 5. 调用流式API(后端返回SSE流式响应)
const response = await fetch(`${API_BASE_URL}/rag/v3/chat`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: storage.getToken() || "" // 从本地存储获取token
},
body: JSON.stringify({
question: content.trim(),
conversationId: state.currentSessionId,
thinking: state.deepThinkingEnabled // 深度思考模式开关
}),
signal // 关联AbortController,用于取消请求
});
// 6. 检查响应状态
if (!response.ok || !response.body) {
throw new Error("流式请求失败,请重试");
}
// 7. 处理流式响应(解析SSE格式)
const reader = response.body.getReader();
const decoder = new TextDecoder();
let aiMessage: Message | null = null; // AI消息(逐步拼接)
while (true) {
const { done, value } = await reader.read();
if (done) break; // 流式结束
// 解析后端返回的流式数据(根据实际API格式调整)
const chunks = decoder.decode(value).split("\n").filter(Boolean);
for (const chunk of chunks) {
const data = JSON.parse(chunk.replace("data: ", ""));
// 处理不同类型的流式数据
switch (data.type) {
case "meta":
// 元数据:包含消息ID、任务ID
aiMessage = {
id: data.messageId,
role: "assistant",
content: "",
createdAt: new Date().toISOString(),
feedback: null
};
set({
streamTaskId: data.taskId,
streamingMessageId: data.messageId
});
break;
case "thinking":
// 深度思考内容(AI思考过程)
get().appendThinkingContent(data.delta);
break;
case "message":
// AI回复内容(流式拼接)
if (!aiMessage) break;
aiMessage.content += data.delta;
// 更新消息列表(替换临时消息,或追加内容)
set((s) => ({
messages: s.messages.map((msg) =>
msg.id === aiMessage!.id ? aiMessage! : msg
)
}));
break;
case "error":
throw new Error(data.message);
}
}
}
// 8. 流式结束,更新状态
set((s) => ({
isStreaming: false,
thinkingStartAt: null,
streamAbort: null
}));
// 9. 更新会话的最后更新时间
if (state.currentSessionId) {
set((s) => ({
sessions: upsertSession(s.sessions, {
id: state.currentSessionId,
title: aiMessage?.content.slice(0, 20) || s.sessions.find(s => s.id === state.currentSessionId)?.title || "新对话",
lastTime: new Date().toISOString(),
unreadCount: 0
})
}));
}
} catch (error) {
// 10. 错误处理:提示错误,重置状态
toast.error((error as Error).message || "发送消息失败");
set({
isStreaming: false,
streamAbort: null,
thinkingStartAt: null
});
}
}
核心逻辑梳理:
- 创建用户消息并立即更新状态,提升用户体验(无需等待API响应)。
- 使用AbortController实现流式请求的取消功能,关联到streamAbort方法。
- 逐段解析流式响应,拼接AI消息内容,实时更新状态,实现"打字机"效果。
- 流式结束后,更新会话的最后更新时间和标题,保持状态一致性。
八、Zustand 使用最佳实践与避坑指南
结合项目实战经验,总结以下最佳实践和避坑点,帮助你更高效、规范地使用Zustand。
8.1 最佳实践
- 使用TypeScript:必须定义状态接口,确保类型安全,避免开发中的类型错误,同时提升代码可读性和可维护性。
- 模块化Store:按业务模块拆分Store,避免单一Store过于庞大,建议一个业务模块对应一个Store。
- 优先使用函数语法set():当状态更新依赖当前状态时,必须使用函数语法,避免异步操作导致的状态不一致。
- 按需订阅状态:使用选择器(Selector)只订阅组件需要的状态字段,配合shallow浅比较,优化组件性能。
- Actions封装业务逻辑:将所有业务逻辑、API调用、状态更新都放在Actions中,组件只负责调用Actions和渲染UI,实现"业务逻辑与UI分离"。
- 跨Store通信用getState() :在Store内部访问其他Store时,使用
Store.getState(),避免组件中使用多个Hook导致的重渲染问题。
8.2 避坑指南
- 不要直接修改state:Zustand的状态是不可变的,直接修改state对象不会触发组件重渲染,且会导致状态异常,必须通过set()方法修改。
- 不要订阅整个Store:订阅整个Store会导致组件在任何状态变化时都重渲染,严重影响性能,务必使用选择器按需订阅。
- 避免Store循环依赖:AStore导入BStore,BStore又导入AStore,会导致Store初始化失败,可通过抽取公共逻辑到工具类解决。
- 异步Action注意竞态条件:异步请求(如API调用)可能存在竞态条件(如快速切换会话),需在请求成功后检查当前状态是否符合预期,避免状态错乱。
- 不要在render中调用Actions:组件render时调用Actions会导致无限循环,Actions应在事件回调(如onClick、onSubmit)或useEffect中调用。
九、总结
Zustand作为一款轻量级、高性能的React状态管理库,以其简洁的API、灵活的扩展能力和优异的性能,成为当前React项目中极具竞争力的选择。它既解决了React原生状态管理的痛点,又避免了重型状态管理库的繁琐配置,非常适合中小型项目,同时也能通过中间件和模块化设计,适配大型项目的复杂需求。
本文从项目实战出发,详细讲解了Zustand的核心用法、Store设计、组件使用、跨Store通信、状态持久化等知识点,结合聊天功能的完整示例,还原了真实项目中的使用场景。希望通过本文,你能快速上手Zustand,并将其灵活运用到实际项目中,提升开发效率和代码质量。
最后,Zustand还有很多高级特性(如中间件、批量更新、服务器端渲染支持等),感兴趣的同学可以查看官方文档进一步学习,解锁更多实用技巧。