2021 年的 React 状态管理小结


redux

redux 是 React 社区目前最流行的状态管理方案。

redux 的数据流

redux 是单向数据流。组件通过 dispatch 一个 action 来触发 store 的修改,然后由 store 的 reducer 函数计算出新的状态,由于组件监听了 store 的数据变化,组件会拿到最新的数据重新渲染。这种数据流动是单向的,特别清晰。

Redux 三个基本原则:

  1. 单一数据源。所有的状态都放到一个 store 里面,一个应用中一般只有一个 store
  2. 保持状态只读。在 Redux 中,只能通过 dispatch 一个 action 来修改 state
  3. 修改数据只能通过纯函数完成。

redux 基础

action: 是一个用于描述修改事件的普通对象。
reducer:规范 state 创建流程的函数。处理 action 的纯函数,通过传入 action 对象以及旧的 state,返回新的 state。
dispatch: 规范 setState 流程的函数。通过 dispatch 一个 action 来触发 store 的修改

连接 React (react-redux)

  1. 使用 Provider 在根组件 注入 Store
  2. 组件使用 connect() 方法连接 Redux

中间件

中间件(middleware)就是一个对 store 的 dispatch 进行修饰的函数。applyMiddleware 就是对 store.dispatch 做了层层包装,最后返回修改了 dispatch 之后的 store。

function applyMiddleware(middlewares) {
  let dispatch = store.dispatch;
  middlewares.forEach((middleware) => (dispatch = middleware(store)(dispatch)));
  return { ...store, dispatch };
}

redux-thunk 中间件就是加了判断 action 是否是一个函数的逻辑

function createThunkMiddleware(extraArgument) {
  return ({ dispatch, getState }) =>
    (next) =>
    (action) => {
      if (typeof action === 'function') {
        return action(dispatch, getState, extraArgument);
      }

      return next(action);
    };
}

const thunk = createThunkMiddleware();

redux-promise 则是判断 action.payload 是否是个 promise

一个简单实现

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

const appContext = React.createContext(null);
let state = undefined;
let reducer = undefined;
const listeners = [];

const setState = (newState) => {
  // console.log(newState);
  state = newState;
  listeners.map((fn) => fn(state));
};

const store = {
  getState() {
    return state;
  },
  dispatch: (action) => {
    setState(reducer(state, action));
  },
  subscribe(fn) {
    listeners.push(fn);
    return () => {
      const index = listeners.findIndex(fn);
      listeners.splice(index, 1);
    };
  },
};

let dispatch = store.dispatch;

const prevDispatch = dispatch;

// redux-thunk
dispatch = (action) => {
  if (typeof action === 'function') {
    action(dispatch);
  } else {
    prevDispatch(action);
  }
};

const preDispatch2 = dispatch;

// redux-promise
dispatch = (action) => {
  if (action.payload instanceof Promise) {
    action.payload.then((data) => {
      dispatch({
        ...action,
        payload: data,
      });
    });
  } else {
    preDispatch2(action);
  }
};

// 一个 middleware 就是一个函数,用来修饰 store 的 dispatch

export const createStore = (_reducer, initialState) => {
  state = initialState;
  reducer = _reducer;
  return store;
};

const changed = (oldState, newState) => {
  let changed = false;
  for (let key in oldState) {
    if (oldState[key] !== newState[key]) {
      changed = true;
    }
  }
  return changed;
};

// 将组件与全局状态连接
// connect 第一个参数封装读,第二个参数封装写,两次调用可以封装读写 connectors
export const connect = (selector, dispatchSelector) => (Component) => {
  return (props) => {
    const [, update] = useState({});

    const data = selector ? selector(state) : { state };
    const dispatchers = dispatchSelector
      ? dispatchSelector(dispatch)
      : { dispatch };

    useEffect(() => {
      return store.subscribe(() => {
        // 检测变化,精准更新
        const newData = selector ? selector(state) : { state };
        if (changed(data, newData)) {
          update({});
        }
      });
    }, [selector]);

    return <Component {...props} {...data} {...dispatchers} />;
  };
};

export const Provider = ({ store, children }) => {
  return <appContext.Provider value={store}>{children}</appContext.Provider>;
};

新的 redux-toolkit

  1. 减少了样板代码,配置更加简单
  2. reducer 内置 immer.js ,降低心智
  3. hooks 语法,内置 selector
  4. 异步请求:createAsyncThunk 和 RTK Query
  5. 规范化数据 createEntityAdapter

redux 复杂情况下的异步任务管理

在面对多个异步过程之间的并行、串行等复杂情况时,前文提到的 redux-thunk、redux-promise 等中间件不容易处理。而 redux-saga 提供了 all、race、takeEvery、takeLatest 等 effect 来指定多个异步过程的关系,是一个比较优秀的异步过程管理工具。个人认为相对 rxjs 这种比较成熟的异步过程管理库还是有一定差距,不过可测试性不错。复杂异步过程管理的中间件还有基于 rxjs 的 redux-observable。在 redux-saga 之上,还有 dva,集成了 redux、redux-saga、react-router-redux、react-router,建议只在复杂场景下考虑使用。

Mobx

Mobx 也是一种在 React 社区非常流行的状态管理库。它通过透明的函数响应式编程(transparently applying functional reactive programming - TFRP)使得状态管理变得简单和可扩展。

核心概念

MobX 区分了应用程序中的以下三个概念:

  1. State(状态)
  2. Actions(动作)
  3. Derivations(派生)
  1. 定义 State 并使其可观察。确保所有你想随时间改变的属性都被标记为 observable:
import { makeObservable, observable, action } from 'mobx';

class Todo {
  id = Math.random();
  title = '';
  finished = false;

  constructor(title) {
    makeObservable(this, {
      title: observable,
      finished: observable,
      toggle: action,
    });
    this.title = title;
  }

  toggle() {
    this.finished = !this.finished;
  }
}
  1. 使用 Action 更新 State

Action(动作) 是任意可以改变 State(状态) 的代码,比如用户事件处理、后端推送数据处理、调度器事件处理等等。

  1. 创建建 Derivations 以便自动对 State 变化进行响应

任何 来源是 State(状态) 并且不需要进一步交互的东西都是 Derivation(派生)。

Derivations 包括许多方式:

  • 用户界面
  • 派生数据 , 比如剩余未完成 todos 的数量
  • 后端集成 , 比如发送改变到服务器端

Mobx 区分了两种 Derivation :

  • Computed values,总是可以通过纯函数从当前的可观测 State 中派生。
  • Reactions, 当 State 改变时需要自动运行的副作用 (命令式编程和响应式编程之间的桥梁)

Computed values 类似 Vue 中的 computed,而 Reactions 类似 watch。

autorun 和 reaction 很相似。但是 autorun 里面的函数在第一次会立即执行一次,而 reaction 不会。

实现原理

同是响应式,与 Vue 类似,包括让数据变成响应式(可观察对象)、收集依赖、派发更新这几步。

  1. 用 Object.defineProperty 或者 Proxy 来拦截 observable 包装的对象属性的 get/set 。
  2. 在 autorun 或者 reaction 执行的时候,会触发依赖状态的 get,此时将 autorun 里面的函数和依赖的状态关联起来。也就是我们常说的依赖收集。
  3. 当修改状态的时候会触发 set,此时会通知前面关联的函数,重新执行他们。

新的状态管理库

在 react 生态圈,状态管理库不停地涌现,个人认为比较有代表性有以下几个。

Recoil

Meta 公司内部开发的状态管理库,目前还在实验阶段。

出于兼容性和简便性的考虑,相比使用外部的全局状态,使用 React 内置的状态管理能力是个最佳的选择。但是 React 有这样一些局限性:

  • 组件间的状态共享只能通过将 state 提升至它们的公共祖先来实现,但这样做可能导致重新渲染一颗巨大的组件树。
  • Context 只能存储单一值,无法存储多个各自拥有消费者的值的集合。
  • 以上两种方式都很难将组件树的顶层(state 必须存在的地方)与叶子组件 (使用 state 的地方) 进行代码分割。

Recoil 定义了一个有向图 (directed graph),正交同时又天然连结于你的 React 树上。状态的变化从该图的顶点(我们称之为 atom)开始,流经纯函数 (我们称之为 selector) 再传入组件。因此 Recoil 的核心概念就两个:atom 和 selector。

atom 是 Recoil 的最小粒度,一个 atom 代表一个状态,可以在任意组件中进行读写。

selector 是一个纯函数,类似于 Vue 和 Mobx 中的 computed 计算属性。它可以从 atom 或者其他 selector 里面来获取,selector 也可以被组件订阅,在变化的时候通知它们重新渲染。

如需在组件中使用 Recoil,则可以将 RecoilRoot 放置在父组件的某个位置。将他设为根组件为最佳:

import React from 'react';
import {
  RecoilRoot,
  atom,
  selector,
  useRecoilState,
  useRecoilValue,
} from 'recoil';

function App() {
  return (
    <RecoilRoot>
      <CharacterCounter />
    </RecoilRoot>
  );
}

生成一个 atom:

const textState = atom({
  key: 'textState', // unique ID (with respect to other atoms/selectors)
  default: '', // default value (aka initial value)
});

使用,类似 React Hooks:

function CharacterCounter() {
  return (
    <div>
      <TextInput />
      <CharacterCount />
    </div>
  );
}

function TextInput() {
  const [text, setText] = useRecoilState(textState);

  const onChange = (event) => {
    setText(event.target.value);
  };

  return (
    <div>
      <input type="text" value={text} onChange={onChange} />
      <br />
      Echo: {text}
    </div>
  );
}

Jotai

与 Recoil 类似,都是采用分散管理原子状态的设计模式。与 Recoil 相比,有以下几个优点:

  1. API 少,简洁易用
  2. 定义 Atom 时不用提供 key
  3. 对 TypeScript 的支持更好

使用 atom 生成一个 atom:

import { atom } from 'jotai';

const countAtom = atom(0);

然后使用 useAtom 即可像 useState 一样读和写

import { useAtom } from 'jotai';

function Counter() {
  const [count, setCount] = useAtom(countAtom);
  return (
    <h1>
      {count}
      <button onClick={() => setCount((c) => c + 1)}>one up</button>
    </h1>
  );
}

Jotai 最大的特点就是轻量、易用,很多项目其实并没有很复杂的状态需要管理,未必要使用 redux,可以考虑使用这个。

zustand

zustand 和 Jotai 一样也是 pmndrs 出品的,但是与 Jotai 分散式管理不同,zustand 还是集中统一管理,这点与 redux 类似。zustand 的优点在于不需要使用 context providers 来包裹组件。zustand 可以理解为是一个外部的世界,通过它的工厂函数创建的 hooks 与 React 世界连接。

工厂函数创建 store:

import create from 'zustand';

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

连接到组件:

function BearCounter() {
  const bears = useStore((state) => state.bears);
  return <h1>{bears} around here ...</h1>;
}

function Controls() {
  const increasePopulation = useStore((state) => state.increasePopulation);
  return <button onClick={increasePopulation}>one up</button>;
}

与 redux 相比,样板代码非常少。

官方参考

react-reduxjs-toolkit
redux-toolkit
redux-saga
dva
zustand
jotai

其他参考

Redux 异步流最佳实践
深入理解 redux 数据流和异步过程管理
zustand 状态管理器与观察者模式
还在学 Redux?不妨提前学下它以后的替代品!——Zustand 源码解析


文章作者: Angus
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Angus !
  目录