针对 zustand 的灵魂拷问

针对 zustand 的灵魂拷问

在 React 状态管理的江湖里,Zustand 凭借"极简 API""轻量无依赖"的标签迅速出圈,成为很多开发者逃离 Redux 样板代码、规避 Context API 性能陷阱的首选。但越是看似简单的工具,越藏着值得深究的底层逻辑与实践取舍。当我们把"好用"的直观感受拆解为一个个具体问题,才发现真正理解 Zustand,需要跨越这些灵魂拷问。

一、基础认知篇:Zustand 真的"零门槛"吗?

1. 不用 Provider 包裹,Zustand 是如何实现全局状态共享的?

这是很多人接触 Zustand 时的第一个困惑------毕竟 Redux、Context API 都需要在根组件嵌套 Provider 才能传递状态。答案藏在 Zustand 的设计本质:它的 Store 是独立于 React 组件树的"外部容器",基于发布/订阅模式实现状态与组件的联动,而非依赖 React 的 Context 机制。

具体来说,通过 create 函数创建的 Store 会返回一个自定义 Hook(如 useStore),组件调用这个 Hook 时,本质上是向 Store 注册了一个订阅者。当 Store 中的状态通过 set 函数更新时,会遍历所有订阅者并触发组件重渲染。这种脱离组件树的设计,不仅省去了 Provider 嵌套的繁琐,还允许在非 React 环境(如异步函数、工具函数)中直接访问和修改状态,这是 Context API 难以实现的优势。

核心实现代码示例:

javascript 复制代码
// 1. 创建全局 Store
import { create } from 'zustand';

// 定义包含状态和修改方法的 Store
const useCounterStore = create((set) => ({
  count: 0,
  increment: () => set((state) => ({ count: state.count + 1 })), // 函数式更新保证拿到最新状态
  decrement: () => set((state) => ({ count: state.count - 1 }))
}));

// 2. 组件中直接使用(无需 Provider)
function Counter() {
  // 订阅 count 状态和 increment 方法
  const { count, increment } = useCounterStore();
  return <button onClick={increment}>Count: {count}</button>;
}

// 3. 非 React 环境使用(如工具函数)
function logCount() {
  const count = useCounterStore.getState().count;
  console.log('当前计数:', count);
}

2. "极简 API"的背后,是简化还是隐藏了复杂度?

Zustand 最吸引人的地方,莫过于几行代码就能创建一个可复用的全局状态:引入 create 函数,定义状态和修改方法,组件中直接调用 useStore 即可使用。这种简洁性很容易让人觉得"它把复杂的逻辑都封装了",但事实是,它只是抛弃了 Redux 中强制性的 Action/Reducer 规范,而非取消了状态管理的核心原则。

比如,Redux 强制要求通过纯函数 Reducer 处理状态更新,以此保证状态变更的可追溯性;而 Zustand 允许直接在 Store 中定义修改状态的方法,看似简化了流程,却把"保证状态不可变性"的责任交给了开发者。如果不小心在 Zustand 中直接修改状态对象(如 state.list.push(item)),就会导致状态变更不可追踪、组件不触发重渲染的隐蔽 Bug。可见,Zustand 的"简单"是对 API 层面的简化,而非对状态管理核心逻辑的弱化,新手很容易在这里踩坑。

状态不可变性正确/错误示例:

javascript 复制代码
const useTodoStore = create((set) => ({
  todos: [{ id: 1, text: '学习 Zustand' }],
  
  // 错误写法:直接修改状态对象(违反不可变性)
  wrongAddTodo: (text) => set((state) => {
    state.todos.push({ id: Date.now(), text }); // 直接修改原数组
    return { todos: state.todos }; // 引用未变,组件不重渲染
  }),
  
  // 正确写法:创建新数组(保证不可变性)
  correctAddTodo: (text) => set((state) => ({
    todos: [...state.todos, { id: Date.now(), text }] // 展开原数组,添加新元素
  }))
}));

可以借助官方 immer 中间件实现直观的状态修改。

3. TypeScript 支持"天生友好",真的不用手动写类型吗?

Zustand 官方强调原生 TypeScript 支持,但这并不意味着"零类型定义"。实际上,要实现真正的类型安全,需要开发者主动定义 State 和 Actions 的接口,尤其是在复杂业务场景中。

简单场景下,Zustand 可以通过类型推导自动识别状态和方法的类型;但当状态结构嵌套较深、Actions 存在依赖关系(如一个 Action 需要调用另一个 Action 的返回值)时,必须显式定义接口来约束 State 和 Actions 的结构。例如,管理文章列表状态时,需要分别定义 State 接口(包含 articleList、loading、error 等字段)和 Actions 接口(包含 queryArticleList、loadMore 等方法),才能让 TypeScript 精准捕获类型错误。所谓"天生友好",是指它的类型推导逻辑清晰,无需像 Redux 那样编写大量模板类型代码,而非完全无需定义类型。

typescript 复制代码
// 1. 显式定义 State 和 Actions 接口
interface ArticleState {
  articleList: { id: number; title: string; content: string }[];
  loading: boolean;
  error: string | null;
  page: number;
  pageSize: number;
}

interface ArticleActions {
  queryArticleList: () => Promise<void>;
  loadMore: () => Promise<void>;
  clearError: () => void;
}

// 2. 创建带类型约束的 Store
const useArticleStore = create<ArticleState & ArticleActions>((set, get) => ({
  articleList: [],
  loading: false,
  error: null,
  page: 1,
  pageSize: 10,

  // 异步 Action,依赖其他状态和方法
  queryArticleList: async () => {
    set({ loading: true, error: null });
    try {
      const { page, pageSize } = get(); // 获取当前状态
      const res = await fetch(`/api/articles?page=${page}&pageSize=${pageSize}`);
      const data = await res.json();
      set({ articleList: data.list, loading: false });
    } catch (err) {
      set({ error: (err as Error).message, loading: false });
    }
  },

  loadMore: async () => {
    const { page, queryArticleList } = get();
    set({ page: page + 1 }); // 更新页码
    await queryArticleList(); // 调用其他 Action
  },

  clearError: () => set({ error: null })
}));

二、实践取舍篇:Zustand 适合你的项目吗?

1. 小项目用 Zustand 是"杀鸡用牛刀"吗?大项目用它又会"力不从心"吗?

这是关于技术选型的核心拷问。首先,小项目用 Zustand 并非"牛刀杀鸡"------因为它的包体积仅约 1KB(gzip 后),几乎没有性能损耗,且无需复杂配置,比 Context API + useReducer 的组合更简洁,反而能提升开发效率。对于只有几个全局状态(如用户登录态、主题设置)的小应用,Zustand 能以最少的代码实现状态共享,优势明显。

而大项目用 Zustand 是否"力不从心",关键在于是否做好了状态拆分。Zustand 支持多 Store 模式,可按业务模块拆分出独立的 Store(如 userStore、cartStore、articleStore),每个 Store 维护自身的状态和逻辑,实现"单一职责"。这种模块化设计能避免 Redux 单一 Store 导致的状态树臃肿问题。此外,Zustand 的中间件生态(如 devtools 用于调试、persist 用于状态持久化)也能支撑复杂场景的需求。真正可能"力不从心"的,是那些需要严格遵循 Flux 架构、强调状态流转规范性的大型团队------此时 Redux 的严格约束反而能减少协作混乱,而 Zustand 的灵活性可能成为团队的"自由负担"。

2. 逻辑迁移到 Zustand Store,如何做逻辑的复用?

这是实践中容易被忽略的关键问题。

React 自定义 Hook 能天然依托 useState、useEffect 等原生 Hook 封装带状态的逻辑,且复用成本极低;而 Zustand 的 Store 脱离了 React 组件上下文,无法直接复用这些成熟的 Hook 资源,只能手动拆解 Hook 内部逻辑,用 Store 状态+局部变量重新实现,不仅增加了开发成本,还可能因手动复刻逻辑引入新的 Bug。这种"为了状态共享而牺牲现有优质生态复用能力"的取舍,正是很多开发者在实际项目中陷入纠结的关键。

最典型的痛点莫过于社区成熟自定义 Hook 的复用困境------以 ahooks 的 useCountdown 为例,原本只需一行代码就能复用成熟的倒计时逻辑,无需关心定时器管理、内存泄漏防护等细节;但迁移到 Zustand 时,却要进行一场"痛苦的重构"。这恰恰暴露了 Zustand 的核心短板:它并未提供与 React 自定义 Hook 同等灵活的逻辑复用能力。

以下是具体的代码对比,清晰呈现这种复用与重构的差异:

  1. 直接复用 ahooks useCountdown(简洁高效,无需关注内部实现):
javascript 复制代码
// 安装依赖:npm install ahooks
import { useCountdown } from 'ahooks';

function CountdownComponent() {
  // 一行代码复用成熟倒计时逻辑,支持丰富配置
  const { current, start, pause, reset } = useCountdown({
    targetDate: Date.now() + 60 * 1000, // 60秒倒计时
    onFinish: () => console.log('倒计时结束'),
  });

  return (
    <div>
      <span>剩余时间:{Math.ceil(current / 1000)}s</span>
      <button onClick={start}>开始</button>
      <button onClick={pause}>暂停</button>
      <button onClick={reset}>重置</button>
    </div>
  );
}
  1. 迁移到 Zustand:为了复用 countdown 的逻辑,实现了一个 createCountdownLogic 方法,然后需要在 create store 里面进行状态的回调的绑定,样板代码的味道很明显。(如果有更好的方案,请留言教教我~)
javascript 复制代码
// 方案:将倒计时核心逻辑(含定时器)封装到独立函数,Store 仅负责状态联动
import { create } from 'zustand';
import { useCountdown } from 'ahooks';

// 1. 封装倒计时核心逻辑(含定时器管理,完全剥离 Store 依赖)
function createCountdownLogic(initialSeconds = 60, onTick) {
  let targetDate = Date.now() + initialSeconds * 1000;
  let timer = null;
  
  // 计算剩余时间
  const calculateRemaining = () => Math.max(0, targetDate - Date.now());
  
  // 启动倒计时(内部管理定时器)
  const start = () => {
    if (timer) clearInterval(timer); // 避免重复启动
    const remaining = calculateRemaining();
    if (remaining === 0) reset(); // 若已结束,先重置
    
    // 启动定时器,触发回调更新外部状态
    timer = setInterval(() => {
      const currentRemaining = calculateRemaining();
      onTick(currentRemaining); // 通知外部更新状态
      
      // 倒计时结束,自动停止
      if (currentRemaining === 0) {
        stop();
      }
    }, 1000);
  };
  
  // 停止倒计时
  const stop = () => {
    clearInterval(timer);
    timer = null;
  };
  
  // 重置倒计时
  const reset = (newSeconds = initialSeconds) => {
    stop();
    targetDate = Date.now() + newSeconds * 1000;
    onTick(calculateRemaining()); // 重置后同步更新外部状态
  };
  
  // 销毁资源(避免内存泄漏)
  const destroy = () => {
    stop();
  };
  
  return {
    getCurrent: calculateRemaining,
    start,
    stop,
    reset,
    destroy
  };
}

// 2. Store 调用封装逻辑,仅负责状态联动(无需关注定时器细节)
const useCountdownStore = create((set) => {
  // 创建倒计时逻辑实例,通过 onTick 回调同步状态
  const countdownLogic = createCountdownLogic(60, (current) => {
    set({ current }); // 同步剩余时间到 Store 状态
  });
  
  // 组件卸载时销毁倒计时资源
  set((state) => ({ ...state, destroyCountdown: countdownLogic.destroy }));
  
  return {
    current: countdownLogic.getCurrent(), // 初始剩余时间
    destroyCountdown: () => {}, // 销毁方法占位
    
    // 代理倒计时方法到封装逻辑
    start: countdownLogic.start,
    pause: countdownLogic.stop,
    reset: () => countdownLogic.reset(60)
  };
});

// 3. 组件使用(需在卸载时销毁资源,避免内存泄漏)
function CountdownComponent() {
  const { current, start, pause, reset, destroyCountdown } = useCountdownStore();
  
  useEffect(() => {
    return () => destroyCountdown(); // 组件卸载时销毁倒计时资源
  }, [destroyCountdown]);
  
  return (
    <div>
      <span>剩余时间:{Math.ceil(current / 1000)}s</span>
      <button onClick={start} disabled={current > 0 && current < 60}>开始</button>
      <button onClick={pause} disabled={current === 60 || current === 0}>暂停</button>
      <button onClick={reset}>重置</button>
    </div>
  );
}

3. 多 Store 该拆分还是合并?核心原则是什么?

拆分或合并的核心原则是"状态的关联性":强关联的状态应合并在同一个 Store 中,无关联的状态应拆分到独立 Store。例如,用户的登录态、个人信息、权限列表属于强关联状态,应放在 userStore 中;而购物车状态与文章列表状态无直接关联,应分别放在 cartStore 和 articleStore 中。

需要避免两个极端:一是将所有状态都塞进一个 Store,导致状态树冗余、修改逻辑混乱;二是过度拆分 Store,导致组件需要同时订阅多个 Store(如页面同时需要用户信息和购物车数据时,需调用 useUserStore 和 useCartStore),虽然不会影响性能,但会增加组件代码的冗余度。此外,当两个 Store 存在依赖关系(如购物车结算需要用户权限)时,可通过在一个 Store 中调用 get 函数获取另一个 Store 的状态,无需强行合并。

结合电商实际业务场景,通过代码示例更直观理解拆分逻辑:

javascript 复制代码
// 1. 强关联状态合并:userStore(用户相关状态强关联)
const useUserStore = create((set) => ({
  userInfo: { id: null, name: '', avatar: '' }, // 个人信息
  token: '', // 登录凭证
  permissions: [], // 权限列表
  // 关联方法:登录后同步更新所有用户相关状态
  login: (userData) => set({
    userInfo: userData.userInfo,
    token: userData.token,
    permissions: userData.permissions
  }),
  logout: () => set({ userInfo: { id: null, name: '', avatar: '' }, token: '', permissions: [] })
}));

// 2. 无关联状态拆分:cartStore(购物车)与 productStore(商品)分离
const useCartStore = create((set, get) => ({
  cartItems: [],
  addToCart: (product) => set((state) => ({
    cartItems: [...state.cartItems, product]
  })),
  // 依赖用户权限:结算前校验权限(跨 Store 获取状态,无需合并)
  checkOut: () => {
    const { permissions } = useUserStore.getState();
    if (!permissions.includes('order:create')) {
      throw new Error('无结算权限');
    }
    // 结算逻辑...
  }
}));

// 3. 无关联状态拆分:productStore(商品相关,与购物车无强关联)
const useProductStore = create((set) => ({
  productList: [],
  currentProduct: null,
  filterConditions: { priceRange: [0, 1000], category: 'all' },
  fetchProductList: async () => {
    const { filterConditions } = get();
    const res = await fetch(`/api/products?category=${filterConditions.category}&minPrice=${filterConditions.priceRange[0]}&maxPrice=${filterConditions.priceRange[1]}`);
    const data = await res.json();
    set({ productList: data.list });
  }
}));

// 组件使用:同时订阅多个无关联 Store(不影响性能)
function ProductDetailPage({ productId }) {
  // 订阅商品状态
  const { currentProduct, fetchProductList } = useProductStore();
  // 订阅购物车状态
  const { addToCart } = useCartStore();
  // 订阅用户状态(用于判断是否登录)
  const { userInfo } = useUserStore();

  useEffect(() => {
    fetchProductList();
  }, [fetchProductList]);

  return (
    <div>
      {currentProduct && (
        <>
          <h3>{currentProduct.name}</h3>
          <p>价格:{currentProduct.price}</p>
          <button
            onClick={() => addToCart(currentProduct)}
            disabled={!userInfo.id}
          >
            {userInfo.id ? '加入购物车' : '登录后可添加'}
          </button>
        </>
      )}
    </div>
  );
}

代码说明:userStore 内的用户信息、token、权限是强关联状态,合并后登录/退出逻辑更集中;cartStore 与 productStore 无直接业务关联,拆分后各自维护核心逻辑,组件需同时使用时直接多 Store 订阅即可,无需强行合并导致状态冗余。

4. 按需订阅状态,真的能避免"无效重渲染"吗?

Zustand 的一大卖点是"组件可精准订阅所需状态,避免不必要的重渲染",这也是它优于 Context API 的关键。但这一优势并非"自动生效",而是需要正确使用选择器(Selector)函数。

如果直接在组件中订阅整个 Store(如 const { count, theme } = useStore()),当 Store 中任何一个状态(无论是 count 还是 theme)更新时,组件都会重渲染。只有通过选择器函数精准指定所需状态(如 const count = useStore(state => state.count)),组件才只会在 count 变化时重渲染。此外,对于嵌套对象类型的状态(如 state.user = { name: 'xxx', age: 20 }),还需要使用 shallow 比较器(import { shallow } from 'zustand/shallow'),否则即使只是 user.age 变化,选择器返回的 user 对象引用改变,仍会触发不必要的重渲染。可见,避免无效重渲染的关键,是开发者能否正确使用选择器和比较器,而非工具本身的"自动优化"。

javascript 复制代码
import { shallow } from 'zustand/shallow';

const useUserStore = create((set) => ({
  user: { name: '张三', age: 25, address: { city: '北京' } },
  theme: 'light',
  updateAge: (age) => set((state) => ({ user: { ...state.user, age } }))
}));

// 组件1:错误订阅(订阅整个 Store,theme 变化也会重渲染)
function BadUserInfo() {
  const { user, updateAge } = useUserStore(); // 不推荐
  return (
    <div>
      <p>姓名:{user.name}</p>
      <button onClick={() => updateAge(26)}>修改年龄</button>
    </div>
  );
}

// 组件2:正确订阅(精准订阅 user 内的部分字段,用 shallow 浅比较)
function GoodUserInfo() {
  // 只订阅 name 和 age,且浅比较对象
  const { name, age } = useUserStore(
    (state) => ({ name: state.user.name, age: state.user.age }),
    shallow // 关键:避免对象引用变化导致的无效重渲染
  );
  const updateAge = useUserStore((state) => state.updateAge);
  return (
    <div>
      <p>姓名:{name}</p>
      <p>年龄:{age}</p>
      <button onClick={() => updateAge(26)}>修改年龄</button>
    </div>
  );
}

三、对比差异篇:Zustand 真的能替代 Redux/Pinia 吗?

1. 与 Redux 相比,Zustand 是"简化版"还是"差异化方案"?

很多人将 Zustand 视为"Redux 简化版",但本质上两者是基于不同设计哲学的差异化方案。Redux 遵循严格的 Flux 架构,通过"单一 Store + 纯函数 Reducer + Action 分发"的模式,保证状态变更的可追溯性和可预测性,适合大型团队协作(规范的流程能减少混乱);而 Zustand 追求"极简灵活",抛弃了 Redux 中繁琐的模板代码,允许直接定义修改状态的方法,更适合追求开发效率、状态逻辑相对简单的场景。

从生态来看,Redux 拥有极其丰富的中间件(如 redux-saga 处理复杂异步、reselect 实现状态缓存)和工具链,适合处理超大型应用的复杂副作用;而 Zustand 内置了 devtools、persist 等常用中间件,能满足大部分场景的需求,但在极端复杂的副作用管理上,生态不如 Redux 完善。因此,Zustand 并非"替代"Redux,而是为开发者提供了"灵活简洁"与"严格规范"之外的另一种选择。

2. 同为轻量方案,Zustand 与 Pinia 的核心差异是什么?

首先要明确两者的技术栈定位:Zustand 是为 React 设计的状态管理库,而 Pinia 是 Vue 官方推荐的状态管理方案(替代 Vuex),两者本质上服务于不同的框架生态,这是最核心的差异。

如果仅从设计理念对比,两者有诸多相似之处(如支持多 Store、简化状态更新流程、原生 TypeScript 支持),但核心差异在于"响应式实现":Pinia 基于 Vue 的 Proxy 响应式系统,天然具备细粒度的状态更新能力(只有依赖该状态的组件才会重渲染);而 Zustand 基于发布/订阅模式,需要通过选择器函数实现精准订阅。此外,Pinia 与 Vue DevTools 深度集成,调试体验更贴合 Vue 开发者习惯;而 Zustand 可集成 Redux DevTools,更适合 React 开发者迁移使用。

四、高级实战篇:Zustand 进阶使用的坑你踩过吗?

1. 异步操作直接写在 Store 中,如何保证状态一致性?

Zustand 允许在 Store 中直接定义异步 Action(如 fetch 数据),这比 Redux 中需要通过中间件(如 redux-thunk)处理异步更简洁。但异步操作的"并行执行""错误处理""加载状态管理",仍需要开发者手动保证状态一致性。

例如,实现文章列表查询功能时,需要在异步 Action 中添加 loading 状态(避免重复请求)和 error 状态(处理请求失败):调用接口前设置 loading: true,请求成功后更新数据并设置 loading: false,请求失败后捕获错误并设置 error 信息。此外,当多个异步操作并行执行时,需要注意"状态覆盖"问题(如同时发起两个列表查询请求,后返回的结果会覆盖先返回的结果),此时需要通过"请求标识"或"取消上一次请求"的方式解决,而 Zustand 本身并不提供这些能力,需要结合 axios 取消请求、React Query 等工具实现。

dart 复制代码
import axios from 'axios';

const useArticleStore = create((set, get) => ({
  articleList: [],
  loading: false,
  error: null,
  // 用于取消上一次请求的控制器
  abortController: null,

  queryArticleList: async () => {
    const { loading, abortController } = get();
    if (loading) {
      // 存在正在进行的请求,取消上一次
      abortController?.abort();
    }
    const newAbortController = new AbortController();
    set({ loading: true, error: null, abortController: newAbortController });
    try {
      const res = await axios.get('/api/articles', {
        signal: newAbortController.signal // 关联取消信号
      });
      set({ 
        articleList: res.data.list, 
        loading: false,
        abortController: null // 请求完成,清空控制器
      });
    } catch (err) {
      if (err.name !== 'CanceledError') { // 排除主动取消的错误
        set({ error: err.message, loading: false, abortController: null });
      }
    }
  }
}));

2. 状态持久化(persist 中间件)真的"开箱即用"吗?

Zustand 内置的 persist 中间件确实能快速实现状态持久化(如存入 localStorage),一行代码即可配置,看似"开箱即用",但在实际使用中仍有诸多细节需要注意。例如:敏感数据(如 token)存入 localStorage 存在安全风险,需结合加密存储或改用 sessionStorage;当状态中包含不可序列化的数据(如函数、Promise)时,persist 中间件会报错,需通过 partialize 配置筛选可序列化状态;多 Store 同时使用 persist 时,需指定不同的 name(存储键名),避免状态覆盖。

因此,persist 中间件的"开箱即用"是针对简单场景的,复杂场景仍需要开发者根据业务需求进行定制化配置。

javascript 复制代码
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
import { encrypt, decrypt } from './utils/crypto'; // 自定义加密解密工具

// 1. 简单场景:基础持久化(存入 localStorage)
const useSimpleStore = create(
  persist(
    (set) => ({
      count: 0,
      increment: () => set((state) => ({ count: state.count + 1 }))
    }),
    {
      name: 'simple-store' // 存储键名,默认存入 localStorage
    }
  )
);

// 2. 复杂场景:加密敏感数据 + 筛选可序列化状态
const useUserStore = create(
  persist(
    (set) => ({
      userInfo: { id: 1, name: '张三', token: 'xxx-xxx-xxx' },
      permissions: ['read', 'write'],
      // 不可序列化的函数,需要排除
      formatUser: (user) => `${user.name}(ID:${user.id})`
    }),
    {
      name: 'user-store',
      storage: {
        // 自定义存储逻辑,加密敏感数据
        getItem: (name) => {
          const item = localStorage.getItem(name);
          if (item) return decrypt(item); // 读取时解密
          return null;
        },
        setItem: (name, value) => {
          localStorage.setItem(name, encrypt(value)); // 存储时加密
        },
        removeItem: (name) => localStorage.removeItem(name)
      },
      // 筛选需要持久化的状态(排除不可序列化的 formatUser)
      partialize: (state) => ({
        userInfo: state.userInfo,
        permissions: state.permissions
      })
    }
  )
);

3. 服务端渲染(SSR)场景下,Zustand 如何保证状态同步?

由于 Zustand 的 Store 是"全局单例",在 SSR 场景下(如 Next.js),多个请求会共享同一个 Store,导致状态污染(如用户 A 的数据被用户 B 获取)。这是 Zustand 在 SSR 中最核心的问题,需要通过"状态隔离"来解决。

解决方案是利用 React 的 Context API 在服务端为每个请求创建独立的 Store 实例,然后通过 Provider 传递给组件(此时需要临时使用 Context,与 Zustand 平时"无需 Provider"的特性相悖);在客户端渲染时,再将服务端传递的状态" hydration"到客户端 Store 中。这一过程需要额外的配置,并非 Zustand 原生支持的"开箱即用"能力,对于不熟悉 SSR 原理的开发者来说,存在一定的学习成本。

javascript 复制代码
// 1. 创建可复用于 SSR 的 Store 工厂函数
import { createContext, useContext, create } from 'zustand';

// 定义 Store 类型
interface CounterState {
  count: number;
  increment: () => void;
}

// 创建 Context
const CounterContext = createContext<ReturnType<typeof createCounterStore> | null>(null);

// 工厂函数:每次调用创建新的 Store 实例(避免请求间共享状态)
function createCounterStore(initialCount = 0) {
  return create<CounterState>((set) => ({
    count: initialCount,
    increment: () => set((state) => ({ count: state.count + 1 }))
  }));
}

// 2. 服务端:为每个请求创建 Store 并注入 Context
// 以 Next.js 为例(page.tsx)
export async function getServerSideProps() {
  // 服务端初始化状态(如从数据库获取)
  const initialCount = 10;
  const serverStore = createCounterStore(initialCount);
  // 将初始状态传递给客户端
  return { props: { initialCount } };
}

// 3. 组件:提供/消费 Context
export default function CounterPage({ initialCount }) {
  // 客户端根据服务端传递的初始状态创建 Store
  const clientStore = createCounterStore(initialCount);
  return (
    <CounterContext.Provider value={clientStore}>
      <CounterComponent />
    </CounterContext.Provider>
  );
}

// 4. 子组件:通过 Context 消费 Store
function CounterComponent() {
  const store = useContext(CounterContext);
  if (!store) throw new Error('Store 未注入 Context');
  const { count, increment } = store;
  return <button onClick={increment}>Count: {count}</button>;
}

五、总结:理解 Zustand 的核心是"取舍"

经过这一系列灵魂拷问,我们会发现:Zustand 不是"银弹",它的优势(极简 API、轻量灵活)与局限(生态不如 Redux 完善、复杂场景需手动封装)是一体两面。选择 Zustand,本质上是选择了"开发效率优先""灵活简洁优先"的状态管理思路;而能否用好 Zustand,关键在于能否理解它的设计原理,在"灵活"与"规范"之间找到平衡,在"简洁 API"背后做好状态拆分、类型约束、性能优化等细节工作。

最终,没有最好的状态管理库,只有最适合项目规模、团队习惯的方案。而这些灵魂拷问,正是帮助我们从"会用"到"用好",从"依赖工具"到"理解工具"的必经之路。

相关推荐
Mintopia11 小时前
2025,我的「Vibe Coding」时刻
前端·人工智能·aigc
西凉的悲伤12 小时前
html制作太阳系行星运行轨道演示动画
前端·javascript·html·行星运行轨道演示动画
C_心欲无痕12 小时前
网络相关 - http1.1 与 http2
前端·网络
一只爱吃糖的小羊12 小时前
Web Worker 性能优化实战:将计算密集型逻辑从主线程剥离的正确姿势
前端·性能优化
低保和光头哪个先来12 小时前
源码篇 实例方法
前端·javascript·vue.js
你真的可爱呀12 小时前
自定义颜色选择功能
开发语言·前端·javascript
小王和八蛋12 小时前
JS中 escape urlencodeComponent urlencode 区别
前端·javascript
奔跑的web.12 小时前
TypeScript类型系统核心速通:从基础到常用复合类型包装类
开发语言·前端·javascript·typescript·vue
Misnice12 小时前
Webpack、Vite 、Rsbuild 区别
前端·webpack·node.js
Kagol12 小时前
🎉历时1年,TinyEditor v4.0 正式发布!
前端·typescript·开源