发布于 2026-01-06 3 阅读
0

如何使用 Zustand

如何使用 Zustand

作者:奇杜梅·纳姆迪

介绍

Redux彻底改变了全局状态管理系统的格局。它如此成功,以至于被广泛采用,并成为各种框架中理想的状态管理系统。不仅在框架中如此,它的原理在软件开发中仍然发挥着重要作用。几乎所有开发者都使用Redux来管理全局状态,我们都可以证明,使用Redux作为全局状态管理工具是多么强大、快速和易于维护。它使调试变得非常容易,并且使我们的应用程序具有可预测性。

没错,Redux 曾经在全局状态管理领域占据了主导地位,但现在出现了一位新秀。这位新秀蓄势待发,准备征服全局状态管理系统的市场。它火力全开,装备精良。它的名字叫Zustand
随着 Zustand 的到来及其带来的诸多优势,Redux 的统治时代是否即将终结呢?

本文将深入探讨 Zustand。我们将了解这个“小家伙”,理解它的工作原理、状态管理技术,以及它将如何席卷状态管理领域。

我们将介绍以下步骤:

什么是 Zustand?

一个基于简化 Flux 原则的小型、快速、可扩展的精简状态管理解决方案。它拥有基于 hooks 的便捷 API,并且没有样板代码或预设方案。https ://github.com/pmndrs/zustand

Zustand 是一个由 Jotai 和 React-spring 构建的小型、无预设的状态管理库。它拥有基于 Hooks 的便捷 API,并且不预设任何特性。Zustand 是开源的,拥有庞大的用户社区和全天候维护的开发者团队,致力于保持其稳定性。它在 GitHub 上拥有超过 3 万颗星。

Zustand 的使用方式与 Redux 截然不同。Zustand 简洁明了,不预设任何规则,它不会像 React-Redux 那样将应用封装在内容提供程序中。它主要使用 Hooks 与状态进行双向通信。Zustand 的
核心理念是单一数据源,即所有应用状态都存储在一个集中式的 store 中。这个 store 由状态切片组成,状态切片是代表应用不同部分的独立状态单元。每个状态切片都被定义为一个独立的 store,从而实现了模块化,并封装了相关的状态属性及其关联的 action。
使用 Zustand,你需要编写的样板代码更少,而且它的状态管理是集中式的,并且基于 action。

Zustand 默认提倡不可变性,确保状态更新以不可变的方式进行。更新状态时,会创建一个新的状态对象,而不是直接修改现有状态。这种方法简化了状态管理,避免了常见的与状态变更相关的错误,并实现了 React 组件中高效的变更检测和重新渲染。

Zustand 的另一个显著特点是其内置的订阅和选择性响应支持。组件可以订阅特定的状态切片,并在这些切片发生变化时自动重新渲染。Zustand 使用基于代理的细粒度依赖跟踪机制,从而实现高效的更新并最大限度地减少不必要的重新渲染。

下一节,我们将学习如何在项目中安装和使用 Zustand。


面向专业 Web 开发人员的开源企业应用平台

refine.new使您能够在浏览器中创建基于 React 的无头 UI 企业应用程序,您可以立即预览、调整和下载这些应用程序。

🚀 通过可视化组合您偏好的✨ React 平台、✨ UI 框架、✨ 后端连接器和✨ 身份验证提供程序,您可以在几秒钟内为您的项目创建量身定制的架构。这就像拥有触手可及的数千个项目模板,让您可以自由选择最符合您需求的模板!

优化博客徽标

Zustand 入门指南

我们知道 Zustand 是一个运行在 Node.js 上的 JavaScript 库。因此,在开始之前,我们需要在您的机器上安装一些基本工具。

  • Node.js:我们需要在系统中安装Node.js二进制文件。请访问https://nodejs.org/en/download并安装适用于您机器的二进制文件。
  • npm 或 yarn:它们是 Node 包管理器,用于维护和管理项目的依赖项和 Node.js 环境。npm 和 yarnnpm都已包含在 Node.js 二进制文件中,因此安装 Node.js 后无需单独安装 npm。yarn可以通过运行以下命令安装npm i yarn -g

让我们创建一个Nodejs项目:

mkdir zustand-prj
cd zustand-prj
npm init --y
Enter fullscreen mode Exit fullscreen mode
  • mkdir zustand-prj此命令会在当前位置创建一个名为“zustand-prj”的新目录。它等同于“make directory”。“zu”
  • cd zustand-prj此命令将当前工作目录更改为“zustand-prj”。运行此命令后,您将进入新创建的目录。
  • npm init --y此命令会在“zustand-prj”目录下初始化一个新的 Node.js 项目。npm init 命令用于生成 package.json 文件,该文件是一个清单文件,用于描述项目的元数据和依赖项。--y添加该标志是为了自动接受所有默认选项,而无需提示用户输入。这相当于对所有初始化问题都回答“是”。

要安装zustand库,我们运行以下命令:

npm install zustand # or yarn add zustand
Enter fullscreen mode Exit fullscreen mode

此命令会将zustand库安装到我们的项目中。

要使用zustand,我们需要导入一个create函数:

import { create } from "zustand";
Enter fullscreen mode Exit fullscreen mode

此函数通过回调函数调用,并返回一个自定义钩子。传递给它的回调函数用于定义状态以及可用于操作状态的函数。状态和函数都包含在回调函数返回的对象中。

我们来看一个例子:

 const useCounter = create((set) => {
    return {
        counter: 0,
        incrCounter: () => set((state) => ({ counter: state.counter + 1 })),
    };
});
Enter fullscreen mode Exit fullscreen mode

请注意create,该函数会将一个set函数传递给回调函数。此set函数用于操作 store 中的状态。zustand 中的状态可以是原始类型、对象或函数。在上面的示例中,我们的 store 中有两个状态:` counterA` 和 ` incrCounterB`。`$_B`useCounter是一个自定义 hook,我们可以在组件中使用此 hook 来获取最新的状态。如果我们在组件 A、B 和 C 中使用此 hook,则对组件 B 中状态所做的任何更改都会反映在组件 A 和 C 中,并且它们都会重新渲染以反映新的更改。

自定义 Hook 返回的create行为类似于useAppSelectorReact-Redux,它允许你从 store 中选择一部分状态。你调用这个 Hook 并传递一个回调函数。这个回调函数由 Hook 内部调用,并将当前状态传递给它。这样我们就能获取到当前状态,并返回我们想要的部分。

我们来看一个例子。

const counter = useCounter((state) => state.counter);
Enter fullscreen mode Exit fullscreen mode

我们调用了useCounter钩子函数并向其传递了一个回调函数。然后,我们期望从钩子函数中获取一个状态,并返回counter该状态的一部分。

然后,我们可以显示计数器:

const DisplayCounter = () => {
    const counter = useCounter((state) => state.counter);
    return <div>Counter: {counter}</div>;
};
Enter fullscreen mode Exit fullscreen mode

现在,我们想要创建一个组件,以便增加状态的值counter

const CounterControl = () => {
    const incrCounter = useCounter((state) => state.incrCounter);

    return (
        <div>
            <button onClick={incrCounter}>Incr. Counter</button>
        </div>
    );
};
Enter fullscreen mode Exit fullscreen mode

这是一个独立的组件,我们在这里增加状态值。可以看到,我们从状态中counter分离出了函数,并将其设置为按钮的事件。这样,当按钮被点击时,状态值就会增加。 可以看到,这些组件彼此独立,但都能“看到”存储中的当前状态。每当我们点击组件中的按钮时,它都会重新渲染以显示最新的状态值。incrStateonClickcounter
Incr. CounterCounterControlDisplayComponentcounter

让我们看看如何使用它们:

const App = () => {
    return (
        <>
            <DisplayCounter />
            <CounterControl />
        </>
    );
};
Enter fullscreen mode Exit fullscreen mode

它们彼此独立,却又神奇地通过 zustand 连接在一起。这让 React-Redux 面临严峻的挑战,因为尝试在 Redux-React 中重新创建这种小型状态需要编写更多代码:

  • 首先,我们将创建一个商店。
  • 我们将把App组件或其子组件包装在内容提供程序中,并通过 props 将 store 传递给上下文提供程序store
  • 我们将导入useSelector, useDispatch任何我们希望在商店中使用的组件。
  • 为了获取状态的一部分,我们将调用useSelector回调函数。
  • 要向商店发送操作,我们将使用useDispatch钩子。

虽然篇幅很长,但有了 Zustand 就变得过于简单了。

返回整个状态
现在,当我们调用create函数返回的自定义 hook 而不使用回调函数时,该 hook 将返回 store 的整个状态。

const state = useCounter();
Enter fullscreen mode Exit fullscreen mode

请注意,我们调用useCounter钩子函数时没有使用回调函数,因此在这种情况下,该函数将返回 store 中的整个状态。

state保存了整个状态useCounter。我们可以counter通过以下方式获取状态:

state.counter;
// 0
Enter fullscreen mode Exit fullscreen mode

我们也可以调用incrCounter状态函数:

state.counter;
// 0

state.incrCounter();
// 1
Enter fullscreen mode Exit fullscreen mode

记忆化:
我们可以对状态存储进行记忆化。记忆化是一种优化技术,它通过缓存耗时或成本高昂的函数调用结果来优化函数的执行。它涉及存储与特定输入参数集关联的函数返回值,这样,如果使用相同的参数再次调用该函数,则可以返回缓存的结果,而无需重新计算函数。
记忆化的目标是通过避免冗余计算来提高性能和效率。

现在,zustand 允许我们为其返回的自定义 hook 添加记忆化功能。它导出一个shallow函数,我们可以使用该函数为状态选择添加记忆化功能。

import { shallow } from "zustand/shallow";
Enter fullscreen mode Exit fullscreen mode

仍然以我们的useCounter例子为例,假设我们要从counterstore 中获取状态。我们可以这样做:

// DisplayComponent
const counter = useCounter((state) => state.counter);
Enter fullscreen mode Exit fullscreen mode

假设组件的初始状态counter为 0,当counter使用 `onState` 更新状态时incrCounter,组件DisplayComponent将会重新渲染。但是,如果更新后的状态值counter仍然是 0,那么组件就没有必要重新渲染DisplayCounter

当前后两个状态相同时,如何避免不必要的重新渲染?Zustand 建议我们向自定义钩子传递一个比较函数作为第二个参数。这个比较函数会比较前一个切片的状态和后一个切片的状态,如果两者相同,组件就不会重新渲染;否则,组件就会重新渲染。

这正是其他 React Hooks 的功能:useEffect、useMemo 和 useCallback。
shallow函数是由 Zustand 提供的比较器函数。它使用浅相等运算符对两个状态切片进行浅比较==

const counter = useCounter((state) => state.counter, shallow);
Enter fullscreen mode Exit fullscreen mode

我们把这个shallow函数作为第二个参数传递给了useCounter钩子函数。每次 store 中的状态发生变化时,它shallow会根据状态的前一个值和后一个值来判断组件是否需要重新渲染counter
如果我们不信任这个函数,可以使用我们自定义的比较器shallow。比较器函数接受两个参数,第一个参数是状态切片的前一个值,第二个参数是状态切片的下一个值。

(previousState, nextStateSlice) =>
Enter fullscreen mode Exit fullscreen mode

这个函数内部进行比较并返回结果。返回 truetrue会让钩子跳过组件的重新渲染,而返回 falsefalse则会让组件重新渲染。

让我们为counter状态切片创建比较器函数。

(previousCounter, nextCounter) => previousCounter === nextCounter;
Enter fullscreen mode Exit fullscreen mode

它使用===相等运算符来检查两个值是否相同,并返回一个布尔值。

让我们把它重新插回useCounter钩子里:

const counter = useCounter(
    (state) => state.counter,
    (previousCounter, nextCounter) => previousCounter === nextCounter
);
Enter fullscreen mode Exit fullscreen mode

现在,我们已经对useCounter钩子函数进行了记忆化。这样一来,我们的应用程序运行速度更快了,不再需要不必要的重新渲染。

更新整个状态
我们之前只讨论了如何从 store 中获取状态,但还没有深入探讨如何设置状态。我们只是在useCounter之前创建 hook 时简单地看到了它。现在,我们将了解如何更新状态。

我们了解到,zustand 会将一个函数传递给回调函数create。这个被广泛接受的函数用于set更新状态的全部或部分内容。

我们来看一下incrCounter状态函数:

const useCounter = create((set) => {
    return {
        counter: 0,
        incrCounter: () => set((state) => ({ counter: state.counter + 1 })),
    };
});
Enter fullscreen mode Exit fullscreen mode

这里,我们向主函数传递了一个回调函数set。主set函数会调用这个回调函数,并将状态作为参数传递给它,然后使用回调函数的返回值来更新状态。可以看到,在回调函数中,我们返回了一个包含counter属性的对象。主set函数会利用对象中的属性来确定状态中需要更新的属性。

我们看到,当我们把一个函数set作为参数传递给另一个函数时,该set函数期望我们返回一个对象。

我们可以将一个对象传递给set函数:

set({
    counter: 9,
});
Enter fullscreen mode Exit fullscreen mode

这将把counter状态值更新为 9。

清除整个状态
我们可以通过向函数传递一个空对象来清除 zustand 存储中的状态set

set({}, true);
Enter fullscreen mode Exit fullscreen mode

这将清除状态和操作。

什么是 action?
Action 是状态存储 (zustand store) 中的一部分,它类似于 React-Redux 中的 dispatch action,用于更新状态存储。例如,我们的incrCounter代码就是一个 action,我们调用set其中的函数来更新counter状态。

zustand 中的 Actions 也支持异步
操作。事实上,根据 Zustand 的文档,zustand 并不关心你的 Action 是否是异步的。
我们可以在 Action 中执行异步函数。例如,我们可以从 Action 向某个端点发送 HTTP 请求,并使用 HTTP 请求的结果更新状态。

我们举个例子:

const useCounter = create((set) => {
    return {
        counter: 0,
        incrCounter: async () => {
            const { data } = await axios.get("/counter");
            set({
                counter: data.counter,
            });
        },
    };
});
Enter fullscreen mode Exit fullscreen mode

您可以看到,incrCounter我们使用async关键字将其设为异步函数。在该函数内部,我们调用了一个/counter端点,并使用该函数来更新状态中set的值。counter

使用 Zustand 构建待办事项应用

现在,我们已经学习了Zustand及其API的基础知识。接下来,我们将使用Zustand创建一个待办事项应用。

待办事项应用将是一个 React 应用,而 Zustand 将为我们的状态管理提供支持。

让我们开始吧,我们将使用该工具搭建一个 React 项目create-react-app

create-react-app todo-app
cd todo-app
Enter fullscreen mode Exit fullscreen mode

接下来,我们安装zustand

npm install zustand
Enter fullscreen mode Exit fullscreen mode

我们创建钩子商店的第一件事:

import create from "zustand";

const useStore = create((set) => ({
    todos: [],
    addTodo: (text) =>
        set((state) => ({
            todos: [
                ...state.todos,
                {
                    id: Date.now(),
                    text,
                    completed: false,
                },
            ],
        })),
    toggleTodo: (id) =>
        set((state) => ({
            todos: state.todos.map((todo) =>
                todo.id === id ? { ...todo, completed: !todo.completed } : todo
            ),
        })),
    deleteTodo: (id) =>
        set((state) => ({
            todos: state.todos.filter((todo) => todo.id !== id),
        })),
}));

export default useStore;
Enter fullscreen mode Exit fullscreen mode

你看,我们有一个todos状态。它将保存待办事项数组。我们有三个操作:addTodo添加、toggleTodo切换和删除deleteTodoaddTodo添加操作会将新的待办事项添加到todos状态中。切换toggleTodo操作会切换completed待办事项的状态。deleteTodo删除操作会从状态数组中移除一个待办事项。
现在,让我们构建组件。

DisplayTodos
组件只有一个功能:渲染状态todos

const DisplayTodos = () => {
    const { todos, deleteTodo } = useStore((state) => {
        return { todos: state.todos, deleteTodo: state.deleteTodo };
    });

    return (
        <ul>
            {todos.map((todo) => (
                <li
                    key={todo.id}
                    style={{
                        textDecoration: todo.completed
                            ? "line-through"
                            : "none",
                    }}
                    onClick={() => toggleTodo(todo.id)}
                >
                    {todo.text}
                    <button onClick={() => deleteTodo(todo.id)}>Delete</button>
                </li>
            ))}
        </ul>
    );
};

export default DisplayTodos;
Enter fullscreen mode Exit fullscreen mode

todos我们从状态中提取了数组,并使用该Array#map方法渲染待办事项,同时还提取了其他部分deleteActionDelete按钮会从列表中移除每个待办事项,它通过调用deleteAction带有被点击待办事项 ID 的操作来实现这一点。
现在,让我们构建一个可以向列表中添加待办事项的组件。

待办事项控制

const TodosControl = () => {
    const addTodo = useStore((state) => state.addTodo);
    const [text, setText] = useState("");

    function handleSubmit(e) {
        e.preventDefault();
        addTodo(text);
        setText("");
    }

    return (
        <form onSubmit={handleSubmit}>
            <input
                type="text"
                value={text}
                onChange={(e) => setText(e.target.value)}
            />
            <button type="submit">Add</button>
        </form>
    );
};

export default TodosControl;
Enter fullscreen mode Exit fullscreen mode

这个组件提供了一个表单,我们可以在其中输入待办事项并将其添加到状态存储中。状态存储着text我们在输入框中输入的文本。然后,handleSubmit当通过按钮提交表单时,会调用一个函数Add。该函数内部会调用handleSubmit一个方法,addTodo并将状态存储中的文本text作为参数传递。这将创建一个新的待办事项并将其添加到todos状态存储中。

将它们整合在一起:

 const App = () => {
    return (
        <>
            <DisplayTodos />
            <TodosControl />
        </>
    );
};

export default App;
Enter fullscreen mode Exit fullscreen mode

结论

我们已经取得了长足的进步。Zustand 非常有趣,而且使用起来极其简洁易上手。请注意,我们在这里学到的只是基础知识,Zustand 的强大功能远未完全展现,它蕴藏着巨大的潜力。

让我们回顾一下。我们首先介绍了 Zustand,包括它的工作原理以及它与全球流行的 Redux 的区别。接下来,我们学习了如何安装 Zustand 库,以及如何在其中设置状态和使用 actions。我们还学习了如何在 actions 中设置异步操作、如何更新状态以及如何从状态中获取切片。

文章来源:https://dev.to/refine/how-to-use-zustand-3okc