Recoil:Facebook 新一代的 React 状态管理库
共 9716字,需浏览 20分钟
·
2021-04-19 14:56
大厂技术 坚持周更 精选好文
本文主要介绍facebook出的状态管理库Recoil(非react官方)。
其优点
避免类似Redux和Mobx这样的库带来的开销。 规避Context 的局限性。
其缺点:
目前只支持hooks 。 处于实验阶段,稳定性有待观察。
引言
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,
isComplete: false,
},
]);
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',
get: async ({get}) => {
const response = await myDBQuery({
userID: get(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状态管理库相比
与hox相比: Recoi由facebook1. 来自facebook官方实验项目, 仍然处于可观察。2. Api较多。 hox由1. 蚂蚁金服来维护的,处于相对稳定的状态。2. Api较少。
总结:
Recoil 将应用中的状态抽离出来组成一个状态树,通过selector来与组件进行沟通。其与App中的组件呈正交性。优点:Recoil 在后台使用的是React本身的状态。使用方式上完全支持hooks。未来会是一个值得期待的状态管理框架。
参考文献:
Recoil 文档[4] Recoil [5] You Might Not Need Redux[6] YouTube-Recoil[7] Mobx中文文档[8] 带你走进Mobx的原理[9] 你需要Mobx还是Redux?[10]
参考资料
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