Contents

状态管理

关于react 的状态管理

Provider + Context(useContext)

一个完整的例子

import React, {
  createContext,
  Dispatch,
  SetStateAction,
  useState,
} from 'react';
export interface IState {
  theme: string;
  color: string;
}

interface Props {
  count: number;
  setCount: Dispatch<SetStateAction<number>>;
}

const Context = createContext<Props>({ count: 0, setCount: () => {} });

const Example = () => {
  console.log('Example');
  const [count, setCount] = useState(0);
  return (
    <Context.Provider value={{ count, setCount }}>
      <button
        onClick={() => {
          setCount(v => v + 1);
        }}
      >
        add
      </button>
      <Component2 />
    </Context.Provider>
  );
};

function Component2() {
  console.log('Component2');
  return (
    <>
      <h1>Component 2</h1>
      <Component3 />
    </>
  );
}

function Component3() {
  console.log('Component3');
  return (
    <>
      <h1>Component 3</h1>
      <Component4 />
    </>
  );
}

function Component4() {
  console.log('Component4');
  const context = React.useContext(Context);
  return (
    <>
      <h1>{context.count}</h1>
      <button
        onClick={() => {
          context.setCount(v => v + 1);
        }}
      >
        add
      </button>
    </>
  );
}

export default Example;

context 可以实现基础的状态共享。步骤:

  1. 通过createContext创建一个Context,并设置默认值。
  2. Context 中的值都在 Provider 的作用域下有效。所以在最外层包一个 Provider,value中设置传递值。
  3. 如果子组件需要时,则通过 React.useContext拿到Context值。

缺点:

  1. 重复渲染,性能差

当 Context 中的某个属性发生变化时,所有使用该 Context 的子组件都会被重新渲染,即使某些组件可能并未使用到这个属性。

这种问题有两种解决方法:

  1. React.memo 和 shouldComponentUpdate 这两种方式来解决。

  2. 将一个大型的 Context 拆分成多个小的 Context,可以精细地控制每个 Context 的更新,从而减少整个应用程序的重新渲染次数,提高性能和用户体验。

  1. 数据变更方法难以维护

为了保证数据变更方法的可维护性与 action 的不变性,有两种方法但各有问题:使用自定义 hooks,但为了不会重复渲染,每个修改方法需要使用useCallback,需要声明依赖,会造成极大的心智负担。使用useReducer,不支持异步函数、不支持内部的 reducer 互相调用,不支持和其他 state 联动(比如要当参数传进去才可用)。

  1. 使用范围限制

context的store只能在react中使用,无法在外部函数使用,例如请求函数中需要store中的属性,不能直接在请求函数中调用,就只能通过react中作为参数传递。

  1. 无法处理异步请求。

对于异步的逻辑,Context API并没有提供任何API,需要自己做封装。

  1. 无法处理数据间的联动

Context API并没有提供API来生成派生状态,同样也需要自行去封装一些方法来实现。

Zustand

优点:

状态共享

context需要在最外层包一个 Provider。 Context 中的值都在 Provider 的作用域下有效。而 zustand默认是单例模式, 不需要 Provider。直接声明一个 hooks 式的 useStore 后就可以在不同组件中进行调用。

如果需要多实例的话,zustand 也提供了对应的 Provider 的书写方式

状态变更

  1. 函数可以直接写,完全不用区分同步或者异步。

  2. zustand 会默认将所有的函数保持同一引用。用 zustand 写的方法,默认都不会造成额外的重复渲染。

  3. 更好的状态内聚性,可以互相调用,不用再重新声明函数。

状态派生

  1. zustand 用了类似 redux selector 的方法,实现相应的状态派生。
  2. 可以实现只有监听的属性变化,才会重新渲染
// zustand 的 selector 用法

// 写法1
const App = () => {
  const url = useStore( s => URL_HITU_DS_BASE(s.name || ''));
  // ...
}

// 写法2 将 selector 单独抽为函数
export const dsUrlSelector = (s) => URL_HITU_DS_BASE(s.name || '');
const App = () => {
  const url = useStore(dsUrlSelector);
  // ...
}

状态组合

可以将相关的状态进行组合,结合中间件实现对单个状态组合进行一些操作,例如对状态组合实现持久化,缓存到localStorage

多环境集成

可以不在 react 环境内直接获取状态数据

// 官方示例

// 1. 创建Store
const useDogStore = create(() => ({ paw: true, snout: true, fur: true }))

// 2. react 环境外直接拿值
const paw = useDogStore.getState().paw

// 3. 提供外部事件订阅
const unsub1 = useDogStore.subscribe(console.log)

// 4. react 世界外更新值
useDogStore.setState({ paw: false })

const Component = () => {
  // 5. 在 react 环境内使用
  const paw = useDogStore((state) => state.paw)

引用:

谈谈复杂应用的状态管理(上):为什么是 Zustand

https://github.com/pmndrs/zustand