zustand 基础和进阶

学习 Zustand 的基本用法,包括创建 Store、定义状态、更新状态以及在 React 组件中使用。

Zustand 是一个小型、快速、可扩展的 React 状态管理解决方案,它基于 Hooks,API 非常简洁。

1. 学会基本创建create 创建状态, 更新状态, 组件中使用

核心概念:create 函数

Zustand 的一切都围绕 create 函数展开。这个函数用来创建你的 "Store"(状态容器)。


1. 创建 Store (使用 create)

首先,你需要安装 Zustand:

bash 复制代码
npm install zustand
# 或者
yarn add zustand
    

然后,创建一个文件(例如 store.js 或 counterStore.js)来定义你的 Store。

js 复制代码
// src/store/counterStore.js
import { create } from 'zustand';

// create 函数接收一个回调函数作为参数
// 这个回调函数接收 set 和 get 作为参数
// 它需要返回一个对象,这个对象就是你的 Store 的初始状态和操作方法(actions)
const useCounterStore = create((set, get) => ({
  // 状态 (State)
  count: 0,
  user: { name: '游客', age: 0 },

  // 操作方法 (Actions) - 用于更新状态
  increment: () => {
    // set 函数用于更新状态
    // 它会 *合并* (merge) 你提供的新状态到现有状态中
    set((state) => ({ count: state.count + 1 }));
    // 相当于 set({ count: get().count + 1 }),但推荐使用上面的函数形式避免闭包问题
  },

  decrement: () => {
    set((state) => ({ count: state.count - 1 }));
  },

  setUserName: (newName) => {
    set((state) => ({
      user: { ...state.user, name: newName } // 更新嵌套对象时,需要展开旧状态
    }));
  },

  // 也可以在这里定义异步操作
  fetchData: async () => {
    // const response = await fetch(...);
    // const data = await response.json();
    // set({ someData: data });
    console.log("模拟异步获取数据");
  },

  // 使用 get() 获取当前状态 (在 action 内部)
  logCurrentCount: () => {
    const currentCount = get().count; // 使用 get() 获取最新状态
    console.log(`当前的 Count 是: ${currentCount}`);
  }
}));

export default useCounterStore;
    

关键点解释:

  • create: Zustand 的核心函数,用于创建 Store Hook。

  • 回调函数 (set, get) => ({...}):

    • set: 这是最重要的函数,用来更新状态 。它接收一个对象或者一个返回对象的函数。默认情况下,set 执行的是浅合并(Shallow Merge),只会更新你指定的属性,其他属性保持不变。如果你传递一个函数 (state) => newState,你可以安全地基于当前状态 state 计算新状态。
    • get: 一个可选函数,允许你在 action 内部读取当前 Store 的状态。
    • 返回的对象: 这个对象定义了你的 Store 的结构,包括初始状态值(如 count: 0)和更新状态的方法(如 increment, decrement)。

2. 创建状态 (Defining State)

状态就是你在 create 回调函数返回的对象中定义的非函数属性。

在上面的例子中:

  • count: 0 是一个数字类型的状态。
  • user: { name: '游客', age: 0 } 是一个对象类型的状态。

3. 更新状态 (Updating State)

状态的更新是通过调用你在 create 回调函数中定义的函数属性(通常称为 actions)来完成的。这些函数内部会调用 set 方法。

在上面的例子中:

  • increment(): 调用 set 将 count 加 1。
  • decrement(): 调用 set 将 count 减 1。
  • setUserName(newName): 调用 set 更新 user 对象中的 name 属性。注意更新嵌套对象时需要正确处理合并。

4. 在 React 组件中使用

创建好的 Store (useCounterStore 在我们的例子中) 本身就是一个 Hook!你可以在任何 React 函数组件中直接调用它来访问状态和操作方法。

js 复制代码
// src/components/CounterComponent.jsx
import React from 'react';
import useCounterStore from '../store/counterStore';
import { shallow } from 'zustand/shallow'; // 引入 shallow

function CounterComponent() {
  // --- 访问状态和方法 ---

  // 方式一:获取整个 store (当 store 任何部分变化时都可能触发重渲染,不推荐用于大型 store)
  // const store = useCounterStore();
  // console.log('Render - Get Full Store');

  // 方式二:使用选择器 (Selector) 获取特定状态 (推荐!)
  // 只有当 count 值变化时,组件才会重渲染
  const count = useCounterStore((state) => state.count);
  console.log('Render - Get Count');

  // 方式三:使用选择器获取操作方法 (Action 通常不会变,不会引起重渲染)
  const increment = useCounterStore((state) => state.increment);
  const decrement = useCounterStore((state) => state.decrement);
  const setUserName = useCounterStore((state) => state.setUserName);
  const userName = useCounterStore((state) => state.user.name);

  // 方式四:一次性选择多个状态/方法 (推荐使用 shallow 比较)
  // 如果不使用 shallow,每次渲染都会创建一个新对象 { count, increment },导致不必要的重渲染
  const { logCurrentCount } = useCounterStore(
    (state) => ({
      // count: state.count, // 如果这里也选了 count,当 count 变,这里也会触发渲染
      logCurrentCount: state.logCurrentCount,
    }),
    shallow // 使用 shallow 函数进行浅比较,优化性能
  );
  console.log('Render - Get Multiple with Shallow');


  const handleIncrement = () => {
    increment(); // 调用 store 中的 action
  };

  const handleDecrement = () => {
    decrement(); // 调用 store 中的 action
  };

  const handleSetName = () => {
    const newName = prompt("输入新名字:", userName);
    if (newName) {
      setUserName(newName); // 调用带参数的 action
    }
  };

  const handleLogCount = () => {
    logCurrentCount(); // 调用使用了 get() 的 action
  }

  return (
    <div>
      <h1>计数器</h1>
      <p>Count: {count}</p>
      <p>User Name: {userName}</p>
      <button onClick={handleIncrement}>增加 +</button>
      <button onClick={handleDecrement}>减少 -</button>
      <button onClick={handleSetName}>设置名字</button>
      <button onClick={handleLogCount}>在控制台打印 Count</button>
    </div>
  );
}

export default CounterComponent;
    

组件中使用关键点:

  • 直接调用 Hook: useCounterStore() 返回整个 Store,但通常不推荐,因为任何状态变化都可能导致组件重渲染。
  • 选择器 (Selectors) : useCounterStore(state => state.someValue) 是推荐的方式。它接收一个函数,该函数接收整个 state 对象并返回你关心的那部分数据。只有当返回的数据发生变化时,组件才会重新渲染。这极大地优化了性能。
  • 选择 Actions: useCounterStore(state => state.actionName)。因为 Actions 通常是固定的函数引用,选择它们一般不会导致不必要的重渲染。
  • 选择多个值: 当你需要从 Store 中选择多个值时,推荐使用 shallow 函数作为 useCounterStore 的第二个参数。shallow 来自 zustand/shallow,它会进行浅比较,只有当选择的对象中的值实际发生变化时才触发重渲染,而不是因为每次都创建了新的对象引用。

总结:

  1. 创建 (create) : 使用 create((set, get) => ({ ... })) 定义 Store,返回状态和 Actions。set 用于更新,get 用于读取。
  2. 状态 (State) : Store 对象中的非函数属性。
  3. 更新 (Update) : 调用 Store 对象中的函数属性 (Actions),这些函数内部使用 set 来修改状态。set 默认浅合并状态。
  4. 使用 (Usage) : 在组件中调用 Store Hook useMyStore(),并最好使用选择器 useMyStore(state => state.value) 来精确获取所需数据,以优化性能。选择多个值时,考虑使用 shallow 进行比较。

这就是 Zustand 的基础用法,非常简洁直观。你可以基于这些基础知识构建更复杂的应用状态管理。

2. 选择器(selector) 只取需要的数据,避免组件无意义刷新

Zustand 的选择器 (Selectors) 。这是 Zustand 中一个非常重要的概念,核心目的就是优化性能,避免 React 组件因为不相关的状态变化而进行不必要的重新渲染 (re-render)

为什么需要选择器?

回顾一下,如果你像这样使用 Zustand Hook:

js 复制代码
import useCounterStore from '../store/counterStore';

function MyComponent() {
  const store = useCounterStore(); // 获取整个 store 对象

  // ... 使用 store.count, store.increment 等
}
    

这种方式很简单,但是有一个潜在的性能问题:只要 useCounterStore 中的任何状态(count, user.name, user.age 等)发生变化,MyComponent 都会重新渲染。即使 MyComponent 可能只用到了 count,但当 user.name 改变时,它还是会刷新。对于复杂的应用和 Store,这会导致大量的"无意义刷新"。

选择器的作用

选择器是一个函数,你把它作为参数传递给 Zustand Hook。这个函数接收完整的 state 对象,并返回你组件真正需要的那部分数据。

Zustand 会比较选择器上一次返回的值这一次返回的值 。只有当这两个值不严格相等 (!==) 时,Zustand 才会通知 React 组件进行重新渲染。

基本用法:选择单个值

这是最常见也是最推荐的用法。

js 复制代码
import useCounterStore from '../store/counterStore';

// --- 组件 A: 只关心 count ---
function CounterDisplay() {
  // 使用选择器只订阅 count 的变化
  const count = useCounterStore(state => state.count);
  console.log('CounterDisplay Rerendered');

  return <p>Count: {count}</p>;
}

// --- 组件 B: 只关心 user.name ---
function UserDisplay() {
  // 使用选择器只订阅 user.name 的变化
  const userName = useCounterStore(state => state.user.name);
  console.log('UserDisplay Rerendered');

  return <p>User Name: {userName}</p>;
}

// --- 组件 C: 提供操作按钮 ---
function Controls() {
  // 选择 action。Action 函数引用通常是稳定的,选择它们不会引起不必要的渲染。
  const increment = useCounterStore(state => state.increment);
  const setUserName = useCounterStore(state => state.setUserName);
  console.log('Controls Rerendered (likely only once on mount)');

  const handleSetName = () => {
    const newName = prompt("输入新名字:");
    if (newName) {
      setUserName(newName);
    }
  };

  return (
    <div>
      <button onClick={increment}>Increment Count</button>
      <button onClick={handleSetName}>Set User Name</button>
    </div>
  );
}

// --- 父组件 ---
function AppSelectors() {
  return (
    <div>
      <h1>Zustand Selectors</h1>
      <CounterDisplay />
      <UserDisplay />
      <Controls />
    </div>
  );
}

export default AppSelectors;
    

运行效果分析:

  1. 点击 "Increment Count" 按钮:

    • counterStore 中的 count 状态改变。
    • CounterDisplay 的选择器 state => state.count 返回了新的值。
    • CounterDisplay 会重新渲染 (控制台输出 "CounterDisplay Rerendered")。
    • UserDisplay 的选择器 state => state.user.name 返回的值没有变。
    • UserDisplay 不会重新渲染。
    • Controls 选择的是 actions,它们没变,所以 Controls 不会因为 count 改变而重新渲染。
  2. 点击 "Set User Name" 按钮并输入新名字:

    • counterStore 中的 user.name 状态改变。
    • CounterDisplay 的选择器 state => state.count 返回的值没有变。
    • CounterDisplay 不会重新渲染。
    • UserDisplay 的选择器 state => state.user.name 返回了新的值。
    • UserDisplay 会重新渲染 (控制台输出 "UserDisplay Rerendered")。
    • Controls 不会因为 user.name 改变而重新渲染。

选择多个值:shallow 优化

有时,一个组件可能需要 Store 中的多个值。考虑这种情况:

js 复制代码
function CombinedDisplay() {
  // ⚠️ 潜在问题:每次渲染都会创建一个新对象,即使 count 和 name 没变
  const data = useCounterStore(state => ({
    count: state.count,
    name: state.user.name
  }));
  console.log('CombinedDisplay Rerendered (Potentially too often!)');

  return (
    <div>
      <p>Combined Count: {data.count}</p>
      <p>Combined Name: {data.name}</p>
    </div>
  );
}
    

问题在哪里?

选择器 state => ({ count: state.count, name: state.user.name }) 在每次 组件渲染(或 Store 更新)时都会返回一个全新的对象 {...}。即使 count 和 user.name 的值本身没有改变,新旧对象引用也不同 ({} !== {})。根据 Zustand 的默认 === 比较规则,这会导致组件在任何 Store 状态变化时都重新渲染,失去了选择器的优化效果!

解决方案:使用 shallow

Zustand 提供了一个 shallow 比较函数来解决这个问题。你需要从 zustand/shallow 导入它,并将其作为第二个参数传递给 Hook。

js 复制代码
import useCounterStore from '../store/counterStore';
import { shallow } from 'zustand/shallow'; // 👈 导入 shallow

function CombinedDisplayOptimized() {
  // ✅ 使用 shallow 进行浅比较
  const data = useCounterStore(
    state => ({
      count: state.count,
      name: state.user.name,
    }),
    shallow // 👈 使用 shallow 作为比较函数
  );
  console.log('CombinedDisplayOptimized Rerendered (Only when count or name changes)');

  return (
    <div>
      <p>Combined Count: {data.count}</p>
      <p>Combined Name: {data.name}</p>
    </div>
  );
}

// --- 包含 Controls 和 CombinedDisplayOptimized 的父组件 ---
function AppSelectorsWithShallow() {
   // ... (可以复用上面的 Controls 组件)
   const Controls = () => { /* ... 同上 ... */ };
  return (
    <div>
      <h1>Zustand Selectors with Shallow</h1>
      <CombinedDisplayOptimized />
      <Controls /> {/* 复用上面的 Controls 组件 */}
    </div>
  )
}
    
shallow 如何工作?

shallow 函数执行浅层比较

  1. 它检查选择器返回的对象(或数组)的顶层属性(或元素)。
  2. 如果新旧对象的所有 顶层属性(按 key 比较)都使用 === 严格相等,那么 shallow 认为这两个对象是相等的,组件不会重新渲染。
  3. 只要有一个 顶层属性的值发生了变化 (!==),shallow 就认为对象不相等,组件重新渲染。

在 CombinedDisplayOptimized 例子中:

  • 如果只有 count 改变,shallow 比较会发现 count 的值不同,触发渲染。
  • 如果只有 user.name 改变,shallow 比较会发现 name 的值不同,触发渲染。
  • 如果 Store 中的其他状态(比如 user.age,它没有被选择)改变,但 count 和 user.name 都没变,shallow 比较会认为新旧对象 { count: ..., name: ... } 是相等的,不会触发渲染。
总结与最佳实践
  1. 优先使用选择器:避免直接获取整个 Store 对象 (useMyStore()),除非组件确实依赖 Store 的大部分内容或者 Store 非常小。

  2. 精细化选择:让选择器只返回组件渲染所必需的最少数据。state => state.value 通常比 state => state 好。

  3. 选择多个值时使用 shallow:当你需要从 Store 中提取多个原始值或稳定引用(如 actions)到一个对象或数组中时,请使用 shallow 作为第二个参数进行优化。

    js 复制代码
    const { val1, val2, action1 } = useMyStore(state => ({
      val1: state.val1,
      val2: state.val2,
      action1: state.action1
    }), shallow);
        
  4. 避免在选择器中创建新数组/对象(除非使用 shallow) :如果你不使用 shallow,state => [state.a, state.b] 或 state => ({ a: state.a, b: state.b }) 会导致不必要的渲染,因为每次都会返回新的数组/对象引用。

  5. 选择 Action 通常是安全的:Action 函数的引用一般在 Store 初始化后就不会改变,所以 useMyStore(state => state.myAction) 通常不会引起额外渲染。

掌握选择器及其 shallow 优化是高效使用 Zustand 的关键,它能显著提升应用的性能,特别是在状态逻辑变得复杂时。

3. zustand 的 Middleware (中间件)

中间件是 Zustand 提供的一种扩展机制,它允许你在 Store 创建过程中或者在状态更新前后注入额外的逻辑。它们通常以函数的形式提供,用来包裹你的 Store 定义函数。


1. devtools 中间件:连接 Redux DevTools

devtools 中间件可以将你的 Zustand Store 连接到强大的 Redux DevTools 浏览器扩展程序。这使得调试状态变化、查看 Action 日志和进行时间旅行调试变得非常方便。

使用步骤:

  1. 安装 Redux DevTools 扩展:确保你的浏览器(Chrome, Firefox, Edge等)已经安装了 "Redux DevTools" 扩展。
  2. 安装依赖:devtools 中间件是 Zustand 内置的,无需额外安装。
  3. 在 create 中使用 devtools
js 复制代码
// src/store/counterStoreWithDevtools.js
import { create } from 'zustand';
import { devtools } from 'zustand/middleware'; // 👈 1. 导入 devtools

// 定义基础的 store 逻辑 (和之前一样)
const storeDefinition = (set, get) => ({
  count: 0,
  user: { name: '游客', age: 0 },
  increment: () => set((state) => ({ count: state.count + 1 })),
  decrement: () => set((state) => ({ count: state.count - 1 })),
  setUserName: (newName) => set((state) => ({
    user: { ...state.user, name: newName }
  })),
});

// 👇 2. 使用 devtools 包裹你的 store 定义函数
// devtools 接收两个参数:
//   - 第一个参数: 你的 store 定义函数 (set, get) => ({...})
//   - 第二个参数 (可选): 配置对象
const useCounterStoreWithDevtools = create(
  devtools(
    storeDefinition,
    {
      name: 'CounterStore', // 👈 在 Redux DevTools 中显示的 Store 名称 (推荐设置!)
      // enabled: process.env.NODE_ENV === 'development', // 只在开发环境启用 (可选)
      // 其他配置...
    }
  )
);

export default useCounterStoreWithDevtools;
    

如何查看效果:

  1. 在你的 React 应用中使用 useCounterStoreWithDevtools。
  2. 打开浏览器的开发者工具,切换到 "Redux" 标签页。
  3. 你应该能看到名为 "CounterStore" (或者你设置的名字) 的实例。
  4. 当你在应用中触发 increment, decrement, setUserName 等操作时,你会在 Redux DevTools 中看到对应的 Action 日志。
  5. 你可以点击 Action 查看状态变化前后的快照 (Diff)。
  6. 你甚至可以使用时间旅行功能回溯到之前的状态。
devtools 的好处:
  • 极大地简化了状态调试过程。
  • 提供了清晰的状态变更历史记录。
  • 支持时间旅行调试。

2. persist 中间件:状态持久化

persist 中间件可以自动将你的 Store 状态保存到某种存储中(最常见的是 localStorage 或 sessionStorage),并在应用加载时自动恢复(Rehydrate)状态。

使用步骤:

  1. 安装依赖:persist 也是 Zustand 内置的。
  2. 在 create 中使用 persist
js 复制代码
// src/store/persistentCounterStore.js
import { create } from 'zustand';
import { persist, createJSONStorage } from 'zustand/middleware'; // 👈 1. 导入 persist 和 storage 工厂

const storeDefinition = (set, get) => ({
  count: 0,
  theme: 'light', // 假设我们想持久化主题设置
  // 注意:默认情况下,persist 会尝试持久化所有状态
  // user 对象也会被尝试持久化
  user: { name: '游客', age: 0 },

  increment: () => set((state) => ({ count: state.count + 1 })),
  toggleTheme: () => set((state) => ({ theme: state.theme === 'light' ? 'dark' : 'light' })),
  setUserName: (newName) => set((state) => ({
    user: { ...state.user, name: newName }
  })),
  // 有些状态你可能不想持久化,比如临时的加载状态
  isLoading: false,
  setLoading: (loading) => set({ isLoading: loading }),
});

// 👇 2. 使用 persist 包裹你的 store 定义函数
// persist 接收两个参数:
//   - 第一个参数: 你的 store 定义函数 (set, get) => ({...})
//   - 第二个参数: 配置对象 (!!! name 是必填项 !!!)
const usePersistentCounterStore = create(
  persist(
    storeDefinition,
    {
      name: 'counter-storage', // 👈 存储的 key 名称 (必须提供且唯一!)

      // storage: 指定存储方式 (可选, 默认是 localStorage)
      // 使用 sessionStorage: createJSONStorage(() => sessionStorage)
      // 使用 IndexedDB 或其他异步存储,需要适配器,查阅文档
      storage: createJSONStorage(() => localStorage), // (默认值)

      // partialize: 选择性持久化 (可选)
      // 只持久化 count 和 theme,忽略 user 和 isLoading
      partialize: (state) => ({
        count: state.count,
        theme: state.theme,
        // user: state.user // 如果也想持久化 user,就包含它
      }),

      // version: 版本号 (可选, 用于数据迁移)
      // version: 1,

      // migrate: 数据迁移函数 (可选, 和 version 配合使用)
      // migrate: (persistedState, version) => { ... return migratedState },

      // onRehydrateStorage: Rehydrate 开始前的回调 (可选)
      // onRehydrateStorage: (state) => { console.log('Hydration starts'); return (state, error) => { if(error) ... }},
    }
  )
);

export default usePersistentCounterStore;
    
如何查看效果:
  1. 在你的 React 应用中使用 usePersistentCounterStore。
  2. 进行一些操作,比如增加 count 或切换 theme。
  3. 打开浏览器的开发者工具,切换到 "Application" (或 "存储") 标签页。
  4. 在 localStorage (或你选择的 storage) 中,你应该能找到一个名为 counter-storage (或你设置的 name) 的条目,其值是一个 JSON 字符串,包含了你选择持久化的状态(根据 partialize 的设置)。
  5. 刷新页面
  6. 你会发现 count 和 theme 的状态被恢复了,而不是回到初始值!user.name 和 isLoading (如果没在 partialize 中包含)则会回到初始值。

persist 的关键配置:

  • name: 必需。在存储中使用的唯一键。
  • storage: 指定存储引擎,默认为 localStorage。createJSONStorage(() => sessionStorage) 用于 sessionStorage。也支持异步存储。
  • partialize: 非常有用。一个函数 (state) => partialState,用于精确控制哪些状态需要被持久化,避免存储不必要或敏感的数据。
  • version / migrate: 用于处理应用版本更新时状态结构的变化。

3. 组合使用中间件

你可以同时使用多个中间件,只需将它们嵌套包裹起来即可。

推荐顺序:devtools 在最外层,persist 在内层。 这样 Redux DevTools 也能监听到 persist 中间件本身触发的一些内部 Action(如 HYDRATE)。

js 复制代码
// src/store/combinedStore.js
import { create } from 'zustand';
import { devtools, persist, createJSONStorage } from 'zustand/middleware';

const storeDefinition = (set, get) => ({
  count: 0,
  theme: 'light',
  increment: () => set((state) => ({ count: state.count + 1 }), false, 'INCREMENT_ACTION'), // devtools 可以显示 action 类型
  toggleTheme: () => set((state) => ({ theme: state.theme === 'light' ? 'dark' : 'light' })),
});

const useCombinedStore = create(
  // 1. 最外层是 devtools
  devtools(
    // 2. 内层是 persist
    persist(
      storeDefinition,
      {
        name: 'combined-storage', // persist 的配置
        storage: createJSONStorage(() => localStorage),
        partialize: (state) => ({ count: state.count, theme: state.theme }),
      }
    ),
    {
      name: 'CombinedStore', // devtools 的配置
    }
  )
);

export default useCombinedStore;
    
总结:
  • 中间件 (Middleware) 提供了一种干净的方式来为 Zustand Store 添加横切关注点的功能。
  • devtools 连接 Redux DevTools,是开发和调试的利器。记得给 Store 起个 name。
  • persist 轻松实现状态持久化,常用于保存用户偏好、购物车等。name 属性是必需的,partialize 用于选择性持久化。
  • 中间件可以组合使用,推荐将 devtools 放在最外层。

通过使用这些中间件,你可以大大增强 Zustand Store 的功能和可维护性。

zustand进阶

4. 分模块 store 大型项目可以把 store 分成多个文件

好的,在大型项目中,将 Zustand Store 拆分成多个模块(或称为 "slices")是一种非常常见的实践,这有助于:

  1. 组织性 (Organization): 按功能或领域划分状态逻辑,使代码库更清晰、更易于导航。
  2. 可维护性 (Maintainability): 修改或调试特定功能的状态时,只需关注相关的文件。
  3. 可扩展性 (Scalability): 添加新功能时,可以创建新的模块文件,而不会让单个 Store 文件变得臃肿。
  4. 团队协作 (Collaboration): 不同开发者可以并行开发不同的状态模块,减少冲突。

Zustand 非常灵活,并没有规定 必须 如何拆分,但主要有两种流行的方式:

方式一:创建多个独立的 Store (最常见、推荐)

这是最简单直接的方式。每个功能模块都有自己完全独立的 Store,通过各自的 create 函数创建。

js 复制代码
// src/store/userStore.js
import { create } from 'zustand';
import { devtools } from 'zustand/middleware';

const useUserStore = create(devtools((set) => ({
  userProfile: null,
  isLoggedIn: false,
  login: (profile) => set({ userProfile: profile, isLoggedIn: true }, false, 'USER_LOGIN'),
  logout: () => set({ userProfile: null, isLoggedIn: false }, false, 'USER_LOGOUT'),
  fetchUserProfile: async () => {
    // const profile = await api.fetchUser();
    // set({ userProfile: profile, isLoggedIn: true });
    console.log('Fetching user profile...');
    // 模拟异步获取
    setTimeout(() => {
      set({ userProfile: { id: 1, name: 'Alice' }, isLoggedIn: true }, false, 'FETCH_USER_SUCCESS');
    }, 500);
  }
}), { name: 'UserStore' }));

export default useUserStore;

// ---

// src/store/cartStore.js
import { create } from 'zustand';
import { devtools, persist } from 'zustand/middleware';

const useCartStore = create(devtools(persist((set, get) => ({
  items: [], // { id, name, quantity }
  addItem: (item) => set((state) => {
    const existingItem = state.items.find(i => i.id === item.id);
    if (existingItem) {
      return {
        items: state.items.map(i =>
          i.id === item.id ? { ...i, quantity: i.quantity + 1 } : i
        ),
      };
    } else {
      return { items: [...state.items, { ...item, quantity: 1 }] };
    }
  }, false, 'ADD_ITEM_TO_CART'),
  removeItem: (itemId) => set((state) => ({
    items: state.items.filter(i => i.id !== itemId)
  }), false, 'REMOVE_ITEM_FROM_CART'),
  getCartTotal: () => {
    // get() 可以在 action 内部读取当前状态
    const items = get().items;
    return items.reduce((total, item) => total + item.quantity, 0);
  }
}), { name: 'CartStore', partialize: state => ({ items: state.items }) }), { name: 'CartStoreDevtools' }));

export default useCartStore;

// ---

// src/components/UserProfile.jsx
import React, { useEffect } from 'react';
import useUserStore from '../store/userStore';

function UserProfile() {
  // 从 userStore 获取状态和 action
  const { userProfile, isLoggedIn, login, logout, fetchUserProfile } = useUserStore();

  useEffect(() => {
    if (!isLoggedIn && !userProfile) {
        // 可以在组件加载时触发 action
        // fetchUserProfile(); // 假设在别处触发登录
    }
  }, [isLoggedIn, userProfile, fetchUserProfile]);

  return (
    <div>
      <h2>User Profile</h2>
      {isLoggedIn ? (
        <div>
          <p>Name: {userProfile?.name}</p>
          <button onClick={logout}>Logout</button>
        </div>
      ) : (
        <button onClick={() => login({ id: 1, name: 'Alice' })}>Login</button>
      )}
    </div>
  );
}

export default UserProfile;

// ---

// src/components/ShoppingCart.jsx
import React from 'react';
import useCartStore from '../store/cartStore';
import { shallow } from 'zustand/shallow';

function ShoppingCart() {
  // 从 cartStore 获取状态和 action
  // 使用 shallow 优化选择多个值
  const { items, addItem, removeItem, getCartTotal } = useCartStore(
    state => ({
      items: state.items,
      addItem: state.addItem,
      removeItem: state.removeItem,
      getCartTotal: state.getCartTotal, // 注意:如果 getCartTotal 只是读取,可以不选它
    }),
    shallow
  );

  const totalItems = getCartTotal(); // 调用 store 中的计算方法

  return (
    <div>
      <h2>Shopping Cart ({totalItems} items)</h2>
      <ul>
        {items.map(item => (
          <li key={item.id}>
            {item.name} - Quantity: {item.quantity}
            <button onClick={() => removeItem(item.id)} style={{marginLeft: '10px'}}>Remove</button>
          </li>
        ))}
      </ul>
      <button onClick={() => addItem({ id: Date.now(), name: `Product ${items.length + 1}` })}>
        Add Random Item
      </button>
    </div>
  );
}
export default ShoppingCart;

// ---

// src/App.jsx
import React from 'react';
import UserProfile from './components/UserProfile';
import ShoppingCart from './components/ShoppingCart';

function App() {
  return (
    <div>
      <h1>My Zustand App with Modules</h1>
      <UserProfile />
      <hr />
      <ShoppingCart />
    </div>
  );
}

export default App;
    

优点:

  • 简单直观: 每个模块是一个独立的 Hook,易于理解和使用。
  • 强隔离: 不同模块的状态默认是完全隔离的,减少了意外相互影响的风险。
  • 类型安全: TypeScript 类型定义相对简单,每个 Store 有自己的类型。
  • 按需加载: 如果配合代码分割,可以实现只加载特定功能模块的 Store。
缺点/注意事项:
  • 跨 Store 交互: 如果一个 Store 的 Action 需要直接读取或更新另一个 Store 的状态,会稍微麻烦一些。常见方法:

    • 在 Action 中调用另一个 Store 的 getState() 方法 (需要先导入另一个 Store)。
    • 通过组件作为中介,读取一个 Store 的状态,然后调用另一个 Store 的 Action。
    • 创建一个订阅函数或使用 subscribe 监听另一个 Store 的变化。
    • 对于非常紧密的交互,可能方式二更合适。
  • 你需要分别导入和使用不同的 Store Hook (useUserStore, useCartStore)。

方式二:创建 Slice 并组合成单个 Store (类似 Redux)

这种方式下,你为每个功能模块定义一个 "Slice"。Slice 本身不是一个完整的 Store,而是一个函数,它接收 set 和 get (以及可能的其他参数),并返回该模块的状态片段和相关的 Action。最后,你在一个中心文件中将所有 Slice 组合起来创建一个全局 Store。

js 复制代码
 // src/store/slices/userSlice.js
// Slice 是一个函数,返回状态片段和 actions
export const createUserSlice = (set, get) => ({
  userProfile: null,
  isLoggedIn: false,
  login: (profile) => set({ userProfile: profile, isLoggedIn: true }, false, 'USER_LOGIN'),
  logout: () => set({ userProfile: null, isLoggedIn: false }, false, 'USER_LOGOUT'),
  // 注意:如果需要访问其他 slice 的状态,可以使用 get()
  // 比如:get().cart.items
});

// ---

// src/store/slices/cartSlice.js
export const createCartSlice = (set, get) => ({
  items: [],
  addItem: (item) => set((state) => {
      // 在组合后的 store 中,state 包含了所有 slice 的状态
      // 但这里 set 只会合并到 cart slice 对应的部分 (如果正确组合)
      // 所以可以直接操作 state.items (假设组合后 items 在顶层)
      // 或者更安全地通过 get() 获取 cart 自己的 state
      // const currentItems = get().cart.items; // 需要知道组合后的结构
       const currentItems = get().items; // 假设 items 直接在顶层
      const existingItem = currentItems.find(i => i.id === item.id);
      if (existingItem) {
          return { items: currentItems.map(i => i.id === item.id ? { ...i, quantity: i.quantity + 1 } : i) };
      } else {
          return { items: [...currentItems, { ...item, quantity: 1 }] };
      }
  }, false, 'ADD_ITEM_TO_CART'),

  removeItem: (itemId) => set((state) => ({
    items: get().items.filter(i => i.id !== itemId)
  }), false, 'REMOVE_ITEM_FROM_CART'),

  // 演示如何访问其他 slice 的状态
  checkout: () => {
    const user = get().userProfile; // 访问 userSlice 的状态
    const cartItems = get().items; // 访问 cartSlice 自己的状态
    if (user && cartItems.length > 0) {
        console.log(`User ${user.name} is checking out with ${cartItems.length} items.`);
        // ... 调用 API
        set({ items: [] }, false, 'CHECKOUT_SUCCESS'); // 清空购物车
    } else {
        console.log('Cannot checkout.');
    }
  }
});

// ---

// src/store/appStore.js
import { create } from 'zustand';
import { devtools, persist } from 'zustand/middleware';
import { createUserSlice } from './slices/userSlice';
import { createCartSlice } from './slices/cartSlice';

// 👇 组合所有 slices 来创建单个 store
const useAppStore = create(
  devtools(
    persist(
      (set, get, api) => ({ // 'api' 参数可以访问 store 实例本身
        // 使用扩展运算符 (...) 将每个 slice 返回的对象合并到根状态
        ...createUserSlice(set, get, api),
        ...createCartSlice(set, get, api),
        // 你可以在这里添加不属于任何特定 slice 的全局状态或 action
        // globalSetting: 'abc',
        // resetAll: () => {
        //   // 需要调用每个 slice 的重置逻辑或直接 set 初始状态
        // }
      }),
      {
        name: 'app-storage',
        // 在 persist 中选择性持久化时,需要指定完整的路径
        partialize: (state) => ({
          // 假设 user 和 cart 状态都在顶层
          userProfile: state.userProfile,
          isLoggedIn: state.isLoggedIn,
          items: state.items,
        }),
      }
    ),
    { name: 'AppStore' }
  )
);

export default useAppStore;

// ---

// src/components/UserProfileSlice.jsx
import React from 'react';
import useAppStore from '../store/appStore';
import { shallow } from 'zustand/shallow';

function UserProfileSlice() {
  // 从 *单个* AppStore 中选择 user 相关的状态和 action
  const { userProfile, isLoggedIn, login, logout } = useAppStore(
    state => ({
      userProfile: state.userProfile,
      isLoggedIn: state.isLoggedIn,
      login: state.login,
      logout: state.logout,
    }),
    shallow // 推荐使用 shallow
  );

  return (
     <div>
      <h2>User Profile (Single Store)</h2>
      {isLoggedIn ? (
        <div>
          <p>Name: {userProfile?.name}</p>
          <button onClick={logout}>Logout</button>
        </div>
      ) : (
        <button onClick={() => login({ id: 1, name: 'Alice' })}>Login</button>
      )}
    </div>
  );
}

export default UserProfileSlice;

// ---

// src/components/ShoppingCartSlice.jsx
import React from 'react';
import useAppStore from '../store/appStore';
import { shallow } from 'zustand/shallow';

function ShoppingCartSlice() {
    // 从 *单个* AppStore 中选择 cart 相关的状态和 action
  const { items, addItem, removeItem, checkout } = useAppStore(
    state => ({
      items: state.items,
      addItem: state.addItem,
      removeItem: state.removeItem,
      checkout: state.checkout, // 获取跨 slice 操作的 action
    }),
    shallow
  );

  return (
      <div>
        <h2>Shopping Cart (Single Store - {items.length} items)</h2>
        <ul>
            {items.map(item => (
            <li key={item.id}>
                {item.name} - Quantity: {item.quantity}
                <button onClick={() => removeItem(item.id)} style={{marginLeft: '10px'}}>Remove</button>
            </li>
            ))}
        </ul>
        <button onClick={() => addItem({ id: Date.now(), name: `Product ${items.length + 1}` })}>
            Add Random Item
        </button>
        <button onClick={checkout} style={{marginLeft: '10px'}}>Checkout</button> {/* 调用 checkout action */}
    </div>
  );
}
export default ShoppingCartSlice;

// ---

// src/AppSlice.jsx (使用 Slice 版本的组件)
import React from 'react';
import UserProfileSlice from './components/UserProfileSlice';
import ShoppingCartSlice from './components/ShoppingCartSlice';

function AppSlice() {
  return (
    <div>
      <h1>My Zustand App with Single Store (Slices)</h1>
      <UserProfileSlice />
      <hr />
      <ShoppingCartSlice />
    </div>
  );
}
export default AppSlice;
    

优点:

  • 单一 Store Hook: 整个应用只需要导入和使用一个 Store Hook (useAppStore)。
  • 简化跨 Slice 交互: 在 Action 内部使用 get() 可以直接访问其他 Slice 的状态,因为它们都在同一个全局 State 对象中。
  • 原子更新 (Potentially): 如果一个 Action 需要同时更新多个 Slice 的状态,可以在一次 set 调用中完成(尽管通常 set 会合并,效果类似)。
  • 概念统一: 对于习惯了 Redux 等单一 Store 模式的开发者可能更熟悉。

缺点/注意事项:

  • 组合逻辑: 需要手动在主 Store 文件中组合 (...) 所有 Slice。

  • 命名空间/冲突: 需要确保不同 Slice 返回的状态属性和 Action 名称不冲突(或者在组合时进行处理/嵌套)。上面的例子将所有状态和 action 放在了顶层,如果 slice 很多,可能会冲突。一种解决方法是在组合时手动添加命名空间:

    js 复制代码
    // appStore.js
    (set, get, api) => ({
        user: createUserSlice(set, get, api), // 状态会是 state.user.userProfile
        cart: createCartSlice(set, get, api), // 状态会是 state.cart.items
        // ...
    })
    // 这样在 slice 内部 get() 就需要写 get().user.isLoggedIn
    // 在组件选择器中就是 state => state.user.isLoggedIn
        
  • 类型定义: 组合后的 Store 类型定义可能比多个独立 Store 更复杂一些,特别是手动添加命名空间时。

  • persist 配置: 在 partialize 中需要指定完整路径(例如 user.userProfile 或 cart.items,取决于你的组合方式)。

如何选择?
  • 对于大多数应用,尤其是功能模块相对独立的应用,推荐使用方式一(多个独立 Store)。 它更简单、隔离性更好,符合 Zustand 轻量、非侵入式的哲学。
  • 如果你的应用模块之间存在大量、紧密的状态交互,或者你非常偏好 Redux 的单一 Store 概念,可以考虑方式二(组合 Slice)。 但要注意组合逻辑和潜在的命名冲突。

无论选择哪种方式,将 Store 逻辑按功能拆分到不同文件都是大型项目中的良好实践。

5. Zustand 中结合 Immer 中间件

如何在 Zustand 中结合 Immer 中间件,以更"舒服"(即更直观、更不容易出错)的方式来管理复杂或深层嵌套的状态对象。

问题所在:更新嵌套状态的痛点

在 Zustand(以及 Redux 等遵循不可变性原则的状态管理库)中,当你需要更新一个嵌套在对象或数组内部的值时,你必须确保不直接修改原始状态。你需要使用展开运算符 (...) 来创建每一层嵌套结构的副本,直到你到达需要修改的目标属性。

示例:没有 Immer 的情况

假设你的状态结构如下:

js 复制代码
 {
  user: {
    id: 1,
    profile: {
      name: '张三',
      email: '[email protected]',
      preferences: {
        theme: 'light',
        notifications: {
          email: true,
          push: false,
        }
      }
    },
    roles: ['editor', 'viewer']
  },
  posts: [
    { id: 101, title: '第一篇文章' }
  ]
}
    

现在,如果你想执行以下操作:

  1. 更改主题 (theme)
  2. 添加一个新角色 (role)
  3. 启用推送通知 (push notifications)
使用标准的 Zustand set 方法会是这样:
js 复制代码
import { create } from 'zustand';

const useUserStore = create((set) => ({
  user: { /* ...初始状态... */ },
  posts: [ /* ...初始状态... */ ],

  // 1. 更改主题
  setTheme: (newTheme) => set((state) => ({
    ...state, // 保持其他顶级状态不变 (虽然这里只有 user 和 posts,但好习惯是展开)
    user: {
      ...state.user, // 保持 user 的其他属性不变
      profile: {
        ...state.user.profile, // 保持 profile 的其他属性不变
        preferences: {
          ...state.user.profile.preferences, // 保持 preferences 的其他属性不变
          theme: newTheme, // 更新 theme
        }
      }
    }
  })),

  // 2. 添加角色
  addRole: (newRole) => set((state) => ({
    ...state,
    user: {
      ...state.user,
      roles: [...state.user.roles, newRole], // 创建新的 roles 数组
    }
  })),

  // 3. 启用推送通知
  enablePushNotifications: () => set((state) => ({
    ...state,
    user: {
      ...state.user,
      profile: {
        ...state.user.profile,
        preferences: {
          ...state.user.profile.preferences,
          notifications: {
            ...state.user.profile.preferences.notifications, // 保持 email 通知状态
            push: true, // 更新 push 通知状态
          }
        }
      }
    }
  })),
}));
    

你可以看到,代码变得非常冗长,充满了 ... 展开操作。每深入一层,就需要多一次展开。这不仅写起来麻烦,而且很容易因为忘记某个层级的展开而意外地修改了原始状态或丢失了同级的其他数据,导致难以追踪的 bug。

解决方案:引入 Immer

Immer 是一个库,它允许你以看似"可变"的方式来编写状态更新逻辑,但它在底层会自动为你处理不可变性。它提供了一个 draft(草稿)状态,你可以像修改普通 JavaScript 对象或数组一样直接修改这个 draft。当你的修改完成后,Immer 会根据你对 draft 的操作,高效地生成一个全新的、不可变的下一状态。

Zustand 提供了一个官方的 Immer 中间件,可以轻松地将 Immer 集成到你的 store 中。

如何使用 Zustand + Immer
  1. 安装:

    bash 复制代码
    npm install immer zustand
    # 或者
    yarn add immer zustand
        
  2. 在 Store 中使用 immer 中间件:

    • 从 zustand/middleware 导入 immer。
    • 将你的 store 创建函数 (set, get, api) => ({...}) 用 immer 包裹起来。
    • 现在,在你的 action 中调用 set 时,传递给 set 的函数会接收到一个 draft 对象作为第一个参数,而不是完整的 state。你可以直接修改这个 draft。

示例:使用 Immer 后的代码

js 复制代码
import { create } from 'zustand';
import { immer } from 'zustand/middleware'; // 导入 Immer 中间件

const useUserStore = create(
  immer( // <--- 用 immer 包裹
    (set) => ({
      // --- 初始状态保持不变 ---
      user: {
        id: 1,
        profile: {
          name: '张三',
          email: '[email protected]',
          preferences: {
            theme: 'light',
            notifications: {
              email: true,
              push: false,
            }
          }
        },
        roles: ['editor', 'viewer']
      },
      posts: [
        { id: 101, title: '第一篇文章' }
      ],

      // --- Actions 现在变得非常简洁 ---

      // 1. 更改主题
      setTheme: (newTheme) => set((draft) => { // <--- 接收 draft
        draft.user.profile.preferences.theme = newTheme; // <--- 直接修改!
      }),

      // 2. 添加角色
      addRole: (newRole) => set((draft) => {
        draft.user.roles.push(newRole); // <--- 直接 push!就像普通数组一样
      }),

      // 3. 启用推送通知
      enablePushNotifications: () => set((draft) => {
        draft.user.profile.preferences.notifications.push = true; // <--- 直接赋值!
      }),

      // 如果需要访问当前状态 (get) 或 api,它们仍然可用
      logUserName: () => set((draft, get) => { // get 仍然是第二个参数
         console.log("Current user name:", draft.user.profile.name);
         // 注意:get() 返回的是当前的、不可变的 state,而不是 draft
         // console.log("From get():", get().user.profile.name);
      }),
    })
  )
);

export default useUserStore;
    

对比和优势

  • 代码简洁性: Immer 极大地减少了样板代码。更新逻辑变得像直接操作普通 JavaScript 对象一样直观。
  • 可读性: 代码意图更清晰,更容易理解你正在修改状态的哪个部分。
  • 减少错误: 消除了因忘记 ... 展开而导致的常见错误。Immer 确保了不可变性。
  • 易于维护: 对于复杂的状态结构,维护和修改更新逻辑变得更加容易。
工作原理简述

当你调用 set(draft => { ... }) 时:

  1. Immer 中间件接收到当前的不可变状态。
  2. Immer 创建一个这个状态的 "代理"(Proxy)版本,这就是 draft。
  3. 你的函数在 draft 上执行操作(赋值、push、delete 等)。这些操作看起来是直接修改,但实际上是在操作代理。
  4. Immer 记录下所有对 draft 的修改。
  5. 当你的函数执行完毕后,Immer 根据记录的修改,高效地生成一个新的不可变状态对象,只在被修改的路径上创建新的对象/数组副本,未修改的部分则保持引用不变(结构共享)。
  6. 这个最终生成的新状态被传递给 Zustand 的核心 set 函数,完成状态更新。

注意事项

  • 只修改 draft: 在传递给 set 的 Immer 函数内部,务必只修改 draft 对象。不要尝试修改从 get() 获取的状态,也不要返回任何东西(除非你想完全替换整个状态,但这通常不推荐在 Immer 中间件内这样做)。Immer 会根据你对 draft 的 副作用 来计算下一个状态。
  • 性能: Immer 非常高效,对于大多数应用来说,性能开销可以忽略不计。它使用了结构共享和写时复制(copy-on-write)的优化。
  • 心智模型: 需要理解你是在操作一个特殊的 draft 对象,而不是原始状态。

总结

对于需要管理具有嵌套结构或复杂关系的 state 的 Zustand store 来说,使用 immer 中间件是一个极佳的选择。它能显著提升代码的可读性、简洁性和可维护性,让你从繁琐的展开操作中解放出来,更专注于业务逻辑本身,从而获得更"舒服"的开发体验。

6. Zustand + React Query 数据请求用 React Query,页面状态用 Zustand

如何将 ZustandReact Query (现在叫 TanStack Query) 结合使用。这是一个非常强大且流行的模式,它充分利用了两个库各自的优势:

  • React Query (TanStack Query): 极擅长管理服务器状态 (Server State) 。这包括从 API 获取数据、缓存数据、处理加载/错误状态、后台自动更新、stale-while-revalidate(后台更新时返回旧数据)逻辑、数据变更操作 (Mutations: POST/PUT/DELETE)、乐观更新等等。它负责管理那些存在于你的客户端应用之外的数据(通常在服务器上)的异步生命周期。
  • Zustand: 极擅长管理客户端状态 (Client State) (也叫 UI 状态)。这包括像主题设置(暗黑/明亮模式)、模态框的打开/关闭状态、表单输入值(虽然有时专用的表单库更好)、选中的筛选条件、用户界面偏好等等。它负责管理那些源自并且存在于你的浏览器应用内部的状态。
为什么要结合使用?
  1. 关注点分离 (Separation of Concerns): 让你的代码库更清晰。服务器数据逻辑(获取、缓存、变更)由 React Query 的 Hooks 处理,而纯粹的客户端 UI 状态由 Zustand 的 Store 管理。
  2. 发挥各自优势: 你可以获得 React Query 为服务器数据提供的复杂缓存、后台重新获取和变更处理能力,同时又能享受到 Zustand 为客户端状态提供的简洁性、灵活性和(通过选择器实现的)高性能。
  3. 避免重复造轮子: 你不需要在 Zustand 中为来自 API 的数据手动实现缓存、加载/错误状态或重新获取逻辑。React Query 在这方面做得好得多,而且是开箱即用的。
  4. 性能: React Query 优化了网络请求和数据同步。Zustand,特别是配合选择器使用时,能确保因客户端状态变化而引起的组件重新渲染次数最少。
如何区分:经验法则

对于一个状态,问自己以下问题:

  • 它是否来自 API 或外部数据源?
  • 它是否需要缓存?
  • 它是否可能变得陈旧(stale)并需要重新获取?
  • 它是否与在服务器上创建、更新或删除数据有关?

如果对大部分问题的回答是"是",那么它是 服务器状态 (Server State) -> 使用 React Query

  • 它是否是只存在于浏览器/UI 内部的状态?
  • 它是否与用户界面控件(模态框、筛选器、主题)有关?
  • 它是否是跨组件需要的、但不会持久化到服务器的临时状态?

如果对这些问题的回答是"是",那么它是 客户端状态 (Client State) -> 使用 Zustand

示例场景:带筛选和主题的产品列表

我们来构建一个简单的例子:

  1. 从 API 获取产品列表(服务器状态 - React Query)。
  2. 允许用户按类别筛选产品(客户端状态 - Zustand)。
  3. 允许用户切换亮色/暗色主题(客户端状态 - Zustand)。
步骤 1: 设置 React Query
bash 复制代码
npm install @tanstack/react-query
# 或者
yarn add @tanstack/react-query
    
js 复制代码
// src/main.jsx 或 src/index.js (或其他应用入口文件)
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools'; // 可选,但强烈推荐

// 创建一个 client
const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      staleTime: 1000 * 60 * 5, // 数据在 5 分钟内被认为是新鲜的
      cacheTime: 1000 * 60 * 30, // 数据在 30 分钟后从缓存中移除
      retry: 1, // 失败的请求重试 1 次
    },
  },
});

ReactDOM.createRoot(document.getElementById('root')).render(
  <React.StrictMode>
    {/* 将 client 提供给你的 App */}
    <QueryClientProvider client={queryClient}>
      <App />
      <ReactQueryDevtools initialIsOpen={false} /> {/* React Query 开发者工具 */}
    </QueryClientProvider>
  </React.StrictMode>
);
    
步骤 2: 设置 Zustand Store 管理客户端状态
js 复制代码
// src/store/uiStore.js
import { create } from 'zustand';
import { devtools, persist } from 'zustand/middleware';

const useUIStore = create(
  devtools(
    persist(
      (set) => ({
        // --- 主题状态 ---
        theme: 'light',
        toggleTheme: () => set((state) => ({
          theme: state.theme === 'light' ? 'dark' : 'light'
        }), false, 'TOGGLE_THEME'), // 为 devtools 提供 action 类型

        // --- 筛选状态 ---
        selectedCategory: 'all', // 'all', 'electronics', 'clothing' 等
        setCategoryFilter: (category) => set({ selectedCategory: category }, false, 'SET_CATEGORY_FILTER'),
      }),
      {
        name: 'ui-preferences-storage', // 用于 localStorage 持久化的 key 名称
        partialize: (state) => ({ theme: state.theme, selectedCategory: state.selectedCategory }), // 只持久化主题和筛选条件
      }
    ),
    { name: 'UIStore' } // 用于 Redux DevTools 的名称
  )
);

export default useUIStore;
    
步骤 3: 使用 useQuery 获取服务器数据
js 复制代码
// src/hooks/useProducts.js
import { useQuery } from '@tanstack/react-query';

// 模拟 API 函数
const fetchProducts = async () => {
  console.log('正在从 API 获取产品...');
  // 模拟 API 延迟
  await new Promise(resolve => setTimeout(resolve, 1000));
  // 真实应用中,使用 fetch 或 axios:
  // const response = await fetch('/api/products');
  // if (!response.ok) throw new Error('网络响应失败');
  // return response.json();
  return [
    { id: 1, name: '笔记本电脑', category: 'electronics', price: 1200 },
    { id: 2, name: 'T恤', category: 'clothing', price: 25 },
    { id: 3, name: '键盘', category: 'electronics', price: 75 },
    { id: 4, name: '牛仔裤', category: 'clothing', price: 50 },
    { id: 5, name: '显示器', category: 'electronics', price: 300 },
  ];
};

export const useProducts = () => {
  return useQuery({
    queryKey: ['products'], // 此查询的唯一键
    queryFn: fetchProducts, // 获取数据的函数
    // React Query 会自动处理 isLoading, isError, data, error
  });
};
    
步骤 4: 在组件中同时使用两者
js 复制代码
 // src/components/ProductList.jsx
import React, { useMemo } from 'react';
import { useProducts } from '../hooks/useProducts'; // React Query Hook
import useUIStore from '../store/uiStore';        // Zustand Hook
import { shallow } from 'zustand/shallow';         // 用于选择多个状态片段

function ProductList() {
  // --- 从 React Query 获取服务器状态 ---
  const { data: products, isLoading, isError, error } = useProducts();

  // --- 从 Zustand 获取客户端状态/Action ---
  // 使用 shallow 比较优化选择多个值
  const { selectedCategory, setCategoryFilter, theme } = useUIStore(
    (state) => ({
      selectedCategory: state.selectedCategory,
      setCategoryFilter: state.setCategoryFilter,
      theme: state.theme,
    }),
    shallow // 重要: 防止 uiStore 中其他部分变化时引起不必要的重渲染
  );

  // --- 筛选逻辑 (派生状态) ---
  // useMemo 仅在 products 或 selectedCategory 变化时重新计算 filteredProducts
  const filteredProducts = useMemo(() => {
    if (!products) return [];
    if (selectedCategory === 'all') {
      return products;
    }
    return products.filter(p => p.category === selectedCategory);
  }, [products, selectedCategory]); // useMemo 的依赖项

  // --- 渲染逻辑 ---
  if (isLoading) {
    return <div>正在加载产品...</div>;
  }

  if (isError) {
    return <div>获取产品出错: {error.message}</div>;
  }

  // 获取唯一的分类用于筛选下拉框
  const categories = useMemo(() => {
      if (!products) return [];
      return ['all', ...new Set(products.map(p => p.category))];
  }, [products]);


  return (
    // 应用主题类名
    <div className={`product-list theme-${theme}`}>
      <h2>产品列表</h2>

      {/* 筛选控件 (使用 Zustand 状态/Action) */}
      <div>
        <label htmlFor="category-filter">按类别筛选: </label>
        <select
          id="category-filter"
          value={selectedCategory}
          onChange={(e) => setCategoryFilter(e.target.value)}
        >
          {categories.map(cat => (
              <option key={cat} value={cat}>{cat === 'all' ? '全部' : cat}</option>
          ))}
        </select>
      </div>

      {/* 产品展示 (使用 React Query 数据,已筛选) */}
      <ul>
        {filteredProducts.map((product) => (
          <li key={product.id}>
            {product.name} ({product.category}) - ${product.price}
          </li>
        ))}
        {filteredProducts.length === 0 && <li>该分类下没有产品。</li>}
      </ul>
      {/* 使用 style jsx 或其他 css 方案定义样式 */}
      <style jsx>{`
        .theme-light { background-color: #fff; color: #333; }
        .theme-dark { background-color: #333; color: #fff; }
        .product-list { padding: 20px; border: 1px solid #ccc; margin-top: 10px; transition: background-color 0.3s, color 0.3s; }
        select { margin-left: 10px; padding: 5px;}
        ul { list-style: none; padding: 0; margin-top: 15px; }
        li { margin-bottom: 8px; }
      `}</style>
    </div>
  );
}


// src/App.jsx
import React from 'react';
import ProductList from './components/ProductList';
import useUIStore from './store/uiStore'; // 引入 Zustand store

function ThemeToggler() {
    // 从 Zustand 获取主题状态和切换函数
    const { theme, toggleTheme } = useUIStore(state => ({
        theme: state.theme,
        toggleTheme: state.toggleTheme
    }), shallow); // 使用 shallow 优化

    return (
         <button onClick={toggleTheme}>
            切换到 {theme === 'light' ? '暗黑' : '明亮'} 主题
        </button>
    )
}


function App() {
  return (
    <div>
      <h1>我的神奇商店</h1>
      <ThemeToggler />
      <ProductList />
    </div>
  );
}

export default App;
    

代码解释:

  1. ProductList 组件: 同时导入了 useProducts (React Query) 和 useUIStore (Zustand)。

  2. 获取数据: useProducts() 负责处理产品列表的获取、缓存、加载和错误状态。组件只需消费 data, isLoading, isError。

  3. 客户端状态管理: useUIStore 提供了 selectedCategory, setCategoryFilter, 和 theme。我们在选择多个状态时使用了 shallow 来优化重渲染。

  4. 筛选: filteredProducts 使用 useMemo 计算得出。这个计算同时依赖服务器状态 (products) 和客户端状态 (selectedCategory)。

  5. UI 交互: <select> 下拉框读取 Zustand 中的 selectedCategory,并在用户改变选项时调用 setCategoryFilter (也来自 Zustand)。主题类名根据 Zustand 中的 theme 状态应用。

  6. 持久化: 用户选择的主题和筛选条件会被保存到 localStorage(得益于 persist 中间件),并在页面重新加载时恢复。

  7. 开发者工具: React Query DevTools 和 Redux DevTools(通过 Zustand 的 devtools 中间件连接)可以同时使用,分别检查服务器状态和客户端状态。

这种模式提供了一种清晰、健壮且高性能的方式来管理 React 应用中不同类型的状态

相关推荐
亭台烟雨中5 分钟前
【前端记事】关于electron的入门使用
前端·javascript·electron
泯泷19 分钟前
「译」解析 JavaScript 中的循环依赖
前端·javascript·架构
抹茶san22 分钟前
前端实战:从 0 开始搭建 pnpm 单一仓库(1)
前端·架构
Senar1 小时前
Web端选择本地文件的几种方式
前端·javascript·html
烛阴1 小时前
UV Coordinates & Uniforms -- OpenGL UV坐标和Uniform变量
前端·webgl
姑苏洛言1 小时前
扫码小程序实现仓库进销存管理中遇到的问题 setStorageSync 存储大小限制错误解决方案
前端·后端
烛阴1 小时前
JavaScript 的 8 大“阴间陷阱”,你绝对踩过!99% 程序员崩溃瞬间
前端·javascript·面试
lh_12542 小时前
ECharts 地图开发入门
前端·javascript·echarts