likes
comments
collection
share

Zustand:强大的 React 状态管理工具

作者站长头像
站长
· 阅读数 85

一、Zustand基本介绍:

1.官方定义

基于 Flux 模型实现的小型、快速和可扩展的状态管理解决方案,拥有基于 hooks 的舒适的API,非常地灵活且有趣.

不要因为它很娇小就忽略它,它拥有锋利的爪子,花费了大量时间用来处理常见的陷阱,比如出现在多个复杂渲染器之间的 zombie childreact concurrency、以及 context loss 问题. Zustand 可以作为 React 应用中的一个状态管理器来正确处理这些问题。

  • Zustand 在德语中是 state 状态的意思

2.基本使用

Step 1: 安装

npm install zustand # or yarn add zustand

Step 2: Store 初始化

创建的 store 是一个 hook,你可以放任何东西到里面:基础变量,对象、函数,状态必须不可改变地更新,set 函数合并状态以实现状态更新。

import { create } from 'zustand'

const useBearStore = create((set) => ({
  bears: 0,
  increasePopulation: () => set((state) => ({ bears: state.bears + 1 })),
  removeAllBears: () => set({ bears: 0 }),
}))

Step 3: Store 绑定组件,就完成了!

可以在任何地方使用钩子,不需要提供 provider。 基于 selector 获取您的目标状态,组件将在状态更改时重新渲染。

##### 选择目标状态:bears
function BearCounter() {
  const bears = useBearStore((state) => state.bears)
  return <h1>{bears} around here ...</h1>
}

##### 更新目标状态:bears
function Controls() {
  const increasePopulation = useBearStore((state) => state.increasePopulation)
  return <button onClick={increasePopulation}>one up</button>
}

二、对比其他状态管理框架

为什么是 zustand 而不是 redux?

通过列子对比两个状态库的区别:

Redux示例

import { createStore } from 'redux';
import { useSelector, useDispatch } from 'react-redux';
 
const initialState = {
  count: 0,
};
 
const reducer = (state = initialState, action) => {
  switch (action.type) {
    case 'INCREMENT':
      return { count: state.count + action.qty };
    case 'DECREMENT':
      return { count: state.count - action.qty };
    default:
      return state;
  }
};
 
const store = createStore(reducer);
 
const Component = () => {
  const count = useSelector((state) => state.count);
  const dispatch = useDispatch();
  // 使用dispatch来更新状态
};

Zustand示例

import { create } from 'zustand';
 
const useCountStore = create((set) => ({
  count: 0,
  increment: (qty) => set((state) => ({ count: state.count + qty })),
  decrement: (qty) => set((state) => ({ count: state.count - qty })),
}));
 
const Component = () => {
  const { count, increment, decrement } = useCountStore();
  // 直接调用increment和decrement来更新状态
};

为什么是 zustand 而不是 react Context?

  • 不依赖 react 上下文,引用更加灵活
  • 当状态发生变化时 重新渲染的组件更少
  • 集中的、基于action的状态管理

通过列子对比两个状态库的区别:

react Context示例

import React, { createContext, useContext, useState } from 'react';

// 创建上下文
const CounterContext = createContext();

// 创建提供者组件
function CounterProvider({ children }) {
  const [count, setCount] = useState(0);

  const increment = () => setCount((prevCount) => prevCount + 1);
  const decrement = () => setCount((prevCount) => prevCount - 1);

  const value = {
    count,
    increment,
    decrement,
  };

  return <CounterContext.Provider value={value}>{children}</CounterContext.Provider>;
}

// 在组件中使用上下文
function CounterComponent() {
  const { count, increment, decrement } = useContext(CounterContext);

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={increment}>Increment</button>
      <button onClick={decrement}>Decrement</button>
    </div>
  );
}

Zustand示例

import create from 'zustand';

// 创建状态存储
const useCounterStore = create((set) => ({
  count: 0,
  increment: () => set((state) => ({ count: state.count + 1 })),
  decrement: () => set((state) => ({ count: state.count - 1 })),
}));

// 在组件中使用状态
function CounterComponent() {
  const count = useCounterStore((state) => state.count);
  const increment = useCounterStore((state) => state.increment);
  const decrement = useCounterStore((state) => state.decrement);

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={increment}>Increment</button>
      <button onClick={decrement}>Decrement</button>
    </div>
  );
}

三、Zustand的性能优势

在选择React状态管理库时,我们常常会被各种库的特性和API所困惑。Zustand作为一款新兴的状态管理库,以其轻量、简单和灵活的特性脱颖而出,成为了许多React开发者的新宠。让我们来看看Zustand的几大优势是如何让React项目的状态管理变得更加高效和优雅的。

1、轻量级设计

Zustand的代码库非常小,gzip压缩后仅有1KB大小,对项目性能的影响几乎微乎其微。在如今这个对应用加载速度和性能要求越来越高的时代,选择一个轻量级的状态管理库尤为重要。Zustand恰好满足了这一需求,让你的项目保持轻量,同时也具备强大的状态管理能力。

2、简洁的API

Zustand提供了清晰而简洁的API,使得开发者可以迅速上手,轻松管理状态。这一点对于初学者或是希望简化项目复杂度的开发者来说尤其友好。Zustand的API设计遵循“少即是多”的原则,通过最少的学习就能达到快速开发的目的。

3、基于Hook的状态管理

Zustand利用了React的hook机制,通过创建自定义hook来访问和更新状态。这种方式与函数组件和hooks的编程模型无缝集成,使得状态管理自然而流畅。对于已经习惯了React hooks的开发者来说,使用Zustand进行状态管理将会感到非常自然和便捷。

4、易于集成

Zustand能够与其他React库(如Redux和MobX)无缝共存,这意味着你可以在不放弃现有库的情况下,逐渐过渡到Zustand。这为项目的状态管理提供了更多的灵活性和选择性。

5、完整的TypeScript支持

Zustand全面支持TypeScript,增强了项目的健壮性和类型安全。在当前软件开发趋势中,TypeScript的重要性日益凸显,Zustand的这一特性让它在众多状态管理库中更加突出。

6、灵活性与可扩展性

Zustand允许根据项目需求组织状态树,适应不同的项目结构。同时,Zustand引入了中间件的概念,通过插件来扩展其功能。无论是日志记录、持久化存储,还是异步操作,中间件都可以让状态管理变得更加灵活和可扩展。

总而言之,Zustand以其轻量、简洁、灵活的特性,为React项目的状态管理提供了一个高效且优雅的解决方案。无论是对于追求性能的高级开发者,还是希望简化开发流程的新手,Zustand都是一个值得尝试的选择。

多种状态管理工具优劣势对比

框架原理优点缺点
hooks context基于 react hook,开发者可实现内/外部存储1. 使用简单 2. 不需要引用第三方库,体积最小 3. 支持存储全局状态,但在复杂应用中不推荐 4. 不依赖 react 上下文,可在组件外调用(外部存储的条件下)1. context value发生变化时,所有用到这个context的组件都会被重新渲染,基于 content 维护的模块越多,影响范围越大。 2.依赖 Context Provider 包裹你的应用程序,修改 store 无法在应用最顶层(App.tsx 层级)触发渲染 3. 受ui框架约束(react) 4. 依赖hooks调用
react-reduxFlux思想,发布订阅模式,遵从函数式编程,外部存储1. 不依赖 react 上下文,可在组件外调用 2. 支持存储全局状态 3. redux 本身是一种通用的状态解决方案1. 心智模型需要一些时间来理解,特别是当你不熟悉函数式编程的时候 2. 依赖 Context Provider 包裹你的应用程序,修改 store 无法在应用最顶层(App.tsx 层级)触发渲染 3.受 ui 框架约束(react)
mobx观察者模式 + 数据截止,外部存储1. 使用简单,上手门槛低 2. 不依赖 react 上下文,可在组件外调用 3. 支持存储全局状态 4.通用的状态解决方案1.可变状态模型,某些情况下可能影响调试 2. 除了体积相对较大之外,笔者目前未感觉到较为明显的缺点,3.99M
zustandFlux思想,观察者模式,外部存储1. 轻量,使用简单,上手门槛低 2. 不依赖 react 上下文,可在组件外调用 3. 支持存储全局状态 4. 通用的状态解决方案1.框架本身不支持 computed 属性,但可基于 middleware 机制通过少量代码间接实现 computed ,或基于第三方库 zustand-computed 实现
jotai基于 react hook,内部存储1. 使用简单 2. 组件颗粒度较细的情况下,jotai性能更好 3.支持存储全局状态1. 依赖 react 上下文, 无法组件外调用,相对而言, zustand 在 react 环境外及全局可以更好地工作 2.受ui框架约束(react)
recoil进阶版 jotai,基于 react hook + provider context,内部存储相对于 jotai而言,会更重一些,但思想基本不变,拥有一些 jotai 未支持的特性及 api,如: 1.监听 store 变化 2. 针对 atom 的操作拥有更多的 api,编程上拥有更多的可能性,更加有趣拥有 jotai 所有的缺点,且相对于 jotai 而言: 1.使用 recoil 需要 < RecoilRoot > 包裹应用程序 2. 编写 selector 会复杂一些
valtio基于数据劫持,外部存储1. 使用简单,类mobx(类vue)的编程体验 2.支持存储全局状态 3.不依赖 react 上下文,可在组件外调用 4. 通用的状态解决方案1.可变状态模型,某些情况下可能影响调试 2.目前笔者没发现其它特别大的缺点,个人猜测之所以star相对zustand较少,是因为 valtio 的数据双向绑定思想与 react 存在冲突。

四、Zustand基本用法

1、获取所有内容

你可以获取整个 store,但请记住这样做会导致组件在每次状态变化时都重新渲染!

const state = useBearStore();

2、选择多个状态片段

默认情况下,Zustand 使用严格相等(old === new)来检测变化,这对于原子状态选择是高效的。

const nuts = useBearStore((state) => state.nuts);
const honey = useBearStore((state) => state.honey);

第二种写法,对象形式
  const {
    nuts,
    honey
  } = useBearStore((state) => ({
    nuts:state.nuts,
    honey:state.honey,
  }));  

如果你想构造一个包含多个状态选择的单个对象,类似于 Redux 的 mapStateToProps,你可以使用 useShallow 来防止当选择器输出根据浅相等不变时不必要的重新渲染。

import { create } from "zustand";
import { useShallow } from "zustand/react/shallow";

const useBearStore = create((set) => ({
  bears: 0,
  increasePopulation: () => set((state) => ({ bears: state.bears + 1 })),
  removeAllBears: () => set({ bears: 0 }),
}));

const { nuts, honey } = useBearStore(
  useShallow((state) => ({ nuts: state.nuts, honey: state.honey })),
);

3、覆盖状态

set 函数有第二个参数,默认为 false。而不是合并,它将替换状态模型。小心不要抹去你依赖的部分,如操作。

import omit from "lodash-es/omit";

const useFishStore = create((set) => ({
  salmon: 1,
  tuna: 2,
  deleteEverything: () => set({}, true), // 清除整个 store,包括操作
  deleteTuna: () => set((state) => omit(state, ["tuna"]), true),
}));

4、异步操作

当你准备好时,只需调用 set,zustand 不介意你的操作是异步的还是同步的。

const useFishStore = create((set) => ({
  fishies: {},
  fetch: async (pond) => {
    const response = await fetch(pond);
    set({ fishies: await response.json() });
  },
}));

5、在操作中读取状态

set 允许函数更新 set(state => result),但你依然可以通过 get 在外部访问状态。

const useSoundStore = create((set, get) => ({
  sound: "grunt",
  action: () => {
    const sound = get().sound;
    // ...
  },
}));

6、在组件外部读写状态和响应变化

有时你需要以非响应式的方式访问状态或对 store 进行操作。对于这些情况,结果钩子有附加到其原型的实用程序函数。

const useDogStore = create(() => ({ paw: true, snout: true, fur: true }));

const paw = useDogStore.getState().paw; // 获取非响应式最新状态
const unsub1 = useDogStore.subscribe(console.log); // 监听所有变化
useDogStore.setState({ paw: false }); // 更新状态,触发监听器
unsub1(); // 取消订阅监听器

7、使用带选择器的订阅

如果你需要使用选择器订阅,subscribeWithSelector 中间件会有所帮助。

import { subscribeWithSelector } from "zustand/middleware";

const useDogStore = create(
  subscribeWithSelector(() => ({ paw: true, snout: true, fur: true })),
);

const unsub2 = useDogStore.subscribe((state) => state.paw, console.log);

8、在没有 React 的情况下使用 zustand

Zustand 核心可以导入并在没有 React 依赖的情况下使用。唯一的区别是 create 函数不返回一个钩子,而是 API 实用程序。

import { createStore } from 'zustand/vanilla';

const store = createStore((set) => ...);
const { getState, setState, subscribe, getInitialState } = store;
export default store;

9、持久中间件

你可以使用任何一种存储来持久化你 store 里的数据。(其他中间件在demo中有体现)

import create from "zustand";
import { persist } from "zustand/middleware";

export const useStore = create(
  persist(
    (set, get) => ({
      fishes: 0,
      addAFish: () => set({ fishes: get().fishes + 1 }),
    }),
    {
      name: "food-storage", // 唯一键
      getStorage: () => sessionStorage, // (可选)默认使用'localStorage'
    }
  )
);

10、Redux 开发者工具

import { devtools } from 'zustand/middleware';

const usePlainStore = create(devtools((set) => ...)); // 使用了 Redux 开发者工具的 store

五、zustand 的工作原理

zustand = 发布订阅 + react hooks: Zustand:强大的 React 状态管理工具

  1. Vanilla 层是发布订阅模式的实现,提供了setState、subscribe 和 getState 方法,并且前两者提供了 selector 和 equalityFn 参数,以供在只有原生 JS 模式下的正常使用,但 React 的使用过程中基本只会用该模块的发布订阅功能。
  2. React 层是 Zustand 的核心,实现了 reselect 缓存和注册事件的 listener 的功能,并且通过 forceUpdate 对组件进行重渲染。
  3. 积极拥抱hooks,不需要使用context providers包裹应用,遵循大道至简的原则,上手简单;
// create 函数实现
// api 本质就是就是 createStore 的返回值,也就是 Vanilla 层的发布订阅器
const api: CustomStoreApi = typeof createState === 'function' ? createStore(createState) : createState

// 这里的 useIsomorphicLayoutEffect 是同构框架常用 API 套路,在前端环境是 useLayoutEffect,在 node 环境是 useEffect
useIsomorphicLayoutEffect(() => {
  const listener = () => {
    try {
      // 拿到最新的 state 与上一次的 compare 函数
      const nextState = api.getState()
      const nextStateSlice = selectorRef.current(nextState)
      // 判断前后 state 值是否发生了变化,如果变化调用 forceUpdate 进行一次强制刷新
      if (!equalityFnRef.current(currentSliceRef.current as StateSlice, nextStateSlice)) {
        stateRef.current = nextState
        currentSliceRef.current = nextStateSlice
        forceUpdate()
      }
    } catch (error) {
      erroredRef.current = true
      forceUpdate()
    }
  }
  // 订阅 state 更新
  const unsubscribe = api.subscribe(listener)
  if (api.getState() !== stateBeforeSubscriptionRef.current) {
    listener()
  }
  return unsubscribe
}, [])

我们首先从第 24 行 api.subscribe(listener) 开始,这里先创建了 listener 的订阅,这就使得任何的 setState 调用都会触发 listener 的执行,接着回到 listener 函数的内部,利用 api.getState() 拿到了最新 state,以及上一次的 compare 函数 equalityFnRef,然后执行比较函数后判断值前后是否发生了改变,如果改变则调用 forceUpdate 进行一次强制刷新。

这就是 zustand 渲染层的原理,简单而精巧,zustand 实现状态共享的方式本质是将状态保存在一个对象里

六、Zustand不足

1、写一个和 setState 一样的功能,需要 n 行代码。

思考如下场景,就是代码中原本的一个 [count, setCount] = useState(0)。想要把 count 和 setCount 都共享到 store 中,需要写上如下代码:

// 定义全局的 count, setCount
const useStore = create((set, get) =>({
    count: 0,
    setCount: () =>
        set((state) => ({
          count: state.count + 1,
        })),
}));

// 组件中使用 
const App = () => {
    const [count, setCount] = useStore((state) => [state.count, state.setCount]);

    return (
    /* some render */
    )
}

2、状态获取不够简洁

create 函数中,状态的获取就使用 get() 就能拿到所有状态,但是使用起来就是不够简洁。开发者往往习惯通过以下的方式获取:

const useStore = create((set, get) => ({
    count: 0,
    reportCount: () => {
        // 原本 zustand 可以通过如下方式取值
        const count = get().count;
        const { count } = get();
        // 但是可能更习惯这样的方式
        // 通过 string 获取
        const count = get('count');
        // 通过 selector 获取
        const count = get((state) => state.count);
    }
}))

3、devtools 中间件配合起来使用不适

zustand 的 set 有第二个参数,表示是否对全部状态进行覆盖,这一点使用下来笔者发现不仅没有使用场景,反而容易犯错误。

而为了配合 devtools 给 set 的行为加上名称,就需要 set(xxx, false, '名称'),可以说是非常不便了。

七、Zustand中间件

1.immer

immer 中间件提供了一种方便的方式来处理不可变状态的更新。在状态更新期间创建和管理不可变副本,以便更轻松地进行状态的修改。

import create from 'zustand';

const store = create(immer(/* initial state */));
store.setState((state) => {
  state.count += 1;
  state.user.name = 'John';
});

在上面的代码中,将初始状态作为参数传递给 immer 中间件。 在状态 store 中,可以直接修改状态对象,而不需要创建不可变副本或手动进行深层复制。通过调用 setState 方法并传递一个回调函数,可以直接对状态进行修改。并且还会自动批处理多个状态更新操作,以提高性能并减少不必要的重新渲染。

2.persist 缓存

把zustand里的数据持久化到localstorage或sessionStorage中,官方提供了中间件,用起来很简单,我想和大家分享的是,只持久化某个字段,而不是整个对象。

persist(() => initialFoodValue, { 
        name: "food store" , // 缓存名称
        storage: createJSONStorage(() => localStorage), //缓存方式
        partialize: (state) =>
          Object.fromEntries(
          Object.entries(state).filter(
            ([key]) => !["mouse"].includes(key)  //缓存排除mouse属性
          )
        )
      })

3.subscribeWithSelector 订阅

subscribeWithSelector 中间件提供了一种订阅状态的特定部分,并在该部分发生变化时执行相应操作的功能。

//创建
subscribeWithSelector(
      persist(() => initialFoodValue, { 
        name: "food store" , // 缓存名称
        storage: createJSONStorage(() => localStorage), //缓存方式
        partialize: (state) =>
          Object.fromEntries(
          Object.entries(state).filter(
            ([key]) => !["mouse"].includes(key)  //缓存排除mouse属性
          )
        )
      })
    ),
    
//调用
   useEffect(()=>{
    const unsub = useFoodStore.subscribe(
      (state)=> state.fish,
      (fish)=>{
        if(fish > 6){
          setBgColor('salmon');
         } else{
          setBgColor('lavender');
         }
      },
      {
        equalityFn: shallow,  //浅层比较
        fireImmediately: true,  // 订阅创建后立即执行一次
      }
      );
      return unsub;  //通过调用 `unsub` 函数,取消对状态变化的订阅
   },[])

4. devtools调试工具

当store里数据变得复杂的时候,可以使用浏览器插件(首先安装一个插件 Redux DevTools)来查看store里的数据,不过需要使用devtools中间件

devtools(
        //监听
        subscribeWithSelector( 
          // 缓存
          persist(
            (set, get) => ({ 
            cats: {
              bigCats: 0,
              smallCats: 0,
            },
            increaseBigCats: () =>
              set((state) => {
                state.cats.bigCats++;
              }, false,'setBigCats'), //函数名称
          {
            name: "cat store", // 缓存名称
          }
          )
        ),
        {
          enabled: true,  // 启用插件
          name: "cat store",  //在开发者工具中显示的名称
        }
      )

默认操作名称都是anonymous这个名字,如果我们想知道调用了哪个函数,可以给set方法传第三个参数,这个表示方法名,比如上文中的setBigCats

Zustand:强大的 React 状态管理工具

结语:

总的来说,Zustand 提供了一种简单、轻量级且灵活的状态管理解决方案,适用于小型到中型的 React 应用程序。它通过简化状态管理的复杂性和提供高性能的状态更新机制,帮助开发者更好地组织和共享状态,并提供了中间件支持以增强功能。

Zustand-demo地址: github.com/li-0801/Zus…

参考社区:

Zustand 官方文档地址 :docs.pmnd.rs/zustand/get…

Zustand 中文社区:awesomedevin.github.io/zustand-vue…

状态管理工具优劣势分析:github.com/AwesomeDevi…

转载自:https://juejin.cn/post/7371423114381443072
评论
请登录