Zustand 最佳实践指南
在本指南中,我们将探讨如何使用 Zustand 来管理 React 应用的状态,同时遵循最佳实践以确保代码的可维护性和性能。
1. 设计原则
1.1 单一 Store + 多 Slice
使用单一的 store 来管理整个应用的状态,但将状态分割成多个 slice。
1.2 No-Store-Actions
将动作逻辑从 store 中分离出来,放到独立的 actions 文件中。
1.3 使用 selector 按需订阅
在 react 组件函数顶部 scope 里面,一律采用 selector 进行按需订阅。
1.4 使用 Immer 实现可变性的更新写法
在创建 store 时,通过中间件注入 Immer,以便于在 action 函数中实现可变性的更新写法。
1.5 需要考虑结合 Redux DevTools 的可调试性
调用 Zustand 的 setState 时,需要传递 action name,以便在 Redux DevTools 中清晰地追踪状态变化,而不是显示为 "anonymous"。
1.6 Type First
在编写代码之前,先定义好类型,确保类型安全。
1.7 代码组织规范
- 将状态(model)、动作(action)和复用型 hook(selector hook)分别放在不同的文件夹中,形成清晰的文件结构。
- 不同类型的文件创建一个
index.ts文件作为该类型文件的统一导出出口文件
2. 目录结构
scss
src/stores/
├── index.ts // 只导出 useStore 和 AppStore
├── createAppStore.ts // 唯一 create 调用,immer 等中间件注入
├── models/ // 初始状态形状和类型定义
│ ├── index.ts // 统一导出所有 model 类型
│ ├── user.model.ts
│ ├── order.model.ts
│ └── cart.model.ts
├── slices/ // 只放"数据 + 纯 set"
│ ├── index.ts // 统一导出所有 slice 类型和创建函数
│ ├── user.ts
│ ├── order.ts
│ └── cart.ts
├── actions/ // 所有业务动作
│ ├── index.ts // 统一导出所有 actions
│ ├── user.ts
│ ├── order.ts
│ └── cart.ts
├── selectors/ // 复用型 selector hooks
│ └── index.ts // 统一导出所有 selector hooks
└── utils/ // 辅助工具
└── actionName.ts // Action 名称生成工具
📁 关键说明:
-
stores/index.ts- 只导出核心 Store- 导出
useStore和AppStore类型 - 不再重新导出其他内容
- 导出
-
各子目录的
index.ts- 各类型文件的统一出口models/index.ts- 导出所有 model 类型和初始状态slices/index.ts- 导出所有 slice 类型和创建函数actions/index.ts- 导出所有 action 函数selectors/index.ts- 导出所有复用型 selector hooks
-
组件导入规则
useStore从@/stores导入- actions 从
@/stores/actions导入 - selectors 从
@/stores/selectors导入 - types 从
@/stores/models导入
-
selectors/- 复用型 selector hooks- 封装常用的状态访问逻辑
- 内部使用
useShallow或useMemo优化性能 - 为复杂的派生状态提供独立的 hook
3. 编码步骤
3.1 定义模型
实现一个特定的业务领域的状态管理,推荐首先从定义该业务领域的数据模型开始:
typescript
// models/user.model.ts
export interface User {
id: string;
name: string;
}
export const userInitial = {
profile: null as User | null,
token: "" as string,
};
3.2 赋予每个 setState 一个 action name
为了让 Redux DevTools 能够清晰地显示每个 action 的名称(而不是 "anonymous"),我们需要创建一个辅助工具来自动生成 action 名称。
设计原则:
- ✅ 使用
createActionName辅助函数自动生成 action 名称 - ✅ Action 名称遵循
{domain}/{action}/{status}格式 - ✅ 在 slice 的 setter 方法中支持可选的
actionName参数 - ✅ 在 actions 中调用 setter 时传递具体的 action 名称
创建辅助工具:
typescript
// utils/actionName.ts
/**
* Action 名称辅助工具
*
* 用于自动生成 Redux DevTools 中显示的 action 名称
*
* 使用示例:
* ```typescript
* const actionName = createActionName("user", login);
* actionName("loading") // "user/login/loading"
* actionName("success") // "user/login/success"
* actionName() // "user/login"
* ```
*/
/**
* 创建 action 名称生成器
*
* @param domain - 领域名称(如 "user", "order", "cart")
* @param fn - action 函数,用于获取函数名
* @returns 返回一个函数,可以传入后缀生成完整的 action 名称
*/
export function createActionName(domain: string, fn: Function) {
return (suffix?: string) => {
const base = `${domain}/${fn.name}`;
return suffix ? `${base}/${suffix}` : base;
};
}
为什么需要这个工具?
- ✅ 自动推断 :利用函数
name属性,无需手动输入 action 名称 - ✅ 统一格式 :遵循
{domain}/{action}/{status}命名规范 - ✅ 调试友好:在 Redux DevTools 中清晰显示,不再是 "anonymous"
- ✅ 易于维护:修改函数名自动更新 action 名称
在 Actions 中使用:
typescript
// actions/user.ts
import { useStore } from "../createAppStore";
import { api } from "../../api/mock";
import { createActionName } from "../utils/actionName";
/**
* 用户登录
*/
export const login = async (email: string, pwd: string) => {
const { _setUser } = useStore.getState();
const actionName = createActionName("user", login); // ✅ 创建 action 名称生成器
// 开始加载
_setUser((draft) => {
draft.isLoading = true;
draft.error = null;
}, actionName("loading")); // "user/login/loading"
try {
const { token, profile } = await api.login(email, pwd);
// 登录成功
_setUser((draft) => {
draft.token = token;
draft.profile = profile;
draft.isLoading = false;
draft.error = null;
}, actionName("success")); // "user/login/success"
} catch (error) {
// 登录失败
_setUser((draft) => {
draft.isLoading = false;
draft.error = error instanceof Error ? error.message : "登录失败";
}, actionName("error")); // "user/login/error"
throw error;
}
};
/**
* 用户登出
*/
export const logout = () => {
const { _setUser } = useStore.getState();
const actionName = createActionName("user", logout);
_setUser((draft) => {
draft.token = "";
draft.profile = null;
}, actionName()); // "user/logout"
};
Action 命名规范:
| 场景 | Action Name 示例 | 说明 |
|---|---|---|
| 异步操作 - 开始 | user/login/loading |
表示正在加载 |
| 异步操作 - 成功 | user/login/success |
表示操作成功 |
| 异步操作 - 失败 | user/login/error |
表示操作失败 |
| 同步操作 | user/logout |
简单的同步操作 |
| 状态更新 | order/setOrderFilter |
设置筛选条件 |
| 跨 slice 操作 | user/logout/clearCart |
登出时清空购物车 |
在 Redux DevTools 中的效果:
使用 createActionName 后,Redux DevTools 会清晰显示:
- ✅
user/login/loading→user/login/success→ 登录流程清晰可见 - ✅
cart/addToCart/success→ 购物车操作一目了然 - ❌ 不再是
anonymous→ 难以追踪问题
3.3 编写 Slice(仅数据 + 纯 set)
✅ Good Case: Slice 只包含数据和纯 set 方法(使用 immer)
typescript
// slices/user.ts
import type { StateCreator } from "zustand";
import type { WritableDraft } from "immer";
import { userInitial, type UserState } from "../models/user.model";
export interface UserSlice {
user: UserState;
_setUser: (
updater: (draft: WritableDraft<UserState>) => void,
actionName?: string // ✅ 添加可选的 action 名称参数
) => void;
}
export const createUserSlice: StateCreator<
UserSlice,
[["zustand/immer", never], ["zustand/devtools", never]], // ✅ 添加 devtools 类型
[],
UserSlice
> = (set) => ({
user: userInitial,
/**
* 纯 setter 方法(由 immer 支持)
*
* @param updater - 状态更新函数
* @param actionName - 可选的 action 名称,用于 Redux DevTools
*
* 使用示例:
* _setUser((draft) => {
* draft.token = "new_token"; // ✅ 直接修改
* draft.profile = { ... }; // ✅ 直接赋值
* }, "user/login"); // ✅ 传递 action 名称(可选)
*/
_setUser: (updater, actionName) =>
set(
(state) => {
updater(state.user);
// immer 会自动处理不可变更新
},
false, // ✅ replace 参数
actionName || "user/_setUser" // ✅ 传递 action 名称给 devtools
),
});
❌ Bad Case: 在 Slice 中混入副作用代码(业务逻辑)
typescript
// ❌ 不推荐:slice 中包含异步逻辑和业务规则
export const createUserSlice: StateCreator<UserSlice> = (set, get) => ({
user: userInitial,
// ❌ 问题 1: 业务逻辑耦合在 slice 中,难以复用和测试
login: async (email: string, pwd: string) => {
const { token, profile } = await api.login({ email, pwd });
set({ user: { token, profile } });
},
// ❌ 问题 2: 跨 slice 访问逻辑散落各处
loginAndLoadOrders: async (email: string, pwd: string) => {
const { token, profile } = await api.login({ email, pwd });
set({ user: { token, profile } });
// 访问其他 slice 的方法
get().loadOrders(profile.id);
},
});
为什么不好?
- 业务逻辑与状态定义耦合,违反单一职责原则
- 难以测试异步逻辑
- 跨 slice 调用会产生复杂的依赖关系
- 无法在组件外(如路由守卫)复用这些逻辑
3.4 单一 Store 组合所有 Slice 并注入 Immer
✅ Good Case: 单一 Store + 中间件统一注入
typescript
// createAppStore.ts
import { create } from "zustand";
import { devtools, subscribeWithSelector } from "zustand/middleware";
import { immer } from "zustand/middleware/immer";
import { createUserSlice, type UserSlice } from "./slices/user";
import { createOrderSlice, type OrderSlice } from "./slices/order";
export type AppStore = UserSlice & OrderSlice;
export const useStore = create<AppStore>()(
devtools(
subscribeWithSelector(
immer((...a) => ({
...createUserSlice(...a),
...createOrderSlice(...a),
}))
),
{ name: "app" }
)
);
❌ Bad Case 1: 每个 Slice 单独注入 Immer
typescript
// ❌ 不推荐:在每个 slice 内部单独使用 immer
import { immer } from "zustand/middleware/immer";
export const createUserSlice: StateCreator<UserSlice> = immer((set) => ({
user: userInitial,
setUser: (data) =>
set((state) => {
state.user = data; // immer 语法
}),
}));
// 问题:
// 1. 重复注入 middleware,性能浪费
// 2. 组合多个 slice 时类型推导复杂
// 3. 无法统一控制 middleware 配置
❌ Bad Case 2: 多个独立的 Store
typescript
// ❌ 不推荐:为每个领域创建独立 store
export const useUserStore = create<UserSlice>()(/* ... */);
export const useOrderStore = create<OrderSlice>()(/* ... */);
export const useCartStore = create<CartSlice>()(/* ... */);
// 组件中使用
function MyComponent() {
const user = useUserStore((s) => s.user);
const orders = useOrderStore((s) => s.orders);
const cart = useCartStore((s) => s.cart);
// ❌ 问题:跨 store 的状态协调很困难
useEffect(() => {
if (user.profile) {
// 如何让 orderStore 知道 user 已登录?
// 只能通过订阅或手动调用,容易出错
}
}, [user.profile]);
}
为什么推荐单一 Store?
- 中间件(devtools/persist)只需配置一次
- 跨领域的业务逻辑编排更简单
- 类型推导更准确,开发体验更好
- DevTools 可以看到完整的状态树
何时可以考虑多 Store?
- 完全独立的子应用(微前端)
- 需要动态加载/卸载的模块
- 跨窗口/Worker 通信的场景
3.5 动作外置(No-Store-Actions)
✅ Good Case: 将业务逻辑抽离到独立的 actions 文件
typescript
// actions/user.ts
import { useStore } from "../createAppStore";
import { api } from "../../api/mock";
import { createActionName } from "../utils/actionName";
/**
* 用户登录
*
* 最佳实践:
* 1. 使用 useStore.getState() 获取状态和 setter
* 2. 包含完整的错误处理和 loading 状态管理
* 3. 使用 createActionName 生成 action 名称(见 3.1)
*/
export const login = async (email: string, pwd: string) => {
const { _setUser } = useStore.getState();
const actionName = createActionName("user", login);
_setUser((draft) => {
draft.isLoading = true;
draft.error = null;
}, actionName("loading"));
try {
const { token, profile } = await api.login(email, pwd);
_setUser((draft) => {
draft.token = token;
draft.profile = profile;
draft.isLoading = false;
}, actionName("success"));
} catch (error) {
_setUser((draft) => {
draft.isLoading = false;
draft.error = error instanceof Error ? error.message : "登录失败";
}, actionName("error"));
throw error;
}
};
/**
* 跨 slice 编排:登出时清空所有用户相关数据
*/
export const logout = () => {
const { _setUser, _setOrder, _setCart } = useStore.getState();
const actionName = createActionName("user", logout);
_setUser((draft) => {
draft.token = "";
draft.profile = null;
}, actionName());
// 跨 slice 协作:清空订单和购物车
_setOrder((draft) => {
draft.list = [];
}, actionName("clearOrders"));
_setCart((draft) => {
draft.items = [];
}, actionName("clearCart"));
};
typescript
// actions/index.ts - 统一导出所有 actions
export * from "./user";
export * from "./order";
export * from "./cart";
为什么要动作外置?
- ✅ 关注点分离:Store 只负责状态定义,Actions 负责业务逻辑
- ✅ 易于测试:Actions 是纯函数,可以独立测试
- ✅ 便于复用:Actions 可以在组件外调用(如路由守卫、中间件)
- ✅ 跨 slice 编排:轻松协调多个 slice 的状态更新
- ✅ 代码组织:按业务功能划分文件,而不是混在一起
❌ Bad Case: 在 Store 中定义 Actions
typescript
// ❌ 不推荐:actions 定义在 store 内部
export const useStore = create<AppStore>()((set, get) => ({
user: userInitial,
// ❌ 问题 1: store 文件臃肿,难以维护
login: async (email: string, pwd: string) => {
const { token, profile } = await api.login({ email, pwd });
set((state) => ({
user: { ...state.user, token, profile },
}));
},
// ❌ 问题 2: 无法在组件外使用(如 Router Guard)
logout: () => {
set({ user: userInitial });
},
// ❌ 问题 3: 测试时必须 mock 整个 store
checkAuth: () => {
return !!get().user.token;
},
}));
为什么不好?
- Store 文件会随着业务增长变得非常庞大
- Actions 无法在非组件环境中调用(需要通过
useStore.getState()绕过) - 测试困难,必须创建完整的 store 实例
- 类型定义复杂,actions 和 state 混在一起
3.6 组件使用:精确订阅 + 调用 Actions
✅ Good Case: 从各自的出口文件导入,使用 selector 精确订阅 + 调用独立 actions
typescript
// ✅ 推荐:从各自的出口文件导入
import { useStore } from "@/stores";
import { login } from "@/stores/actions";
function LoginBtn() {
// ✅ 只订阅需要的状态
const profile = useStore((s) => s.user.profile);
return (
<button onClick={() => login("ok@mail.com", "123456")}>
{profile ? profile.name : "登录"}
</button>
);
}
❌ Bad Case 0: 从具体文件导入,而非出口文件
typescript
// ❌ 不推荐:绕过出口文件,直接从具体文件导入
import { useStore } from "@/stores/createAppStore";
import { login } from "@/stores/actions/user";
import { useUserProfile } from "@/stores/selectors/user";
function LoginBtn() {
const profile = useStore((s) => s.user.profile);
return (
<button onClick={() => login("ok@mail.com", "123456")}>
{profile ? profile.name : "登录"}
</button>
);
}
// 问题:
// 1. 导入路径不一致,难以维护
// 2. 破坏了出口文件的设计原则
// 3. 如果目录结构调整,需要修改多处导入
❌ Bad Case 1: 组件内直接操作 State
typescript
import { useStore } from "@/stores";
function LoginBtn() {
const profile = useStore((s) => s.user.profile);
const handleLogin = async () => {
// ❌ 问题:业务逻辑散落在组件中,无法复用
const { token, profile } = await api.login("ok@mail.com", "123456");
useStore.getState()._setUser((draft) => {
draft.token = token;
draft.profile = profile;
});
};
return <button onClick={handleLogin}>{profile?.name ?? "登录"}</button>;
}
❌ Bad Case 2: 订阅整个 Store 或过多字段
typescript
import { useStore } from "@/stores";
function LoginBtn() {
// ❌ 问题 1: 订阅了整个 store,任何状态变化都会导致重渲染
const store = useStore();
// ❌ 问题 2: 订阅了不需要的字段
const { user, order, cart } = useStore((s) => ({
user: s.user,
order: s.order,
cart: s.cart,
}));
return (
<button onClick={() => login("ok@mail.com", "123456")}>
{store.user.profile?.name ?? "登录"}
</button>
);
}
❌ Bad Case 3: 在 Render 中调用 getState()
typescript
function LoginBtn() {
// ❌ 严重问题:不会响应状态变化!
const profile = useStore.getState().user.profile;
return (
<button onClick={() => login("ok@mail.com", "123456")}>
{profile?.name ?? "登录"}
</button>
);
}
为什么不好?
- Bad Case 0: 导入路径不一致,破坏出口文件设计原则
- Bad Case 1: 业务逻辑重复,难以测试和维护
- Bad Case 2: 不必要的重渲染,影响性能
- Bad Case 3: 组件不会随状态更新而更新,出现 UI 不同步
3.7 复用型 Selector Hooks
✅ Good Case: 将常用的状态访问逻辑封装成独立的 selector hook
typescript
// selectors/index.ts
import { useStore } from "../createAppStore";
import { useShallow } from "zustand/react/shallow";
import { useMemo } from "react";
import type { OrderStatus } from "../models";
/**
* 获取用户信息的复用 hook
*/
export function useUserProfile() {
return useStore(
useShallow((s) => ({
profile: s.user.profile,
isLoggedIn: s.user.isLoggedIn,
}))
);
}
/**
* 获取购物车摘要信息的复用 hook
*/
export function useCartSummary() {
const items = useStore((s) => s.cart.items);
return useMemo(() => {
const itemCount = items.reduce((sum, item) => sum + item.quantity, 0);
const totalPrice = items.reduce(
(sum, item) => sum + item.price * item.quantity,
0
);
return { itemCount, totalPrice };
}, [items]);
}
/**
* 获取订单列表的复用 hook(支持筛选)
*/
export function useOrderList(status: OrderStatus | "all" = "all") {
const list = useStore((s) => s.order.list);
return useMemo(() => {
if (status === "all") {
return list;
}
return list.filter((o) => o.status === status);
}, [list, status]);
}
组件中使用:
typescript
// ✅ 从 selectors 出口文件导入复用型 selector hooks
import { useUserProfile, useCartSummary } from "@/stores/selectors";
function UserDashboard() {
const { profile, isLoggedIn } = useUserProfile();
const { itemCount, totalPrice } = useCartSummary();
if (!isLoggedIn) {
return <div>请先登录</div>;
}
return (
<div>
<h1>欢迎,{profile?.name}</h1>
<p>
购物车:{itemCount} 件商品,总价 ¥{totalPrice}
</p>
</div>
);
}
❌ Bad Case: 在每个组件中重复编写相同的 selector 逻辑
typescript
// ❌ 不推荐:在多个组件中重复相同的逻辑
function ComponentA() {
const items = useStore((s) => s.cart.items);
const itemCount = items.reduce((sum, item) => sum + item.quantity, 0);
const totalPrice = items.reduce(
(sum, item) => sum + item.price * item.quantity,
0
);
return <div>总价:¥{totalPrice}</div>;
}
function ComponentB() {
// ❌ 重复的逻辑
const items = useStore((s) => s.cart.items);
const itemCount = items.reduce((sum, item) => sum + item.quantity, 0);
const totalPrice = items.reduce(
(sum, item) => sum + item.price * item.quantity,
0
);
return <div>商品数:{itemCount}</div>;
}
// 问题:
// 1. 代码重复,难以维护
// 2. 如果计算逻辑需要修改,要改多处
// 3. 没有性能优化(useMemo)
何时应该创建复用型 Selector Hook?
- 多个组件需要相同的状态访问逻辑 - 如
useUserProfile、useCartSummary - 需要对状态进行复杂计算或派生 - 如计算总价、筛选列表
- 需要组合多个状态片段 - 如同时获取用户信息和登录状态
- 需要性能优化 - 使用
useShallow或useMemo避免不必要的重渲染
最佳实践:
- 将 selector hooks 统一放在
selectors/目录 - 在
selectors/index.ts中统一导出 - 在 hook 内部使用
useShallow或useMemo优化性能 - 为 selector hook 添加清晰的注释和类型定义
4. 性能与陷阱
4.1 引用类型订阅必须使用 useShallow
✅ Good Case: 使用 useShallow 避免不必要的重渲染
typescript
import { useShallow } from "zustand/react/shallow";
function UserCard() {
// ✅ 使用 shallow 比较,只有 profile 或 token 真正变化时才 re-render
const { profile, token } = useStore(
useShallow((s) => ({
profile: s.user.profile,
token: s.user.token,
}))
);
return <div>{profile?.name}</div>;
}
❌ Bad Case: 不使用 useShallow 导致每次都重新渲染
typescript
function UserCard() {
// ❌ 问题:每次 useStore 调用都会返回新的对象引用
// 即使 profile 和 token 的值没变,组件也会 re-render
const { profile, token } = useStore((s) => ({
profile: s.user.profile,
token: s.user.token,
}));
return <div>{profile?.name}</div>;
}
// 验证问题:
function ParentComponent() {
const unrelatedState = useStore((s) => s.cart.items); // 购物车变化
return (
<div>
{/* ❌ UserCard 也会重渲染,即使 user 数据没变 */}
<UserCard />
</div>
);
}
为什么不好?
- Selector 返回的对象每次都是新引用,React 默认使用
Object.is比较 - 导致大量不必要的重渲染
- 在列表渲染时性能问题尤为明显
规则:
- 返回单个原始值 (string/number/boolean)→ 无需
useShallow - 返回对象/数组 (即使内容相同)→ 必须使用
useShallow
4.2 合并多次更新到单个 setState
✅ Good Case: 在单次 setState 中更新多个字段
typescript
export const syncUserData = async (userId: string) => {
const [profile, settings, orders] = await Promise.all([
api.getProfile(userId),
api.getSettings(userId),
api.getOrders(userId),
]);
// ✅ 方案 1: 合并到单个 setState(推荐)
useStore.setState((state) => ({
user: { ...state.user, profile },
settings: { ...state.settings, data: settings },
orders: { ...state.orders, list: orders },
}));
};
❌ Bad Case: 连续多次 setState 可能影响性能
typescript
export const syncUserData = async (userId: string) => {
const [profile, settings, orders] = await Promise.all([
api.getProfile(userId),
api.getSettings(userId),
api.getOrders(userId),
]);
// ❌ 不推荐:虽然 React 18+ 会自动批处理,但不够优雅
useStore.getState()._setUser((draft) => {
draft.profile = profile;
});
useStore.getState()._setSettings((draft) => {
draft.data = settings;
});
useStore.getState()._setOrders((draft) => {
draft.list = orders;
});
};
说明:
- React 18+ 已经自动批处理同步事件和异步回调中的更新
- 但最佳实践仍然是合并到单个 setState ,原因:
- 代码更清晰,意图更明确("这是一次原子操作")
- 避免中间状态(即使很短暂)
- 在某些边缘场景下更可靠
如果必须分开调用怎么办?(React 18+)
typescript
// React 18+ 会自动批处理,无需担心
export const updateMultiple = async () => {
await someAsyncOp();
// ✅ 这些更新会被自动合并
useStore.getState()._setUser((draft) => {
draft.profile = newProfile;
});
useStore.getState()._setOrders((draft) => {
draft.list = newOrders;
});
};
5. 出口文件模式详解
5.1 为什么需要出口文件?
问题场景:没有出口文件时的混乱
typescript
// 组件 A
import { useStore } from "@/stores/createAppStore";
import { login } from "@/stores/actions/user";
import type { UserState } from "@/stores/models/user.model";
// 组件 B
import { useStore } from "../stores/createAppStore";
import { login } from "../stores/actions/user";
import type { UserState } from "../stores/models/user.model";
// 组件 C
import { useStore } from "../../stores/createAppStore";
import { login } from "../../stores/actions/user";
import type { UserState } from "../../stores/models/user.model";
// 问题:
// 1. 需要记住每个具体文件的位置
// 2. 目录结构调整时,需要修改多处导入
// 3. 导入路径冗长,难以维护
解决方案:出口文件模式
typescript
// 从各自的出口文件导入
import { useStore } from "@/stores";
import { login } from "@/stores/actions";
import type { UserState } from "@/stores/models";
// 优势:
// 1. 导入路径简洁统一
// 2. 不需要关心具体文件名
// 3. 目录重构时只需修改对应的 index.ts
// 4. 便于代码审查和规范检查
5.2 如何实现出口文件?
为每个类型的文件创建 index.ts
typescript
// stores/models/index.ts
export * from "./user.model";
export * from "./order.model";
export * from "./cart.model";
typescript
// stores/slices/index.ts
export * from "./user";
export * from "./order";
export * from "./cart";
typescript
// stores/actions/index.ts
export * from "./user";
export * from "./order";
export * from "./cart";
typescript
// stores/selectors/index.ts
// 在这个文件中直接定义和导出 selector hooks
import { useStore } from "../createAppStore";
import { useShallow } from "zustand/react/shallow";
export function useUserProfile() {
return useStore(
useShallow((s) => ({
profile: s.user.profile,
isLoggedIn: !!s.user.profile,
}))
);
}
// ...
stores/index.ts 只导出核心 Store
typescript
// stores/index.ts
/**
* Store 核心导出文件
*
* 只导出核心的 Store Hook 和类型
*/
export { useStore } from "./createAppStore";
export type { AppStore } from "./createAppStore";
5.3 使用出口文件
✅ Good Case: 从各自的出口文件导入
typescript
// ✅ 推荐:从各自的出口文件导入
import { useStore } from "@/stores";
import { login, logout, loadOrders, addToCart } from "@/stores/actions";
import { useUserProfile, useCartSummary } from "@/stores/selectors";
import type { UserState, Order, CartItem } from "@/stores/models";
function MyComponent() {
const { profile } = useUserProfile();
const { itemCount } = useCartSummary();
return (
<div>
<h1>{profile?.name}</h1>
<p>购物车:{itemCount} 件</p>
<button onClick={() => login("user@example.com", "123456")}>登录</button>
</div>
);
}
❌ Bad Case: 直接从具体文件导入
typescript
// ❌ 不推荐:绕过出口文件
import { useStore } from "@/stores/createAppStore";
import { login, logout } from "@/stores/actions/user";
import { loadOrders } from "@/stores/actions/order";
import { useUserProfile } from "@/stores/selectors/useUserProfile";
import type { UserState } from "@/stores/models/user.model";
import type { Order } from "@/stores/models/order.model";
// 问题:
// 1. 导入语句冗长,难以维护
// 2. 需要记住每个具体文件的位置
// 3. 破坏了出口文件的设计
5.4 TypeScript 类型导入
类型导入也应该从出口文件
typescript
// ✅ 推荐:类型从 models/index.ts 导入
import type { UserState, Order, CartItem } from "@/stores/models";
// ❌ 不推荐:直接从具体 model 文件导入类型
import type { UserState } from "@/stores/models/user.model";
import type { Order } from "@/stores/models/order.model";
使用 import type 语法
typescript
// ✅ 推荐:使用 import type 明确标识类型导入
import { useStore } from "@/stores";
import { login } from "@/stores/actions";
import type { UserState, Order } from "@/stores/models";
// 或者混合导入
import { login } from "@/stores/actions";
import { type UserState, type Order } from "@/stores/models";
5.5 内部文件之间的导入
内部文件(stores 目录内)可以直接相互导入
typescript
// stores/actions/user.ts
// ✅ 内部文件可以直接导入其他内部文件
import { useStore } from "../createAppStore";
import type { UserState } from "../models/user.model";
export const login = async (email: string, pwd: string) => {
// ...
};
typescript
// stores/createAppStore.ts
// ✅ 内部文件可以直接导入 slices
import { createUserSlice } from "./slices/user";
import { createOrderSlice } from "./slices/order";
import { createCartSlice } from "./slices/cart";
规则:
- 外部使用 (组件、页面等)→ 从各自的出口文件导入
useStore→@/stores- actions →
@/stores/actions - selectors →
@/stores/selectors - types →
@/stores/models
- 内部使用(stores 目录内的文件)→ 可以直接相对导入
5.6 出口文件的维护
添加新的 action 时:
- 在
actions/xxx.ts中编写 action - 在
actions/index.ts中导出(如果使用export *,会自动导出) - 组件从
@/stores/actions导入
添加新的 model 时:
- 在
models/xxx.model.ts中定义类型和初始状态 - 在
models/index.ts中导出 - 组件从
@/stores/models导入类型
添加新的 selector hook 时:
- 在
selectors/index.ts中编写 hook - 组件从
@/stores/selectors导入
最佳实践:
- 使用
export *简化导出语句 - 为每个类型的文件创建
index.ts作为出口 stores/index.ts只导出useStore和AppStore- 定期检查是否有组件绕过出口文件导入