Recoil:Facebook 新一代的 React 状态管理库

本文主要介绍facebook出的状态管理库Recoil(非react官方)。

其优点

  1. 避免类似Redux和Mobx这样的库带来的开销。
  2. 规避Context 的局限性。

其缺点:

  1. 目前只支持hooks 。
  2. 处于实验阶段,稳定性有待观察。

引言

Redux

放一张很熟悉的图。redux的状态管理如下图所示。

Mobx

  • Observable State, 所有可以改变的值。

  • Derivation:

    • Computed Value(又称Derivation), 是可以用纯函数从当前可观察状态中衍生出的值。
  • Reaction, 与Computed Value类似也是基于Observable State 。当状态改变时需要自动发生的副作用,用来连接命令式编程和响应式编程,最终都需要实现I/O操作,例如发送请求,更新页面等。

  • Action, 所有修改Observable State的动作,用户事件,后端数据推送等。

  • 注:可变数据流。(如果需要Mutable方式管理react状态,可以参考 Mobx中文文档 [1])。

两者联系与区别:

  • 编程方式:redux 更加偏向函数式编程,Mobx思想上更加偏向面向对象编程和响应式编程。
  • 数据存储方式不同:Redux将数据保存在单一store中,Mobx将数据保存在分散的多个store中。
  • 状态存储的形式:
    • redux存储的js原生对象形式:需要手动追踪状态的变化。
  • Mobx会将该状态包装成一个可观察对象,并自动追踪这个状态的更新。
  • 数据是否是可变状态:Redux更多的偏向使用不可变状态,不能直接去修改它,而是应该使用纯函数返回一个新的状态。Mobx中的状态是可以直接修改的。 https://juejin.cn/post/6844903797085437966 [2]

State 与 Content

问题: State 与 Content 存在的问题

场景:有 List 和 Canvas 两个组件,List 中节点更新,Canvas 中对应的节点也更新。

第一种方法:将 State 传到公共父节点。

缺点: 会全量re-render。

第二种方法:给父节点加 Provider 在子节点加 Consumer,不过每多加一个 item 就要多一层 Provider。

一. 介绍:

在构建一个react应用时一个令人头痛的问题是状态管理。虽然目前有较为成熟的状态管理库如redux和Mobx,使用他们所带来的开销也是难以估量的。当然最理想的方法是使用react来进行状态管理。

但是这带来了以下三个问题。组件状态只能与其祖先组件进行共享,这可能会带来组件树中大量的重绘开销。Context 只能保存一个特定值而不是与其 Consumer 共享一组不确定的值。

以上两点导致组件树顶部组件(状态生产者)与组件树底部组件(状态消费者)之间的代码拆分变得非常困难 Recoil 在组件树中定义了一个正交且内聚的单向图谱。状态变更通过以下方法从图谱的底部(atoms)通过纯函数(selectors)进入组件。

思想:将组件中的状态单独抽离出来,构成一个独立于组件的状态树,树的底部是atom通过selectors进入组件。

如图所示。提供了一些无依赖的方法,这些方法像 React 局部状态一样暴露相同的 get/set 接口(简单理解为 reducers 之类的概念亦可)。

我们能够与一些 React 新功能(比如并发模式)兼容。状态定义是可伸缩和分布式的,代码拆分成为可能。

不用修改组件即可派生数据状态。派生数据状态支持同步和异步。把跳转看作一级概念,甚至可以对链接中的状态流转进行编码。

所以可以简单地使用向后兼容的方式来持久化整个应用的状态,应用变更时持久化状态也可以因此得以保留。可以把 Atom 想象为为一组 state 的集合,改变一个 Atom 只会渲染特定的子组件,并不会让整个父组件重新渲染。与Redux和Mobx相比,redux与Mobx 不能访问React内部调度的程序。而recoil在后台使用React本身的状态。

二. 主要概念

Atoms - 共享状态

组件可订阅的最小状态单元-可被定义和更新类似于setState中的state。(一般定义一些基础)

const todoListState = atom({
  key'todoListState'//key是RecoilRoot 作用域内唯一的
default: [],
});

Selector(derived state) - 纯函数

一个selector代表一个派生的状态(由基础的状态atom派生)。入参是Atoms/Selector类型的纯函数。当它的上游改变时,它会自动更新。其使用方法和Atom基本类似。

const fontSizeLabelState = selector({
  key'fontSizeLabelState',
  get({get}) => {
const fontSize = get(fontSizeState);
const unit = 'px';
return `${fontSize}${unit}`;
  },
  set: ({get, set},newValue) => {
      return set('',newValue)
  },
});
  • Key:  与atom 的key一样的作用具有唯一性。
  • Get属性:定义如何取值。是一个计算函数,可以使用get字段来访问输入的Atom和Selector。当其所依赖的状态更新时,改状态也会跟着更新。
  • Set :返回新的可写状态的可选函数。

注:只有同时具有get和set的selector才具备可读写属性。set: 设置原子值的函数。

相关hooks

  • useRecoilValue():对Atom/Selector进行读操作(有些Selector只有可读属性没有可写属性)。
function TodoList({
const todoList = useRecoilValue(todoListState);
return (
    <>
      <TodoItemCreator />
      {todoList.map((todoItem) => (
        <TodoItem key={todoItem.id} item={todoItem} />
      ))}
    </>

  );
}
  • useSetRecoilState():对Atom/Selector进行写操作。

其他相关hooks

function TodoItemCreator({
const [inputValue, setInputValue] = useState('');
const setTodoList = useSetRecoilState(todoListState);
const addItem = () => {
    setTodoList((oldTodoList) => [
      ...oldTodoList,
      {
        id: getId(),
        text: inputValue,
        isCompletefalse,
      },
    ]);
    setInputValue('');
  };
const onChange = ({target: {value}}) => {
    setInputValue(value);
  };
return (
    <div>
      <input type="text" value={inputValue} onChange={onChange} />
      <button onClick={addItem}>Add</button>
    </div>

  );
}
// utility for creating unique Id
let id = 0;
function getId({
return id++;
}
  • useRecoilState(): 对原子进行读写操作。
  • useResetRecoilState():重置原子的默认值。

useSetRecoilState 与 useRecoilState 的不同之处在于,数据流的变化不会导致组件 Rerende, useSetRecoilState仅仅是写入该原子, 没有订阅该原子以及原子的更新。

注:所有的Atom都是可读写的状态。

<RecoilRoot ...props>

全局的数据流管理需要在RecoilRoot作用域上才可以,被嵌套时最内层会嵌套外曾的作用域。

三. 异步处理:

  • Sync

同步状态下,只要上游的数据变了它就会自动改变。如上文所示。

  • Async

只需要get函数返回的是一个promise即可。Recoil 对于异步处理是需要与React Suspense[3] 一起来处理异步的数据。如果任何依赖项发生更改,将重新计算选择器并执行新查询。会对结果进行缓存,如果输入一样将不会进行查询,对相同的输入也只会进行一次查询。

  • 例子:
const currentUserNameQuery = selector({
  key'CurrentUserName',
  getasync ({get}) => {
const response = await myDBQuery({
      userIDget(currentUserIDState),
    });
return response.name;
  }
});
function CurrentUserInfo() {
const userName = useRecoilValue(currentUserNameQuery);
return <div>{userName}</div>;
}
//处于pending状态会将promise抛出,交给suspense来处理。
function MyApp({
return (
    <RecoilRoot>
      <React.Suspense fallback={<div>Loading...</div>}>
        <CurrentUserInfo />
      </React.Suspense>
    </RecoilRoot>

  );
}
  • 异步状态可以被 Suspence 捕获。
  • 异步过程报错可以被ErrorBoundary 捕获。

不使用Suspence

除了使用Suspence来处理异步的selector,还可以使用useRecoilValueLoadable()这个Api在当前组件。

function UserInfo({userID}{
const userNameLoadable = useRecoilValueLoadable(userNameQuery(userID));
switch (userNameLoadable.state) {
case 'hasValue':
return <div>{userNameLoadable.contents}</div>;
case 'loading':
return <div>Loading...</div>;
case 'hasError':
throw userNameLoadable.contents;
  }
}

可以通过state的状态来读取到异步的请求。

依赖外部变量进行查询

有些时候需要使用其他参数(而不是Atom/Select)来进行数据查询。

const userNameQuery = selectorFamily({
  key'UserName',
  get(userID) => async ({get}) => {
const response = await myDBQuery({userID});
if (response.error) {
throw response.error;
    }
return response.name;
  },
});
function UserInfo({userID}{
const userName = useRecoilValue(userNameQuery(userID));
return <div>{userName}</div>;
}

四. Utils

  • atomFamily()
    • 与autom()类似,不同的是atomFamily返回一个函数,该函数接受一个参数。可以根据这个参数来提供不同的Atom.
const elementPositionStateFamily = atomFamily({
  key: 'ElementPosition',
default: [0, 0],
});
function ElementListItem({elementID}) {
const position = useRecoilValue(elementPositionStateFamily(elementID));
return (
    <div>
      Element: {elementID}
      Position: {position}
    </div>
  );
}
  • 默认值可以根据传入的参数进行改变。
const myAtomFamily = atomFamily({
  key: ‘MyAtom’,
  default: param => defaultBasedOnParam(param),
});
  • selectorFamily()

  • 与Selector类似,但是可以将参数传递给set和get属性。

const myNumberState = atom({
  key: 'MyNumber',
default: 2,
});
const myMultipliedState = selectorFamily({
  key: 'MyMultipliedNumber',
  get: (multiplier) => ({get}) => {
return get(myNumberState) * multiplier;
  },
// optional set
  set: (multiplier) => ({set}, newValue) => {
set(myNumberState, newValue / multiplier);
  },
});
function MyComponent() {
// defaults to 2
const number = useRecoilValue(myNumberState);
// defaults to 200
const multipliedNumber = useRecoilValue(myMultipliedState(100));
return <div>...</div>;
}
  • 那么就可以通过这样将其依赖的值传递进去,从而进行数据查询。

五. 与Hox状态管理库相比

  1. 与hox相比:
    1. Recoi由facebook1. 来自facebook官方实验项目, 仍然处于可观察。2. Api较多。
    2. hox由1. 蚂蚁金服来维护的,处于相对稳定的状态。2. Api较少。

总结:

Recoil 将应用中的状态抽离出来组成一个状态树,通过selector来与组件进行沟通。其与App中的组件呈正交性。优点:Recoil 在后台使用的是React本身的状态。使用方式上完全支持hooks。未来会是一个值得期待的状态管理框架。

参考文献:

  1. Recoil 文档 [4]
  2. Recoil [5]
  3. You Might Not Need Redux [6]
  4. YouTube-Recoil [7]
  5. Mobx中文文档 [8]
  6. 带你走进Mobx的原理 [9]
  7. 你需要Mobx还是Redux? [10]

参考资料

[1]

Mobx中文文档: https://cn.mobx.js.org/

[2]

https://juejin.cn/post/6844903797085437966: https://juejin.cn/post/6844903797085437966

[3]

React Suspense: https://reactjs.org/docs/concurrent-mode-suspense.html

[4]

Recoil 文档: https://recoil.js.cn/docs/guides/asynchronous-data-queries

[5]

Recoil : https://bytedance.feishu.cn/wiki/wikcnrGEa9YON5PqlxC7sMJSymc

[6]

You Might Not Need Redux: https://medium.com/@dan_abramov/you-might-not-need-redux-be46360cf367

[7]

YouTube-Recoil: https://www.youtube.com/watch?v=_ISAA_Jt9kI

[8]

Mobx中文文档: https://cn.mobx.js.org/

[9]

带你走进Mobx的原理: https://juejin.cn/post/6844903797085437966#heading-6

[10]

你需要Mobx还是Redux?: https://juejin.cn/post/6844903562095362056

本文分享自微信公众号 - 前端从进阶到入院(code_with_love)。
如有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。

展开阅读全文
打赏
0
0 收藏
分享
加载中
更多评论
打赏
0 评论
0 收藏
0
分享
返回顶部
顶部