React状态管理之手撕 Zustand

Zustand

什么是 Zustand?

Zustand 是一个轻量级的状态管理库,用于 JavaScript 应用程序,特别是在 React 生态系统中。它提供了一个简单、可扩展的解决方案来中心化和管理应用程序的状态。

为什么会有 Zustand

随着 React 应用程序的增长和复杂性的提升,开发者开始寻找更简单、更易于维护的状态管理解决方案。在 Zustand 出现之前,许多 React 项目使用 Redux 作为其主要的状态管理工具。Redux 有很多优点,比如可预测性、中心化状态和时间旅行调试功能,但是它也有一些缺点,比如:

  • 重度依赖样板代码(action、reducer、selector 等)
  • 学习曲线陡峭
  • 对于小到中型的项目来说可能过于复杂 因此,社区开始探索其他的状态管理解决方案,目的是减少样板代码、简化开发流程,并提供与 React 更自然的集成。

Zustand 解决了什么问题

Zustand 主要旨在解决以下问题:

  • 简化状态管理:提供一个更直观的 API,通过避免 Redux 那样的冗余样板代码,使得状态管理更加简洁和直接。

  • 更好的开发体验:通过使用 React Hooks,Zustand 使得在函数组件中访问和更新状态变得容易。

  • 无拘无束:Zustand 允许创建多个独立的 store,不强制要求"单一数据源",给予开发者更多的灵活性。

  • 性能优化:Zustand 允许组件仅订阅状态的一部分,从而减少不必要的渲染和提高性能。

  • 简单的状态共享:不需要复杂的上下文或提供者,状态可以跨组件和文件轻松共享。

  • 中间件和增强功能:支持中间件,使得开发者可以轻松添加日志记录、持久化存储等增强功能。

  • 适应现代 React 功能:考虑到了 React 的新特性,如 Concurrent Mode 和 Suspense,从而确保在现代 React 特性下的稳定性。

Zustand 上手用法

用法一

主要是针对只在 React 环境中使用的场景

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

const useBearStore = create((set, get, api) => ({
    bears: 0,
    requestData: '暂无数据',
    increasePopulation: () => set(state => ({bears: state.bears + 1})),
    removeAllBears: () => set({bears: 0}),
    // 获取异步数据
    getData: async () => {
        await new Promise(res => {
            setTimeout(res, 3000);
        });
        set({
            requestData: '数据请求成功',
        });
    },
}));

// index.jsx
import { useBearStore } from './store'

function App() {
    const {bears, increasePopulation, requestData, getData} = useBearStore();
    // const bears = useBearStore(store => bears);
    return (
        <div>
            <p>{bears}</p>
            <button onClick={increasePopulation}>增加</button>
            <p>{requestData}</p>
            <button onClick={getData}>获取异步数据</button>
        </div>
    );
}

用法二

针对有不在 React 环境的使用场景

javascript 复制代码
// store.js
import {useStore, createStore} from 'zustand';
const store = createStore(set => ({
    bears: 0,
    requestData: '暂无数据',
    increasePopulation: () => set(state => ({bears: state.bears + 1})),
    removeAllBears: () => set({bears: 0}),
    // 获取异步数据
    getData: async () => {
        await new Promise(res => {
            setTimeout(res, 3000);
        });
        set({
            requestData: '数据请求成功',
        });
    },
}));
const {getState, setState, subscribe, getInitialState} = store;
export {
    getState,
    store,
}

// index.jsx
import {getState, store} from './store'
function About() {
    const {getData} = getState();
    return <div onClick={getData}>About获取异步数据</div>;
}
function App() {
   const {bears, increasePopulation, requestData, getData} = useStore(store);
    return (
        <div>
            <p>{bears}</p>
            <button onClick={increasePopulation}>增加</button>
            <p>{requestData}</p>
            <button onClick={getData}>获取异步数据</button>
            <About />
        </div>
    );
}

export default App;

Zustand 和 Redux 数据流

Redux + react-redux 数据流

redux 本身和 react 并没有什么关系,需要用 react-redux 将 react 和 redux 连接起来。而 react-redux 的本质就是通过 context + connect这个高阶组件(在 connect 中消费store的数据并注入包裹的组件中),因此得到上图:

Provider 接受 store 作为生产的数据传,并包裹整个组件树方便消费数据。需要消费数据时,需要用 connect 将用到数据的组件包裹,connect 作为高阶组件,在内部消费store的数据通过props传给组件,同时 mapDispatchToProps也会将 dispatch 作为函数通过 props 传给组件,方便更新数据,最后订阅 store 的更新。 默认来说 dispatch 只能处理对象,所以可以用 saga、thunk等中间件来对 dispatch 进行增强,让他可以处理函数和 promise。当组件内部调用 dispatch 一个 action 时,就会执行对应的 reducer 来更新 store,更新完成后 store 也会发布一个通知触发组件的更新。

zustand 数据流

zustand 使用起来就简单多了,通过 hook 直接在组件中使用就行,组件就会订阅 store 的更新,当调用 set (同时天然支持异步函数的处理)方法更新 store 时,就会触发组件的更新。另外 zustand 还可以创建多个 store,使用起来更加灵活。zustand 也有中间件可以用,原理和 redux 的中间件差不多。

zustand 是非常契合 hooks 的一个状态管理工具。因为 zustand 和 react 连接就是通过 hook 的组合来实现的。

可以看到 useBearStore 就是一个自定义 hook,里面包含了一堆 hook 的组合。

总结

  • zustand 使用起来更简单,开发体验更好。
  • zustand 更契合 hooks 版本

50行代码撸一个 Zustand

useSyncExternalStore

先介绍一下这个 hook,你的多数组件只会从它们的 props),state 读取数据。然而,有时一个组件需要从一些 React 之外的 store 读取一些随时间变化的数据。这包括:

  • 在 React 之外持有状态的第三方状态管理库
  • 暴露出一个可变值及订阅其改变事件的浏览器 API

在组件顶层调用 useSyncExternalStore 以从外部 store 读取值。useSyncExternalStore一般来说接收两个参数:

  • subscribe:一个函数,接收一个单独的 callback 参数并把它订阅到 store 上。当 store 发生改变,它应当调用被提供的 callback。这会导致组件重新渲染。subscribe 函数会返回清除订阅的函数。
  • getSnapshot:一个函数,返回组件需要的 store 中的数据快照。在 store 不变的情况下,重复调用 getSnapshot 必须返回同一个值。如果 store 改变,并且返回值也不同了(用 Object.is 比较),React 就会重新渲染组件
javascript 复制代码
import { useSyncExternalStore } from 'react';  

import { todosStore } from './todoStore.js';  

function TodosApp() {  

const todos = useSyncExternalStore(todosStore.subscribe, todosStore.getSnapshot);  
    return (
        <div>{todos}</div>
    )
}

代码

ini 复制代码
import {useSyncExternalStore} from 'react';
function createStore(createState) {
    let state;
    const listeners = new Set();
    // 设置状态
    const setState = function setState(partial, replace) {
        const nextState = typeof partial === 'function' ? partial(state) : partial;
        if (!Object.is(nextState, state)) {
            const _previousState = state;
            // 更新state
            state = (replace != null ? replace : typeof nextState !== 'object' || nextState === null) ? nextState : Object.assign({}, state, nextState);
            // 发布更新通知
            listeners.forEach(listener => {
                return listener(state, _previousState);
            });
        }
    };
    // 获取state
    const getState = function getState() {
        return state;
    };
    // 订阅
    const subscribe = function subscribe(listener) {
        listeners.add(listener);
        return function () {
            return listeners.delete(listener);
        };
    };
    // 清除订阅
    const destroy = function destroy() {
        listeners.clear();
    };
    const api = {
        setState: setState,
        getState: getState,
        subscribe: subscribe,
        destroy: destroy,
    };
    state = createState(setState, getState, api);
    return api;
}

function useStore(api, selector) {
    const {getState, subscribe} = api;
    const slice = useSyncExternalStore(subscribe, getState);
    if(selector) {
        return selector(slice);
    }
    return slice;
}

function create(createState) {
    const api = typeof createState === 'function' ? createStore(createState) : createState;
    const useBoundStore = function useBoundStore(selector) {
        return useStore(api, selector);
    };
    Object.assign(useBoundStore, api);
    return useBoundStore;
}
export {
    createStore,
    useStore,
    create,
};

总结

如果可以选 React 的状态管理库,那么我选 zustand

参考

相关推荐
夏河始溢3 分钟前
一七八、Node.js PM2使用介绍
前端·javascript·node.js·pm2
记忆深处的声音4 分钟前
vue2 + Element-ui 二次封装 Table 组件,打造通用业务表格
前端·vue.js·代码规范
陈随易5 分钟前
兔小巢收费引发的论坛调研Node和Deno有感
前端·后端·程序员
熊的猫19 分钟前
webpack 核心模块 — loader & plugins
前端·javascript·chrome·webpack·前端框架·node.js·ecmascript
速盾cdn26 分钟前
速盾:vue的cdn是干嘛的?
服务器·前端·网络
四喜花露水1 小时前
Vue 自定义icon组件封装SVG图标
前端·javascript·vue.js
前端Hardy1 小时前
HTML&CSS: 实现可爱的冰墩墩
前端·javascript·css·html·css3
web Rookie2 小时前
JS类型检测大全:从零基础到高级应用
开发语言·前端·javascript
Au_ust2 小时前
css:基础
前端·css
帅帅哥的兜兜2 小时前
css基础:底部固定,导航栏浮动在顶部
前端·css·css3