zustand 从原理到实践 - 最佳实践

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 名称生成工具

📁 关键说明:

  1. stores/index.ts - 只导出核心 Store

    • 导出 useStoreAppStore 类型
    • 不再重新导出其他内容
  2. 各子目录的 index.ts - 各类型文件的统一出口

    • models/index.ts - 导出所有 model 类型和初始状态
    • slices/index.ts - 导出所有 slice 类型和创建函数
    • actions/index.ts - 导出所有 action 函数
    • selectors/index.ts - 导出所有复用型 selector hooks
  3. 组件导入规则

    • useStore@/stores 导入
    • actions 从 @/stores/actions 导入
    • selectors 从 @/stores/selectors 导入
    • types 从 @/stores/models 导入
  4. selectors/ - 复用型 selector hooks

    • 封装常用的状态访问逻辑
    • 内部使用 useShallowuseMemo 优化性能
    • 为复杂的派生状态提供独立的 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/loadinguser/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?

  1. 多个组件需要相同的状态访问逻辑 - 如 useUserProfileuseCartSummary
  2. 需要对状态进行复杂计算或派生 - 如计算总价、筛选列表
  3. 需要组合多个状态片段 - 如同时获取用户信息和登录状态
  4. 需要性能优化 - 使用 useShallowuseMemo 避免不必要的重渲染

最佳实践:

  • 将 selector hooks 统一放在 selectors/ 目录
  • selectors/index.ts 中统一导出
  • 在 hook 内部使用 useShallowuseMemo 优化性能
  • 为 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 时:

  1. actions/xxx.ts 中编写 action
  2. actions/index.ts 中导出(如果使用 export *,会自动导出)
  3. 组件从 @/stores/actions 导入

添加新的 model 时:

  1. models/xxx.model.ts 中定义类型和初始状态
  2. models/index.ts 中导出
  3. 组件从 @/stores/models 导入类型

添加新的 selector hook 时:

  1. selectors/index.ts 中编写 hook
  2. 组件从 @/stores/selectors 导入

最佳实践:

  • 使用 export * 简化导出语句
  • 为每个类型的文件创建 index.ts 作为出口
  • stores/index.ts 只导出 useStoreAppStore
  • 定期检查是否有组件绕过出口文件导入
相关推荐
牛仔只喝牛奶17 小时前
多端跨端技术选型:小程序 + H5 + App 的两栈拆分落地
react native·react.js
wear工程师20 小时前
setState 到底是不是异步的?这题从 React 16 问到 18,标准答案早就变了
react.js·面试
wanghowie1 天前
LangGraph4j 落地(二)— ReAct 多步工具与 trace 扩展
前端·react.js·前端框架
csj501 天前
前端基础之《React(11)—state声明式》
前端·react.js
张元清1 天前
React useLocalStorage Hook:SSR 安全的持久化状态(2026)
javascript·react.js
析数塔1 天前
2026前端框架深度解析:React Compiler、Angular Zoneless、Vue 3.5重写游戏规则
前端·vue.js·react.js
杖雍皓1 天前
没有服务器如何构建现代 Web 应用:平台选型与组合策略
运维·服务器·前端·react.js·github·netlify·vercel
sun_weitao1 天前
微前端框架之qiankun 怎么用?vue3+vite+qiankun 和 react + qiankun
前端·react.js·前端框架
名字还没想好☜2 天前
React useEffect 依赖数组踩坑:闭包陷阱与清理函数
前端·javascript·react.js·react·useeffect
我是大卫2 天前
【图】React源码解析-第四部分:事件与跨层传递
react.js·源码