• Babel 插件通关秘籍
  • Git 原理详解及实用指南
  • Nest 通关秘籍
  • React 通关秘籍
  • TypeScript 全面进阶指南
  • TypeScript 类型体操通关秘籍
  • 现代CSS
  • Babel 插件通关秘籍
  • Git 原理详解及实用指南
  • Nest 通关秘籍
  • React 通关秘籍
  • TypeScript 全面进阶指南
  • TypeScript 类型体操通关秘籍
  • 现代CSS
  • React 通关秘籍

    • 1.关于本小册
    • 2.一网打尽组件常用Hook
    • 3.Hook的闭包陷阱的成因和解决方案
    • 4.React组件如何写TypeScript类型
    • 5.React组件如何调试
    • 6.受控模式VS非受控模式
    • 7.组件实战:迷你Calendar
    • 8.组件实战:Calendar日历组件(上)
    • 9.组件实战:Calendar日历组件(下)
    • 10.快速掌握Storybook
    • 11.React组件如何写单测
    • 12.深入理解Suspense和ErrorBoundary
    • 13.组件实战:Icon图标组件
    • 14.组件实战:Space间距组件
    • 15.React.Children和它的两种替代方案
    • 16.三个简单组件的封装
    • 17.浏览器的5种Observer
    • 18.组件实战:Watermark防删除水印组件
    • 19.手写react-lazyload
    • 20.图解网页的各种距离
    • 21.自定义hook练习
    • 22.自定义hook练习(二)
    • 23.用react-spring做弹簧动画
    • 24.react-spring结合use-gesture手势库实现交互动画
    • 25.用react-transition-group和react-spring做过渡动画
    • 26.快速掌握Tailwind:最流行的原子化CSS框架
    • 27.用CSSModules避免样式冲突
    • 28.CSSInJS:快速掌握styled-components
    • 29.react-spring实现滑入滑出的转场动画
    • 30.组件实战:Message全局提示组件
    • 31.组件实战:Popover气泡卡片组件
    • 32.项目里如何快速定位组件源码
    • 33.一次超爽的React调试体验
    • 34.组件实战:ColorPicker颜色选择器(一)
    • 35.组件实战:ColorPicker颜色选择器(二)
    • 36.组件实战:onBoarding漫游式引导组件
    • 37.组件实战:Upload拖拽上传
    • 38.组件实战:Form表单组件
    • 39.React组件库都是怎么构建的
    • 40.组件库实战:构建esm和cjs产物,发布到npm
    • 41.组件库实战:构建umd产物,通过unpkg访问
    • 42.数据不可变:immutable和immer
    • 43.基于ReactRouter实现keepalive
    • 44.Historyapi和ReactRouter实现原理
    • 45.ReactContext的实现原理和在antd里的应用
    • 46.ReactContext的性能缺点和解决方案
    • 47.手写一个Zustand
    • 48.原子化状态管理库Jotai
    • 49.用react-intl实现国际化
    • 50.国际化资源包如何通过Excel和GoogleSheet分享给产品经理
    • 51.基于react-dnd实现拖拽排序
    • 52.react-dnd实战:拖拽版TodoList
    • 53.ReactPlayground项目实战:需求分析、实现原理
    • 54.ReactPlayground项目实战:布局、代码编辑器
    • 55.ReactPlayground项目实战:多文件切换
    • 56.ReactPlayground项目实战:babel编译、iframe预览
    • 57.ReactPlayground项目实战:文件增删改
    • 58.ReactPlayground项目实战:错误显示、主题切换
    • 59.ReactPlayground项目实战:链接分享、代码下载
    • 60.ReactPlayground项目实战:WebWorker性能优化
    • 61.ReactPlayground项目实战:总结
    • 62.手写MiniReact:思路分析
    • 63.手写MiniReact:代码实现
    • 64.手写MiniReact:和真实React源码的对比
    • 65.React18的并发机制是怎么实现的
    • 66.Ref的实现原理
    • 67.低代码编辑器:核心数据结构、全局store
    • 68.低代码编辑器:拖拽组件到画布、拖拽编辑json
    • 69.低代码编辑器:画布区hover展示高亮框
    • 70.低代码编辑器:画布区click展示编辑框
    • 71.低代码编辑器:组件属性、样式编辑
    • 72.低代码编辑器:预览、大纲
    • 73.低代码编辑器:事件绑定
    • 74.低代码编辑器:动作弹窗
    • 75.低代码编辑器:自定义JS
    • 76.低代码编辑器:组件联动
    • 77.低代码编辑器:拖拽优化、Table组件
    • 78.低代码编辑器:Form组件、store持久化
    • 79.低代码编辑器:项目总结
    • 80.快速掌握ReactFlow画流程图
    • 81.ReactFlow振荡器调音:项目介绍
    • 82.ReactFlow振荡器调音:流程图绘制
    • 83.ReactFlow振荡器调音:合成声音
    • 84.AudioContext实现在线钢琴
    • 85.React服务端渲染:从SSR到hydrate
    • 86.小册总结

提到状态管理,大家可能首先想到的是 redux。

redux 是老牌状态管理库,能完成各种基本功能,并且有着庞大的中间件生态来扩展额外功能。

但 redux 经常被人诟病它的使用繁琐。

近两年,React 社区出现了很多新的状态管理库,比如 zustand、jotai、recoil 等,都完全能替代 redux,而且更简单。

zustand 算是其中最流行的一个。

看 star 数,redux 有 60k,而 zustand 也有 38k 了:

redux:

zustand:

看 npm 包的周下载量,redux 有 880w,而 zustand 也有 260w 了:

redux:

zustand:

从各方面来说,zustand 都在快速赶超 redux。

那 zustand 为什么会火起来呢?

我觉得主要是因为简单,zustand 用起来真的是没有什么学习成本,没有 redux 的 action、reducer 等概念。

而且功能很强大,zustand 也有中间件,可以给它额外扩展功能。

既然功能上能替代 redux,那为什么不选择一个更简单的呢?

下面我们就来试试看:

npx create-react-app zustand-test

用 cra 创建个 react 项目。

进入项目把它跑起来:

npm run start

浏览器访问下:

然后安装 zustand:

npm install --save zustand

改下 App.js

import { create } from "zustand";

const useXxxStore = create((set) => ({
    aaa: "",
    bbb: "",
    updateAaa: (value) => set(() => ({ aaa: value })),
    updateBbb: (value) => set(() => ({ bbb: value })),
}));

export default function App() {
    const updateAaa = useXxxStore((state) => state.updateAaa);
    const aaa = useXxxStore((state) => state.aaa);

    return (
        <div>
            <input
                onChange={(e) => updateAaa(e.currentTarget.value)}
                value={aaa}
            />
            <Bbb></Bbb>
        </div>
    );
}

function Bbb() {
    return (
        <div>
            <Ccc></Ccc>
        </div>
    );
}

function Ccc() {
    const aaa = useXxxStore((state) => state.aaa);
    return <p>hello, {aaa}</p>;
}

用 create 函数创建一个 store,定义 state 和修改 state 的方法。

然后在组件里调用 create 返回的函数,取出属性或者方法在组件里用:

此外,你还可以调用 subscribe 来添加一个监听器:

useEffect(() => {
    useXxxStore.subscribe((state) => {
        console.log(useXxxStore.getState());
    });
}, []);

回调函数可以拿到当前 state,或者调用 store.getState 也可以拿到 state。

这就是 zustand 的全部用法了,就这么简单。

有的同学说,不是还有中间件么?

其实中间件并不是 zustand 自己实现的功能。

你看这个 create 方法的参数,它是一个接受 set、get、store 的三个参数的函数:

那我们可不可以包一层,自己拿到 get、set、store,对这些做一些修改,之后返回一个接受三个参数的函数呢?

比如这样:

function logMiddleware(func) {
    return function (set, get, store) {
        function newSet(...args) {
            console.log("调用了 set,新的 state:", get());
            return set(...args);
        }

        return func(newSet, get, store);
    };
}

我接受之前的函数,然后对把 set、get、store 修改之后再调用它:

这样不就给 zustand 的 set 方法加上了额外的功能么?

这个就是中间件,和 redux 的中间件是一样的设计。

它并不需要 zustand 本身做啥支持,只要把 create 的参数设计成一个函数,这个函数接收 set、get 等函作为参数,那就自然支持了中间件。

zustand 内置了一些中间件,比如 immer、persist。

persist 就是同步 store 数据到 localStorage 的。

我们试一下:

效果如下:

而且,中间件是可以层层嵌套的:

我们把自己写的 log 和内置的 persist 结合起来:

效果如下:

因为中间件不就是修改 set、get 这些参数么,这些 set、get 是可以层层包装的,所以自然中间件也就可以层层嵌套。

redux 和 zustand 的中间件一脉相承,都是很巧妙的设计。

学完了 zustand 的功能后,你觉得写这样一个 zustand 需要多少代码呢?

其实不到 100 行就能搞定。

不信我们试试看:

const createStore = (createState) => {
    let state;
    const listeners = new Set();

    const setState = (partial, replace) => {
        const nextState =
            typeof partial === "function" ? partial(state) : partial;

        if (!Object.is(nextState, state)) {
            const previousState = state;

            if (!replace) {
                state =
                    typeof nextState !== "object" || nextState === null
                        ? nextState
                        : Object.assign({}, state, nextState);
            } else {
                state = nextState;
            }
            listeners.forEach((listener) => listener(state, previousState));
        }
    };

    const getState = () => state;

    const subscribe = (listener) => {
        listeners.add(listener);
        return () => listeners.delete(listener);
    };

    const destroy = () => {
        listeners.clear();
    };

    const api = { setState, getState, subscribe, destroy };

    state = createState(setState, getState, api);

    return api;
};

state 是全局状态,listeners 是监听器。

然后 setState 修改状态、getState 读取状态、subscribe 添加监听器、destroy 清除所有监听器。

这些都很容易理解。

至于 replace,这是 zustand 在 set 状态的时候默认是合并,你也可以传一个 true 改成替换:

那如果状态变了,如何触发渲染呢?

useState 就可以。

这样写:

function useStore(api, selector) {
    const [, forceRender] = useState(0);
    useEffect(() => {
        api.subscribe((state, prevState) => {
            const newObj = selector(state);
            const oldobj = selector(prevState);

            if (newObj !== oldobj) {
                forceRender(Math.random());
            }
        });
    }, []);
    return selector(api.getState());
}

selector 说的是传入的这个函数:

我们用 useState 设置随机数来触发渲染。

监听 state 的变化,变了之后,根据新旧 state 调用 selector 函数的结果,来判断是否需要重新渲染。

然后定义 create 方法:

export const create = (createState) => {
    const api = createStore(createState);

    const useBoundStore = (selector) => useStore(api, selector);

    Object.assign(useBoundStore, api);

    return useBoundStore;
};

它就是先调用 createStore 创建 store。

然后返回 useStore 的函数,用于组件内调用。

测试下:

把 create 函数换成我们自己的,其余代码不变:

可以看到,功能依然正常:

我们的 my-zustand 已经能够完美替代 zustand 了。

全部代码如下:

import { useEffect, useState } from "react";

const createStore = (createState) => {
    let state;
    const listeners = new Set();

    const setState = (partial, replace) => {
        const nextState =
            typeof partial === "function" ? partial(state) : partial;

        if (!Object.is(nextState, state)) {
            const previousState = state;

            if (!replace) {
                state =
                    typeof nextState !== "object" || nextState === null
                        ? nextState
                        : Object.assign({}, state, nextState);
            } else {
                state = nextState;
            }
            listeners.forEach((listener) => listener(state, previousState));
        }
    };

    const getState = () => state;

    const subscribe = (listener) => {
        listeners.add(listener);
        return () => listeners.delete(listener);
    };

    const destroy = () => {
        listeners.clear();
    };

    const api = { setState, getState, subscribe, destroy };

    state = createState(setState, getState, api);

    return api;
};

function useStore(api, selector) {
    const [, forceRender] = useState(0);
    useEffect(() => {
        api.subscribe((state, prevState) => {
            const newObj = selector(state);
            const oldobj = selector(prevState);

            if (newObj !== oldobj) {
                forceRender(Math.random());
            }
        });
    }, []);
    return selector(api.getState());
}

export const create = (createState) => {
    const api = createStore(createState);

    const useBoundStore = (selector) => useStore(api, selector);

    Object.assign(useBoundStore, api);

    return useBoundStore;
};

60 多行代码。

其实,代码还可以进一步简化。

react 有一个 hook 就是用来定义外部 store 的,store 变化以后会触发 rerender:

有了这个 useSyncExternalStore 的 hook,我们就不用自己监听 store 变化触发 rerender 了:

可以简化成这样:

function useStore(api, selector) {
    function getState() {
        return selector(api.getState());
    }

    return useSyncExternalStore(api.subscribe, getState);
}

这样,my-zustand 就完美了。

import { useSyncExternalStore } from "react";

const createStore = (createState) => {
    let state;
    const listeners = new Set();

    const setState = (partial, replace) => {
        const nextState =
            typeof partial === "function" ? partial(state) : partial;

        if (!Object.is(nextState, state)) {
            const previousState = state;

            if (!replace) {
                state =
                    typeof nextState !== "object" || nextState === null
                        ? nextState
                        : Object.assign({}, state, nextState);
            } else {
                state = nextState;
            }
            listeners.forEach((listener) => listener(state, previousState));
        }
    };

    const getState = () => state;

    const subscribe = (listener) => {
        listeners.add(listener);
        return () => listeners.delete(listener);
    };

    const destroy = () => {
        listeners.clear();
    };

    const api = { setState, getState, subscribe, destroy };

    state = createState(setState, getState, api);

    return api;
};

function useStore(api, selector) {
    function getState() {
        return selector(api.getState());
    }

    return useSyncExternalStore(api.subscribe, getState);
}

export const create = (createState) => {
    const api = createStore(createState);

    const useBoundStore = (selector) => useStore(api, selector);

    Object.assign(useBoundStore, api);

    return useBoundStore;
};

有的同学可能会质疑,zustand 的源码就这么点么?

我们调试下就知道了:

点击 vscode 的 create a launch.json file,创建一个调试配置:

改下调试的端口,点击调试启动:

把 zustand 换成之前的,然后打个断点:

通过调试,可以看到 create 的实现如下:

而 useStore 的实现如下:

唯一的区别就是它用的是一个 shim 包里的,因为它要保证这个 hook 的兼容性。

所以说,我们通过 60 行代码实现的,就是一比一复刻的 zustand。

至此,zustand 还有一个非常大的优点就呼之欲出了:体积小。

一共也没多少代码,压缩后能多大呢?只有 1kb。

最后,zustand 有一个坑点要注意下:

当你用了 ts 并且用了中间件,那要这样写,第一次调用不传参数,第二次传:

源码里确实做了处理:

至于为什么要这样,官方解释说是为了好做中间件的 ts 类型处理:

反正功能是一样的,大家记住 ts + middleware 的场景换个写法就好了。

案例代码上传了小册仓库

总结

近几年出了很多可以替代 redux 的优秀状态管理库,zustand 是其中最优秀的一个。

它的特点有很多:体积小、简单、支持中间件扩展。

它的核心就是一个 create 函数,传入 state 来创建 store。

create 返回的函数可以传入 selector,取出部分 state 在组件里用。

它的中间件和 redux 一样,就是一个高阶函数,可以对 get、set 做一些扩展。

zustand 内置了 immer、persist 等中间件,我们也自己写了一个 log 的中间件。

zustand 本身的实现也很简单,就是 getState、setState、subscribe 这些功能,然后再加上 useSyncExternalStore 来触发组件 rerender。

一共也就 60 行代码。

这样一个简单强大、非常流行的状态管理库,你确定不自己手写一个试试么?

上次更新: 6/21/25, 9:42 AM
贡献者: YNight
Prev
46.ReactContext的性能缺点和解决方案
Next
48.原子化状态管理库Jotai