当前位置: 首页 > article >正文

Zustand 状态管理:从入门到实践

Zustand 状态管理:从入门到实践

Zustand 是一个轻量、快速且灵活的 React 状态管理库。它基于 Hooks API,提供了简洁的接口来创建和使用状态,同时易于扩展和优化。本文将通过一个 TODO 应用实例带你快速入门 Zustand,并探讨其核心概念、性能优化技巧以及一些高级用法。

快速入门:构建一个 TODO 应用

让我们通过实现一个经典的 TODO 应用来熟悉 Zustand 的基本用法。

1. 安装

首先,你需要安装 Zustand:

# 使用 npm
npm install zustand# 或者使用 yarn
yarn add zustand

2. 创建 Store

接下来,我们使用 Zustand 的 create 函数来定义我们的状态存储(Store)。Store 包含了应用所需的状态以及更新这些状态的方法。

import { create } from 'zustand';// 定义 Todo 项的类型 (可选,但推荐使用 TypeScript)
interface Todo {id: number;title: string;completed: boolean;
}// 定义 Store 的状态和操作类型 (可选)
interface TodoState {filter: 'all' | 'completed' | 'incompleted';todos: Todo[];setFilter: (filter: TodoState['filter']) => void;setTodos: (fn: (prevTodos: Todo[]) => Todo[]) => void;// 如果需要添加 Todo 的方法,也可以在这里定义// addTodo: (title: string) => void;
}// 用于生成唯一 ID (简化示例)
let keyCount = 0;const useStore = create<TodoState>((set) => ({// --- 状态 (State) ---filter: 'all', // 当前筛选条件,默认为 'all'todos: [],     // Todo 列表,初始为空数组// --- 操作 (Actions) ---// 设置筛选条件setFilter(filter) {// set 函数用于更新状态,它接收一个对象,包含要更新的部分状态set({ filter });},// 更新 Todo 列表setTodos(fn) {// set 函数也可以接收一个函数,参数是当前状态 (prev)// 这对于基于先前状态进行更新非常有用,可以避免竞态条件set((prev) => ({ todos: fn(prev.todos) }));},// 示例:添加 Todo 的 Action (可以直接在 Store 中定义)// addTodo(title) {//   set((prev) => ({//     todos: [//       ...prev.todos,//       { title, completed: false, id: keyCount++ },//     ],//   }));// },
}));export default useStore;

在这个 Store 中,我们定义了:

  • filter: 代表当前的筛选选项(all - 全部, completed - 已完成, incompleted - 未完成)。
  • todos: 一个数组,存储所有的代办事项对象。每个对象包含 id (唯一标识), title (事项名称) 和 completed (是否完成)。
  • setFilter: 一个 Action (操作),用于修改 filter 状态。
  • setTodos: 一个 Action,用于修改 todos 状态。它接收一个函数作为参数,该函数接收当前的 todos 数组并返回新的 todos 数组,确保状态更新的原子性和安全性。

3. 构建 React 组件

现在我们来构建构成 TODO 应用界面的 React 组件。整个应用大致分为三块:输入和过滤控制区、Todo 列表展示区。

[图片:TODO 应用界面截图 - 展示输入框、过滤按钮和 Todo 列表]

App 组件 (入口与表单处理)

App 组件作为应用的根组件,包含添加新 Todo 的表单逻辑。

import React from 'react';
import useStore from './store'; // 引入我们创建的 Store Hook
import Filter from './Filter';
import Filtered from './Filtered';// 用于生成唯一 ID (简化示例,应与 store 中的 keyCount 保持一致或使用更健壮的 ID 生成方式)
let keyCount = 0;const App = () => {// 从 Store 中获取更新 todos 的方法const { setTodos } = useStore();// 处理表单提交事件,用于添加新的 Todoconst add = (e: React.FormEvent<HTMLFormElement>) => {e.preventDefault(); // 阻止表单默认提交行为const form = e.currentTarget;const inputElement = form.elements.namedItem('inputTitle') as HTMLInputElement;const title = inputElement.value.trim(); // 获取输入框的值并去除首尾空格if (title) { // 确保标题不为空inputElement.value = ''; // 清空输入框// 调用 setTodos 更新状态,添加新的 Todo 项setTodos((prevTodos) => [...prevTodos,{ title, completed: false, id: keyCount++ }, // 创建新的 Todo 对象]);}};return (<div className="todo-app"><h1>My Todos</h1><form onSubmit={add}>{/* 过滤组件 */}<Filter />{/* 输入框 */}<input name="inputTitle" placeholder="Add a new todo..." autoComplete="off" />{/* 列表展示组件 */}<Filtered />{/* 隐藏的提交按钮或依赖 Enter 键提交 */}<button type="submit" style={{ display: 'none' }}>Add</button></form></div>);
};export default App;

App 组件中:

  • 我们使用 useStore() 获取 setTodos Action。
  • add 函数在表单提交时触发(通常是按下 Enter 键),它获取输入框的值,创建一个新的 Todo 对象,并使用 setTodos 将其添加到 Store 的 todos 数组中,最后清空输入框。
Filter 组件 (过滤选项)

Filter 组件提供单选按钮,让用户可以选择查看所有、已完成或未完成的 Todo。

import React from 'react';
import { Radio } from 'antd'; // 假设使用 antd UI 库
import useStore from './store';const Filter = () => {// 从 Store 中获取 filter 状态和 setFilter Actionconst { filter, setFilter } = useStore();return (<div className="filter-controls"><Radio.Group onChange={(e) => setFilter(e.target.value)} value={filter}><Radio value="all">All</Radio><Radio value="completed">Completed</Radio><Radio value="incompleted">Incompleted</Radio></Radio.Group></div>);
};export default Filter;
  • Filter 组件从 useStore 获取当前的 filter 值和 setFilter Action。
  • 当用户点击不同的 Radio 按钮时,onChange 事件触发,调用 setFilter 更新 Store 中的 filter 状态。
Filtered 组件 (展示过滤后的列表)

Filtered 组件负责根据当前的 filter 状态,从 Store 中获取 todos 列表,进行筛选,并渲染最终的 Todo 列表。这里还使用了 react-spring 来添加简单的动画效果。

import React from 'react';
import { useTransition, animated as a } from '@react-spring/web'; // 引入动画库
import useStore from './store';
import TodoItem from './TodoItem'; // 引入单个 Todo 项组件const Filtered = () => {// 从 Store 中获取 todos 列表和 filter 状态const { todos, filter } = useStore();// 根据 filter 状态筛选 todosconst filteredTodos = todos.filter((todo) => {if (filter === 'all') return true; // 显示全部if (filter === 'completed') return todo.completed; // 显示已完成return !todo.completed; // 显示未完成 (filter === 'incompleted')});// 使用 react-spring 创建列表项的进入/离开动画const transitions = useTransition(filteredTodos, {keys: (todo) => todo.id, // 使用 Todo 的唯一 ID 作为 keyfrom: { opacity: 0, height: 0 },enter: { opacity: 1, height: 40 }, // 假设每个 Todo 项高度为 40pxleave: { opacity: 0, height: 0 },config: { tension: 280, friction: 25 } // 动画物理效果配置});return (<div className="todo-list">{transitions((style, item) => (// 使用 animated.div 应用动画样式<a.div className="item" style={style}>{/* 渲染单个 Todo 项 */}<TodoItem item={item} /></a.div>))}{filteredTodos.length === 0 && <p className="empty-message">No todos here!</p>}</div>);
};export default Filtered;
  • Filtered 组件读取 todosfilter 状态。
  • 它根据 filter 的值计算出 filteredTodos
  • useTransition (来自 react-spring) 用于为列表项添加平滑的进入和离开动画。
  • 每个筛选后的 Todo 项被传递给 TodoItem 组件进行渲染。
TodoItem 组件 (单个 Todo 项)

TodoItem 组件负责渲染单个 Todo 项,并处理完成状态切换和删除操作。

import React from 'react';
import { CloseOutlined } from '@ant-design/icons'; // 假设使用 antd 图标
import useStore from './store';
import { Todo } from './store'; // 引入 Todo 类型定义interface TodoItemProps {item: Todo;
}const TodoItem = ({ item }: TodoItemProps) => {// 获取 setTodos Actionconst { setTodos } = useStore();const { title, completed, id } = item;// 切换 Todo 的完成状态const toggleCompleted = () =>setTodos((prevTodos) =>prevTodos.map((prevItem) =>prevItem.id === id ? { ...prevItem, completed: !completed } : prevItem,),);// 删除 Todo 项const remove = () => {setTodos((prevTodos) => prevTodos.filter((prevItem) => prevItem.id !== id));};return (<><inputtype="checkbox"checked={completed}onChange={toggleCompleted}aria-label={`Mark ${title} as ${completed ? 'incomplete' : 'complete'}`}/><span style={{ textDecoration: completed ? 'line-through' : 'none' }}>{title}</span><CloseOutlinedonClick={remove}style={{ cursor: 'pointer', marginLeft: '8px' }}aria-label={`Remove ${title}`}/></>);
};export default TodoItem;
  • TodoItem 接收一个 item (Todo 对象) 作为 prop。
  • toggleCompleted 函数通过 setTodos 更新对应 ID 的 Todo 项的 completed 状态。
  • remove 函数通过 setTodos 过滤掉当前 ID 的 Todo 项,实现删除。

至此,一个基本的 TODO 应用就完成了。我们通过 create 定义了状态和操作,并通过 useStore Hook 在组件中访问和更新状态。

使用 Immer 简化嵌套状态更新

当状态结构变得复杂,例如包含多层嵌套对象时,直接使用展开运算符(...)进行不可变更新会变得非常冗长且容易出错。

考虑以下嵌套状态:

const nestedObject = {deep: {nested: {obj: {count: 0,},},},
};

如果想更新 count,传统的 set 写法会是:

const useStore = create((set) => ({nestedObject,updateState() {set(prevState => ({nestedObject: {...prevState.nestedObject,deep: {...prevState.nestedObject.deep,nested: {...prevState.nestedObject.deep.nested,obj: {...prevState.nestedObject.deep.nested.obj,count: prevState.nestedObject.deep.nested.obj.count + 1, // 更新 count},},},},}));},
}));

这种写法非常繁琐。幸运的是,Zustand 可以与 Immer 库无缝集成,极大地简化深层嵌套状态的更新。

首先,安装 Immer:

npm install immer
# or
yarn add immer

然后,在 set 函数中使用 Immer 的 produce

import { create } from 'zustand';
import { produce } from 'immer'; // 引入 produceconst useStore = create((set) => ({nestedObject,updateState() {// 使用 produce 包装更新逻辑set(produce((state) => {// 在 produce 回调中,可以直接修改 state (Immer 会处理不可变性)state.nestedObject.deep.nested.obj.count += 1;}));},
}));

使用 Immer 后,代码变得清晰简洁,就像直接修改对象一样。

注意: 虽然 Immer 提高了开发效率,但它在内部执行了额外的操作来保证不可变性。对于服务端渲染 (SSR) 或性能极其敏感的场景,相比于手动解构,Immer 可能会带来微小的 CPU 开销。请根据实际情况权衡。

优化性能:状态选取 (Selectors)

默认情况下,当你在组件中使用 useStore() 获取状态时,例如 const { todos, filter } = useStore();,只要 Store 中的 任何 状态发生变化,该组件都会重新渲染 (re-render)。

考虑以下场景:

let renderCount = 0;// Display 组件只依赖 todos
const Display = () => {renderCount++;console.log('Display re-rendered:', renderCount);const { todos } = useStore(); // 获取整个 store 或多个属性return (<div>{/* ... 渲染 todos ... */}</div>);
};// Control 组件只调用 setFilter
const Control = () => {const { setFilter } = useStore();return <button onClick={() => setFilter('completed')}>Set Filter to Completed</button>;
};const App = () => (<><Display /><Control /></>
);

当点击 Control 组件的按钮时,filter 状态更新了。尽管 Display 组件根本不使用 filter,它仍然会重新渲染。这是因为 Zustand 在状态更新后,默认使用 Object.is 比较 useStore() 返回的对象。由于每次状态更新都会产生一个新的 Store 状态对象,即使 todos 本身没变,useStore() 返回的对象引用也变了,导致 Object.is 比较结果为 false,从而触发 Display 组件的重渲染。

解决方案:使用 Selector 函数

为了解决这个问题,useStore Hook 接受一个可选的 selector 函数 作为第一个参数。这个函数接收整个 state 对象,并返回你真正需要的部分。Zustand 会比较 selector 函数 返回的值 是否发生变化,而不是比较整个 state 对象。

Display 组件修改为:

const Display = () => {renderCount++;console.log('Display re-rendered:', renderCount);// 使用 selector 函数只选取 todosconst todos = useStore((state) => state.todos);return (<div>{/* ... 渲染 todos ... */}</div>);
};

现在,当 filter 变化时,state.todos 的引用没有改变,Object.is(prevTodos, currentTodos) 返回 trueDisplay 组件就不会再进行不必要的重渲染了。

最佳实践: 始终使用 selector 函数来精确选取组件所需的状态片段,避免不必要的渲染。

辅助工具:createSelectors

为了简化为每个状态属性编写 selector 的过程,并提供类似 store.use.propertyName() 的便捷语法,社区提供了一个 createSelectors 辅助函数(注意:这不是 Zustand 核心库的一部分,需要自行实现或引入)。

import { StoreApi, UseBoundStore } from 'zustand';// 类型定义,为 Store 添加 use 属性
type WithSelectors<S> = S extends { getState: () => infer T }? S & { use: { [K in keyof T]: () => T[K] } }: never;// createSelectors 函数实现
const createSelectors = <S extends UseBoundStore<StoreApi<object>>>(_store: S,
) => {const store = _store as WithSelectors<typeof _store>;store.use = {}; // 初始化 use 对象// 遍历 store 的所有 state keysfor (const k of Object.keys(store.getState())) {// 为每个 key 创建一个 selector hook 并挂载到 use 对象上(store.use as any)[k] = () => store((s) => s[k as keyof typeof s]);}return store;
};// --- 使用 createSelectors ---// 1. 先创建基础 store
const useStoreBase = create<TodoState>((set) => ({filter: 'all',todos: [],setFilter(filter) {set({ filter });},setTodos(fn) {set((prev) => ({ todos: fn(prev.todos) }));},
}));// 2. 使用 createSelectors 包装基础 store
const useStore = createSelectors(useStoreBase);// --- 在组件中使用 ---
// 获取状态属性
const todos = useStore.use.todos();
const filter = useStore.use.filter();// 获取 Action (方法)
const setTodos = useStore.use.setTodos();
const setFilter = useStore.use.setFilter();

使用 createSelectors 后,可以通过 useStore.use.propertyName() 的方式获取状态,内部自动应用了 selector,减少了手动编写 selector 的模板代码,并降低了忘记使用 selector 的风险。

优化性能:浅层比较 (Shallow Comparison)

当你需要在一个组件中选取 多个 状态片段时,即使使用了 selector,也可能遇到不必要的重渲染。

考虑以下情况:

const MyComponent = () => {// 同时选取 todos 和 setFilterconst { todos, setFilter } = useStore((state) => ({todos: state.todos,setFilter: state.setFilter,}));// ...
}

在这个例子中,selector 函数返回一个包含 todossetFilter 的新对象 { todos: ..., setFilter: ... }。当 Store 中 任何 状态(比如 filter)发生变化时,这个 selector 函数会重新执行,即使 state.todosstate.setFilter 本身没有改变,它也会返回一个 新的对象引用。由于 Object.is 比较的是对象引用,它会认为状态发生了变化,导致组件重渲染。

解决方案:使用浅层比较

Zustand 提供了 shallow 函数来进行浅层比较。它会比较 selector 返回对象的第一层属性值是否发生变化,而不是比较对象本身的引用。

shallow 作为 useStore 的第二个参数传入:

import { shallow } from 'zustand/shallow'; // 引入 shallowconst MyComponent = () => {const { todos, setFilter } = useStore((state) => ({todos: state.todos,setFilter: state.setFilter,}),shallow, // 使用 shallow 进行比较);// ...
}

现在,只有当 state.todosstate.setFilter 的值(或引用,对于非原始类型)真正发生变化时,组件才会重渲染。

推荐用法:useShallow Hook

Zustand 还提供了一个更简洁的 useShallow Hook (来自 zustand/react/shallow),它是目前推荐的进行浅层比较的方式:

import { useShallow } from 'zustand/react/shallow'; // 引入 useShallowconst MyComponent = () => {// 使用 useShallow 包装 selectorconst { todos, setFilter } = useStore(useShallow((state) => ({todos: state.todos,setFilter: state.setFilter,})),);// ...
}

useShallow 内部封装了 selector 和 shallow 比较逻辑,使得代码更易读。

总结:

  • 选取单个状态片段时,直接使用 selector 函数:const todos = useStore(state => state.todos);
  • 选取多个状态片段时,使用 useShallow 包装 selector:const { todos, filter } = useStore(useShallow(state => ({ todos: state.todos, filter: state.filter })));

处理异步操作

Zustand 的 Action 天然支持异步操作。你可以直接在 Action 中编写 async/await 代码来处理数据获取、API 调用等异步任务,并在完成后使用 set 更新状态。

import { create } from 'zustand';interface AsyncState {todos: Todo[] | null; // 初始可能为 nullerror: Error | null;isLoading: boolean;fetchData: () => Promise<void>;
}const useAsyncStore = create<AsyncState>((set) => ({todos: null,error: null,isLoading: false,fetchData: async () => {set({ isLoading: true, error: null }); // 开始加载,清除旧错误try {const res = await fetch(`https://jsonplaceholder.typicode.com/todos`);if (!res.ok) {throw new Error(`HTTP error! status: ${res.status}`);}const todos = await res.json();set({ todos, isLoading: false }); // 成功,更新 todos,结束加载} catch (error) {console.error("Failed to fetch todos:", error);set({ error: error as Error, isLoading: false }); // 失败,记录错误,结束加载}},
}));// --- 在 React 组件中使用 ---
import React, { useEffect } from 'react';
import useAsyncStore from './asyncStore';function App() {// 从 store 获取状态和 actionconst { todos, error, isLoading, fetchData } = useAsyncStore();// 组件挂载时触发数据获取useEffect(() => {fetchData();}, [fetchData]); // fetchData 通常是稳定的,但作为依赖项是好习惯if (isLoading) return <div>Loading...</div>;if (error) return <div>Error: {error.message}</div>;if (!todos) return <div>No data yet.</div>; // 可能在加载完成但无数据时return (<div><h1>Fetched Todos</h1><ul>{todos.map((todo) => (<li key={todo.id}>{todo.title}</li>))}</ul></div>);
}export default App;

在这个例子中:

  • fetchData 是一个 async 函数,它负责调用 API。
  • 在请求开始时,通过 setisLoading 设为 true
  • 请求成功后,通过 set 更新 todos 并将 isLoading 设为 false
  • 请求失败后,通过 set 记录 error 并将 isLoading 设为 false
  • React 组件根据 isLoading, error, todos 的状态来决定渲染哪个视图(加载中、错误信息、Todo 列表)。

模块化:拆分 Store (Slices)

随着应用功能的增加,单个 Store 文件可能会变得庞大而难以维护。Zustand 支持将 Store 拆分成多个逻辑相关的 “切片” (Slices),然后在主 Store 中将它们组合起来。每个 Slice 本质上是一个函数,它接收 set, get 等参数,并返回该切片的状态和操作。

示例:创建 Bear 和 Fish 切片

// bearSlice.js
export const createBearSlice = (set, get) => ({bears: 0,addBear: () => set((state) => ({ bears: state.bears + 1 })),eatFish: () => {// 可以在一个 slice 中调用 set 来影响其他 slice 的状态 (如果它们被合并在同一个 store 中)// 注意:这里假设 fish slice 也在同一个 store 中且有 fishes 属性// 更好的方式可能是通过 get() 获取其他 slice 的 action 来调用// 或者定义跨 slice 的 action (见下文)// set((state) => ({ fishes: state.fishes - 1 })) // 这种直接修改其他 slice 状态的方式需要谨慎},
});// fishSlice.js
export const createFishSlice = (set, get) => ({fishes: 0,addFish: () => set((state) => ({ fishes: state.fishes + 1 })),
});

合并 Slices

在主 Store 文件中,使用 create 并将各个 Slice 的结果合并。

// useBoundStore.js
import { create } from 'zustand';
import { createBearSlice } from './bearSlice';
import { createFishSlice } from './fishSlice';// 合并 slices,将它们的属性直接展开到根级别
export const useBoundStore = create((...a) => ({...createBearSlice(...a), // a 包含了 set, get, api 等参数...createFishSlice(...a),
}));/*
// 另一种合并方式:创建命名空间 (如果希望按模块访问)
export const useBoundStoreWithNamespaces = create((...a) => ({bearSlice: { ...createBearSlice(...a) },fishSlice: { ...createFishSlice(...a) },
}));
// 使用时:useBoundStoreWithNamespaces(state => state.bearSlice.bears)
*/

在 React 组件中使用合并后的 Store

使用方式与普通 Store 完全相同,因为所有状态和操作都被合并到了顶层。

import React from 'react';
import { useBoundStore } from './stores/useBoundStore';function App() {// 使用 selector 精确选取所需状态和操作const bears = useBoundStore((state) => state.bears);const fishes = useBoundStore((state) => state.fishes);const addBear = useBoundStore((state) => state.addBear);const addFish = useBoundStore((state) => state.addFish); // 假设需要 addFishreturn (<div><h2>Number of bears: {bears}</h2><h2>Number of fishes: {fishes}</h2><button onClick={addBear}>Add a bear</button><button onClick={addFish}>Add a fish</button></div>);
}export default App;

更新跨 Slice 的状态

如果一个操作需要同时更新多个 Slice 中的状态,有几种方式:

  1. 在 Action 中多次调用 set (不推荐,可能触发多次更新)。
  2. 在 Action 中一次性 set 多个状态 (如果状态都在一个 Slice 控制)。
  3. 定义一个专门用于交互的 Action:可以在合并 Store 时定义,或者创建一个新的 “交互 Slice”。
// interactionSlice.js
// 这个 slice 依赖于其他 slice 的 action 存在于最终合并的 store 中
export const createInteractionSlice = (set, get) => ({addBearAndFish: () => {// 通过 get() 获取其他 slice 的 action 并调用// 这是更推荐的方式,因为它不直接依赖 set 的内部实现细节get().addBear();get().addFish();// 或者,如果 action 只是简单的 set,也可以直接 set// set(state => ({ bears: state.bears + 1, fishes: state.fishes + 1 }));},
});// useBoundStore.js (合并所有 slices)
import { create } from 'zustand';
import { createBearSlice } from './bearSlice';
import { createFishSlice } from './fishSlice';
import { createInteractionSlice } from './interactionSlice';export const useBoundStore = create((...a) => ({...createBearSlice(...a),...createFishSlice(...a),...createInteractionSlice(...a), // 合并交互 slice
}));// 组件中使用
const addBearAndFish = useBoundStore((state) => state.addBearAndFish);
// <button onClick={addBearAndFish}>Add Bear and Fish</button>

使用中间件 (Middleware)

Zustand 支持中间件,允许你在 Store 创建过程中包装 setget 方法,添加额外的功能,如持久化、日志记录、开发者工具集成等。

中间件本质上是一个函数,它接收 Store 的创建函数 ((set, get, api) => ({...})) 作为参数,并返回一个新的创建函数。

示例:使用 persist 中间件进行状态持久化

Zustand 提供了官方的 persist 中间件,可以将 Store 的状态保存到 localStorage (或其他存储)。

首先,安装中间件(如果尚未包含在主 zustand 包中,根据版本可能需要单独安装):

npm install zustand # 通常已包含 middleware

然后,在 create 时应用中间件:

import { create } from 'zustand';
import { persist, createJSONStorage } from 'zustand/middleware'; // 引入 persist
import { createBearSlice } from './bearSlice';
import { createFishSlice } from './fishSlice';export const useBoundStore = create(// 1. 调用 persist 中间件persist(// 2. 将原始的 store 创建逻辑 (包括 slice 合并) 作为参数传入 persist(...a) => ({...createBearSlice(...a),...createFishSlice(...a),}),// 3. 提供 persist 的配置对象{name: 'bound-store-storage', // localStorage 中的 key 名称storage: createJSONStorage(() => localStorage), // 指定存储引擎 (默认 localStorage)// partialize: (state) => ({ bears: state.bears }), // 可选:只持久化部分状态})
);

现在,bearsfishes 的状态会在每次更新后自动保存到 localStorage,并在页面加载时恢复。

其他常用的中间件包括 devtools (用于集成 Redux DevTools 浏览器扩展) 等。你可以组合使用多个中间件。

处理计算属性 (例如 MobX 迁移)

如果你从 MobX 等具有计算属性(Computed Properties)概念的库迁移过来,可能会想知道如何在 Zustand 中实现类似的功能。计算属性是根据其他状态派生出来的值。

MobX 示例:

// MobX Store
class UserStore {mobilePhone = null; // { countryCode: string, caller: string } | null@computedget mobileDisplay() {if (!this.mobilePhone) {return '';}const { countryCode = '0086', caller } = this.mobilePhone;return `+${parseInt(countryCode)} ${caller}`;}
}

Zustand 实现方案:

方案 1:在组件中使用 Selector 函数计算

这是最常见且推荐的方式。直接在组件的 selector 函数中进行计算。Zustand 的 selector 具有记忆化特性,只有当依赖的状态(如此处的 state.mobilePhone)发生变化时,selector 才会重新计算。

import React from 'react';
import useUserStore from '../../store/zustandUserStore';const UserProfile = () => {// 在 selector 中直接计算 mobileDisplayconst mobileDisplay = useUserStore(state => {if (!state.mobilePhone) {return '';}const { countryCode = '0086', caller } = state.mobilePhone;// 注意:parseInt 可能不是必须的,取决于 countryCode 的格式return `+${countryCode} ${caller}`;});return (<div><p>Mobile: {mobileDisplay || 'Not provided'}</p></div>);
};

方案 2:在 Store 中定义一个普通函数

你也可以在 Store 中定义一个普通函数,该函数使用 get() 来访问当前状态并执行计算。

import { create } from 'zustand';interface UserState {mobilePhone: { countryCode?: string; caller: string } | null;getMobileDisplay: () => string;// ... 其他状态和操作
}const useUserStore = create<UserState>((set, get) => ({mobilePhone: null,// 将计算属性改为一个普通函数,使用 get() 获取最新状态getMobileDisplay: () => {const state = get(); // 获取当前完整状态if (!state.mobilePhone) {return '';}const { countryCode = '0086', caller } = state.mobilePhone;return `+${countryCode} ${caller}`;},// ... 其他属性和方法
}));// --- 在组件中使用 ---
const UserProfile = () => {// 获取计算函数const getMobileDisplay = useUserStore((state) => state.getMobileDisplay);// 调用函数获取计算结果const mobileDisplay = getMobileDisplay();// 或者,如果希望在状态变化时自动更新,仍然需要 selector// const mobileDisplay = useUserStore(state => state.getMobileDisplay()); // 这样每次渲染都会调用// 更好的方式是结合 selector,仅在 mobilePhone 变化时重新获取并调用const mobileDisplayResult = useUserStore(state => {// 依赖 mobilePhoneif (!state.mobilePhone) return '';// 调用 store 内的函数return state.getMobileDisplay();}, shallow); // 如果 getMobileDisplay 函数本身是稳定的,可以不用 shallowreturn (<div><p>Mobile: {mobileDisplayResult || 'Not provided'}</p></div>);
};

对比:

  • 方案 1 (Selector) 更符合 Zustand 的理念,利用了 selector 的自动重计算和优化机制,通常是首选。
  • 方案 2 (Store 函数) 将计算逻辑保留在 Store 内部,但需要在组件中正确调用(通常还是结合 selector 来触发更新)。当计算逻辑非常复杂或需要在 Store 内部多处复用时可以考虑。

这篇文章涵盖了 Zustand 的核心概念和常见用法,从基础的 Store 创建、组件交互,到性能优化(Selectors, Shallow Comparison)、异步处理、代码组织(Slices)、功能扩展(Middleware)以及特定场景(计算属性)的解决方案。希望这能帮助你更好地理解和运用 Zustand 进行状态管理。

相关文章:

Zustand 状态管理:从入门到实践

Zustand 状态管理&#xff1a;从入门到实践 Zustand 是一个轻量、快速且灵活的 React 状态管理库。它基于 Hooks API&#xff0c;提供了简洁的接口来创建和使用状态&#xff0c;同时易于扩展和优化。本文将通过一个 TODO 应用实例带你快速入门 Zustand&#xff0c;并探讨其核心…...

[RITSEC CTF 2025] Crypto

这个忘打了&#xff0c;难度不小。 Alien Encryption 101 一个很小的RSA&#xff0c;略 Cuwves 2 Electric Boogaloo 已知p,在p^2下的两个椭圆曲线的j不变量&#xff0c;直接用函数 Mothership AES_CBC加密给出密文和IV&#xff0c;通过调整IV来修改明文 import base64 …...

算法250327题目

1114: 4006 AB问题 题目描述 给定两个整数A和B&#xff0c;其表示形式是&#xff1a;从个位开始&#xff0c;每三位数用逗号,隔开。 现在请计算AB的结果&#xff0c;并以正常形式输出。 输入 输入包含多组数据&#xff0c;每组数据占一行&#xff0c;由两个整数A和B组成&am…...

PGP实现简单加密教程

模拟情景&#xff1a; 假设001和002两位同学的电脑上都安装了PGP&#xff0c;现在两人需要进行加密通讯。 一、创建密钥 1.新建密钥&#xff0c;输入名称和邮箱&#xff0c;输入8位口令&#xff0c;根据指示完成。 2.将其添加到主密钥&#xff0c;鼠标右击出现选项。 这里出…...

7.8 窗体间传递数据

版权声明&#xff1a;本文为博主原创文章&#xff0c;转载请在显著位置标明本文出处以及作者网名&#xff0c;未经作者允许不得用于商业目的 当项目中有多个窗体时&#xff08;在本节中为两个窗体&#xff1a;Form1和Form2&#xff09;&#xff0c;窗体间传递数据有以下几种方…...

一文了解 MCP Server:AI 工具与外部世界的桥梁

引言 随着大语言模型&#xff08;LLM&#xff09;的普及与 AI Agent 的爆发&#xff0c;Anthropic 于 2024 年底提出并开源的 Model Context Protocol&#xff08;MCP&#xff0c;模型上下文协议&#xff09;成为构建智能体系统的关键基石之一。本文将结合最新的实战经验&#…...

【redis】集群 数据分片算法:哈希求余、一致性哈希、哈希槽分区算法

文章目录 什么是集群数据分片算法哈希求余分片搬运 一致性哈希扩容 哈希槽分区算法扩容相关问题 什么是集群 广义的集群&#xff0c;只要你是多个机器&#xff0c;构成了分布式系统&#xff0c;都可以称为是一个“集群” 前面的“主从结构”和“哨兵模式”可以称为是“广义的…...

基于Springboot的网上订餐系统 【源码】+【PPT】+【开题报告】+【论文】

网上订餐系统是一个基于Java语言和Spring Boot框架开发的Web应用&#xff0c;旨在为用户和管理员提供一个便捷的订餐平台。该系统通过简化餐饮订购和管理流程&#xff0c;为用户提供快速、高效的在线订餐体验&#xff0c;同时也为管理员提供完善的后台管理功能&#xff0c;帮助…...

Redis常见面试问题汇总

Redis 面试笔记整理 一、Redis 基础知识1. Redis 概述Redis 是什么&#xff1f;主要特点有哪些&#xff1f;Redis 和 Memcached 的区别是什么&#xff1f;Redis 是单线程还是多线程&#xff1f;为什么单线程还能高效&#xff1f;Redis 6.0 之后的多线程模型是怎样的&#xff1f…...

【redis】集群 如何搭建集群详解

文章目录 集群搭建1. 创建目录和配置2. 编写 docker-compose.yml完整配置文件 3. 启动容器4. 构建集群超时 集群搭建 基于 docker 在我们云服务器上搭建出一个 redis 集群出来 当前节点&#xff0c;主要是因为我们只有一个云服务器&#xff0c;搞分布式系统&#xff0c;就比较…...

NLP高频面试题(二十)——flash attention原理

FlashAttention是一种针对Transformer模型中自注意力机制的优化算法&#xff0c;旨在提高计算效率并降低内存占用&#xff0c;特别适用于处理长序列任务。 在Transformer架构中&#xff0c;自注意力机制的计算复杂度和内存需求随着序列长度的平方增长。这意味着当处理较长序列时…...

飞牛NAS本地部署小雅Alist结合内网穿透实现跨地域远程在线访问观影

文章目录 前言1. VMware安装飞牛云&#xff08;fnOS&#xff09;1.1 打开VMware创建虚拟机1.3 初始化系统 2. 飞牛云搭建小雅Alist3. 公网远程访问小雅Alist3.1 安装Cpolar内网穿透3.2 创建远程连接公网地址 4. 固定Alist小雅公网地址 前言 嘿&#xff0c;小伙伴们&#xff0c…...

Episode, time step, batch, epoch

1. Episode&#xff08;回合&#xff09; 回合&#xff08;episode&#xff09;表示智能体从开始执行任务到完成任务&#xff08;例如成功到达目标或触发失败条件&#xff09;的全过程。 例如&#xff0c;如果我们训练一个四足机器人走到一个目标点&#xff0c;一个回合就是从…...

Linux版本控制器Git【Ubuntu系统】

文章目录 **前言**一、版本控制器二、Git 简史三、安装 Git四、 在 Gitee/Github 创建项目五、三板斧1、git add 命令2、git commit 命令3、git push 命令 六、其他1、git pull 命令2、git log 命令3、git reflog 命令4、git stash 命令 七、.ignore 文件1、为什么使用 .gitign…...

browser-use 库网页元素点击测试工具

目录 代码代码解释输出结果 代码 import asyncio import jsonfrom browser_use.browser.browser import Browser, BrowserConfig from browser_use.dom.views import DOMBaseNode, DOMElementNode, DOMTextNode from browser_use.utils import time_execution_syncclass Eleme…...

Vue 中使用 ECharts

在 Vue 中使用 ECharts 主要分为以下步骤&#xff0c;结合代码示例详细说明&#xff1a; 1. 安装 ECharts 通过 npm 或 yarn 安装 ECharts&#xff1a; npm install echarts --save # 或 yarn add echarts2. 基础使用&#xff08;完整引入&#xff09; 在 Vue 组件中使用 &…...

Spring AI + DeepSeek 构建大模型应用 Demo

Spring AI + DeepSeek 构建大模型应用 Demo 下面我将展示如何使用 Spring AI 框架结合 DeepSeek 的大模型能力构建一个简单的 AI 应用。 1. 环境准备 首先确保你已安装: JDK 17+Maven 3.6+Spring Boot 3.2+2. 创建 Spring Boot 项目 使用 Spring Initializr 创建项目,添加…...

解决GitLab无法拉取项目

1、验证 SSH 密钥是否已生成 ls ~/.ssh/ 如果看到类似 id_rsa 和 id_rsa.pub 的文件&#xff0c;则说明已存在 SSH 密钥。 避免麻烦&#xff0c;铲掉重来最方便。 如果没有&#xff0c;请生成新的 SSH 密钥&#xff1a; ssh-keygen -t rsa -b 4096 -C "your_emailexam…...

POSIX 线程取消与资源清理完全指南

POSIX 线程取消与资源清理完全指南 引言&#xff1a;为什么需要线程取消机制&#xff1f; 在多线程编程中&#xff0c;优雅地终止线程并确保资源释放是开发者面临的重要挑战。直接终止线程可能导致内存泄漏、文件未关闭等问题。POSIX 线程库提供了一套完整的线程取消和清理机…...

FPGA学习篇——Verilog学习之寄存器的实现

1 寄存器理论 这里在常见的寄存器种加了一个复位信号sys_rst_n。&#xff08;_n后缀表示复位信号低电平有效&#xff0c;无这个后缀的则表示高电平有效&#xff09; 这里规定在时钟的上升沿有效&#xff0c;只有当时钟的上升沿来临时&#xff0c;输出out 才会改变&#xff0c;…...

Cursor异常问题全解析-无限使用

title: Cursor异常问题全解析无限使用 tags: cursor categories:aiai编程 mathjax: true description: Cursor异常问题全解析与解决方案大全 abbrlink: 64908bd0 date: 2025-03-19 14:48:32 &#x1f916; Assistant &#x1f6a8; Cursor异常问题全解析与解决方案大全 &…...

【VUE】ant design vue实现表格table上下拖拽排序

适合版本&#xff1a;ant design vue 1.7.8 实现效果&#xff1a; 代码&#xff1a; <template><div class"table-container"><a-table:columns"columns":dataSource"tableData":rowKey"record > record.id":row…...

Vue实现动态数据透视表(交叉表)

需求:需要根据前端选择的横维度、竖维度、值去生成一个动态的表格&#xff0c;然后把交叉的值放入到对应的横维度和竖维度之下&#xff0c;其实就是excel里面的数据透视表功能&#xff0c;查询交叉语句为sql语句。 实现页面&#xff1a; 选择一下横维度、竖维度、值之后点击查…...

推荐《人工智能算法》卷1、卷2和卷3 合集3本书(附pdf电子书下载)

今天&#xff0c;咱们就一同深入探讨人工智能算法的卷1、卷2和卷3&#xff0c;看看它们各自蕴含着怎样的奥秘&#xff0c;并且附上各自的pdf电子版免费下载地址。 《人工智能算法&#xff08;卷1&#xff09;&#xff1a;基础算法》 下载地址&#xff1a;https://www.panziye…...

元宇宙浪潮下,数字孪生如何“乘风破浪”?

在当今科技飞速发展的时代&#xff0c;元宇宙的概念如同一颗璀璨的新星&#xff0c;吸引了全球的目光。元宇宙被描绘为一个平行于现实世界、又与现实世界相互影响且始终在线的虚拟空间&#xff0c;它整合了多种前沿技术&#xff0c;为人们带来沉浸式的交互体验。而数字孪生&…...

WPF 附加属性

在WPF&#xff08;Windows Presentation Foundation&#xff09;中&#xff0c;附加属性&#xff08;Attached Properties&#xff09;是一种特殊的依赖属性机制&#xff0c;它允许父元素为子元素提供额外的属性支持。这种特性特别适用于布局系统、输入处理和其他需要跨多个控件…...

数据分析 之 怎么看懂图 一

韦恩图怎么看 ①颜色:不同颜色代表不同的集合 ②)颜色重叠部分:表示相交集合共有的元素 ③颜色不重叠的部分:表示改集合独有的元素 ④数字:表示集合独有或共有的元素数量 ⑤百分比:表示该区域元素数占整体的比例 PCA图怎么看 ① 第一主成分坐标轴及主成分贡献率主成分贡献…...

手写数据库MYDB(一):项目启动效果展示和环境配置问题说明

1.项目概况 这个项目实际上就是一个轮子项目&#xff0c;现在我看到的这个市面上面比较火的就是这个首先RPC&#xff0c;好多的机构都在搞这个&#xff0c;还有这个消息队列之类的&#xff0c;但是这个是基于MYSQL的&#xff0c;我们知道这个MYSQL在八股盛宴里面是重点考察对象…...

深入理解椭圆曲线密码学(ECC)与区块链加密

椭圆曲线密码学&#xff08;ECC&#xff09;在现代加密技术中扮演着至关重要的角色&#xff0c;广泛应用于区块链、数字货币、数字签名等领域。由于其在提供高安全性和高效率上的优势&#xff0c;椭圆曲线密码学成为了数字加密的核心技术之一。本文将详细介绍椭圆曲线的基本原理…...

使用 PowerShell 脚本 + FFmpeg 在 Windows 系统中批量计算 MP4视频 文件的总时长

步骤 1&#xff1a;安装 FFmpeg 访问 FFmpeg 官网(Download FFmpeg)&#xff0c;下载 Windows 版编译包&#xff08;如 ffmpeg-release-full.7z&#xff09;。或者到&#xff08;https://download.csdn.net/download/zjx2388/90539014&#xff09;下载完整资料 解压文件&#…...