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
  • 定期检查是否有组件绕过出口文件导入
相关推荐
静待雨落9 小时前
zustand持久化
前端·react.js
yangpow29 小时前
React 列表里 ID 转名称?一个组件搞定批量请求 + 缓存 + 懒加载
react.js
xhxxx10 小时前
Vite + React 黄金组合:打造秒开、可维护、高性能的现代前端工程
前端·react.js·vite
用户81686947472510 小时前
深入 useState、useEffect 的底层实现
前端·react.js
Tzarevich10 小时前
React 中的 JSX 与组件化开发:以函数为单位构建现代前端应用
前端·react.js·面试
别急国王10 小时前
React Hooks 为什么不能写在判断里
react.js
Mintopia10 小时前
⚛️ React 17 vs React 18:Lanes 是同一个模型,但跑法不一样
前端·react.js·架构
玉木成琳10 小时前
Taro + React + @nutui/nutui-react-taro 时间选择器重写
前端·react.js·taro
2401_8604947011 小时前
在React Native中实现鸿蒙跨平台开发中开发一个运动类型管理系统,使用React Navigation设置应用的导航结构,创建一个堆栈导航器
react native·react.js·harmonyos