当前期刊数: 152

1 引言

Recoil 是 Facebook 公司出的数据流管理方案,有一定思考的价值。

Recoil 是基于 Immutable 的数据流管理方案,这也是它值得被拿出来看的最重要原因,如果要用 Mutable 方式管理 React 数据流,直接看 mobx-react 就足够了。

然而 React Immutable 特性带来的可预测性非常利于调试和维护:

  1. 断点调试时变量的值与当前执行位置无关,已创建过的值不会突然 Mutable 突变,非常可预测。
  2. 在 React 框架下组件更新机制单一,只有引用变化才触发重渲染,而没有 Mutable 模式下 ForceUpdate 的心智负担。

当然 Immutable 模式下存在一定编码心智负担,所以各有优劣。

但 Recoil 和 Redux 一样,并不代表 React 官方数据流管理方案,因此不用带着官方光环去看它。

2 简介

Recoil 解决 React 全局数据流管理的问题,采用分散管理原子状态的设计模式,支持派生数据与异步查询,在基本功能上可以覆盖 Redux。

状态作用域

和 Redux 一样,全局数据流管理需要存在作用域 RecoilRoot

1
2
3
4
5
6
7
8
9
10
import React from "react";
import { RecoilRoot } from "recoil";

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

RecoilRoot 在被嵌套时,最内层的 RecoilRoot 会覆盖外层的配置及状态值。

定义数据

与 Redux 集中定义 initState 不同,Recoil 采用 atom 以分散方式定义数据:

1
2
3
4
const textState = atom({
key: "textState",
default: "",
});

其中 key 必须在 RecoilRoot 作用域内唯一,也可以认为是 state 树打平时 key 必须唯一的要求。

default 定义默认值,既然数据定义分散了,默认值定义也是分散的。

读取数据

与 Redux 的 Connect 或 useSelector 类似,Recoil 采用 Hooks 方式读取数据:

1
2
3
4
5
import { useRecoilValue } from "recoil";

function App() {
const text = useRecoilValue(textState);
}

useRecoilValueuseSetRecoilState 都可以获取数据,区别是 useRecoilState 还可以获取写数据的函数:

1
2
3
4
5
import { useRecoilState } from "recoil";

function App() {
const [text, setText] = useRecoilState(useRecoilState);
}

修改数据

与 Redux 集中定义纯函数 reducer 修改数据不同,Recoil 采用 Hooks 方式写数据。

除了上面提到的 useRecoilState 之外,还有一个 useSetRecoilState 可以仅获取写函数:

1
2
3
4
5
import { useSetRecoilState } from "recoil";

function App() {
const setText = useSetRecoilState(useRecoilState);
}

useSetRecoilStateuseRecoilStateuseRecoilValue 的不同之处在于,数据流的变化不会导致组件 Rerender,因为 useSetRecoilState 仅写不读。

这也导致 Recoil API 偏多被诟病,这也是 Immutable 模式下存的编码心智负担,虽然很好理解,但也只有 useSelector 或 Recoil 这样拆分 API 的方式可以解决。

另外还提供了 useResetRecoilState 重置到默认值并读取。

仅读不订阅

与 ReactRedux 的 useStore 类似,Recoil 提供了 useRecoilCallback 用于只读不订阅场景:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import { atom, useRecoilCallback } from "recoil";

const itemsInCart = atom({
key: "itemsInCart",
default: 0,
});

function CartInfoDebug() {
const logCartItems = useRecoilCallback(async ({ getPromise }) => {
const numItemsInCart = await getPromise(itemsInCart);

console.log("Items in cart: ", numItemsInCart);
});
}

useRecoilCallback 通过回调方式定义要读取的数据,这个数据变化也不会导致当前组件重渲染。

派生值

与 Mobx computed 类似,recoil 提供了 selector 支持派生值,这是比较有特色的功能:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import { atom, selector, useRecoilState } from "recoil";

const tempFahrenheit = atom({
key: "tempFahrenheit",
default: 32,
});

const tempCelcius = selector({
key: "tempCelcius",
get: ({ get }) => ((get(tempFahrenheit) - 32) * 5) / 9,
set: ({ set }, newValue) => set(tempFahrenheit, (newValue * 9) / 5 + 32),
});

function TempCelcius() {
const [tempF, setTempF] = useRecoilState(tempFahrenheit);
const [tempC, setTempC] = useRecoilState(tempCelcius);
}

selector 提供了 getset 分别定义如何赋值与取值,所以其与 atom 定义一样可以被 useRecoilState 等三套 API 操作,这里甚至不用看源码就能猜到,atom 应该是基于 selector 的一个特定封装。

异步读取

基于 selector 可以实现异步数据读取,只要将 get 函数写成异步即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
const currentUserNameQuery = selector({
key: "CurrentUserName",
get: async ({ get }) => {
const response = await myDBQuery({
userID: get(currentUserIDState),
});
if (response.error) {
throw response.error;
}
return response.name;
},
});

function CurrentUserInfo() {
const userName = useRecoilValue(currentUserNameQuery);
return <div>{userName}</div>;
}

function MyApp() {
return (
<RecoilRoot>
<ErrorBoundary>
<React.Suspense fallback={<div>Loading...</div>}>
<CurrentUserInfo />
</React.Suspense>
</ErrorBoundary>
</RecoilRoot>
);
}
  1. 异步状态可以被 Suspense 捕获。
  2. 异步过程报错可以被 ErrorBoundary 捕获。

如果不想用 Suspense 阻塞异步,可以换 useRecoilValueLoadable 这个 API 在当前组件内管理异步状态:

1
2
3
4
5
6
7
8
9
10
11
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;
}
}

依赖外部变量

reselect 一样,Recoil 也面临状态管理不纯粹的问题,即数据读取依赖外部变量,这样会面临较为复杂的缓存计算问题,甚至还出现了 re-reselect 库。

因为 Recoil 本身是原子化状态管理的,所以这个问题相对好解决:

1
2
3
4
5
6
7
8
9
10
const myMultipliedState = selectorFamily({
key: "MyMultipliedNumber",
get: (multiplier) => ({ get }) => {
return get(myNumberState) * multiplier;
},
});

function MyComponent() {
const number = useRecoilValue(myMultipliedState(100));
}

当外部传参 multiplier 与依赖值 myNumberState 不变时,就不会重新计算。

Recoil 在 getset 函数定义 Atom 时,内部会自动生成依赖,这个部分做的比较好。

依赖外部变量使用了 Family 后缀,比如 selector -> selectorFamily;atom -> atomFamily。

3 精读

Recoil 以原子化方式对状态进行分离管理,确实比较契合 Immutable 的编程模式,尤其在缓存处理时非常亮眼,但编程领域中,优势换一个角度看往往就变成了劣势,我们还是要客观评价一下 Recoil。

Immutable 心智负担

API 较多,在简介中也提到了,这可能是 Immutable 自带的硬伤,而不仅仅是 Recoil 的问题。

Immutable 模式中,对数据流只有读与写两种诉求,而申明式编程讲究的是数据变化后 UI 自动 Rerender,那么对数据的读自然而然就被赋予了订阅其变化后触发 Rerender 的期待,但是写与读不同,为什么 setState 强调用回调方式写数据?因为回调方式的写不依赖读,有写诉求的组件没必要与读挂上钩,也就是写组件的地方不一定要订阅对应数据。

Recoil 提供了 useRecoilState 作为读写双重 API,仅在既读又写的场景使用,而 useRecoilValue 仅仅是为了简化 API,替换为 useRecoilState 不会有性能损失,而 useSetRecoilValue 则必须认真对待,在仅写不读的场景必须严格使用这个 API。

useState 为什么默认是读写的?因为 useState 是单组件状态管理的场景,一个定义在组件内的状态不可能只写不读,但 Recoil 是全局状态解决方案,读写分离的场景下,对于只写的组件很有必要脱离对数据的订阅实现性能最大化。

条件访问数据

这也是 Hooks 的通病,由于 Hooks 不能写在条件语句中,因此要利用 Hooks 获取一个带有条件判断的数据时,必须回到 selector 模式:

1
2
3
4
5
6
7
8
9
10
const articleOrReply = selectorFamily({
key: "articleOrReply",
get: ({ isArticle, id }) => ({ get }) => {
if (isArticle) {
return get(article(id));
}

return get(reply(id));
},
});

这样的代码其实挺冗余的,其实在 Mutable 模式下可以 isArticle ? store.articles[id] : store.replies[id] 就能搞定的模式,必须单独抽一个 selector 出来写上头十行代码,显得非常繁琐。

Recoil 的本质

从 Hooks API 到派生值,这两个核心特点恰巧是对 Context 与 useMemo 的封装。

首先基于 Hooks 的 useContext 已经足够轻量易用,可以认为 atomuseRecoilStateuseRecoilValueuseSetRecoilValue 分别对应封装后的 createContextuseContext

再看 useMemo,大部分情况我们可以利用 useMemo 造出派生值,这对应了 Recoil 的 selectorselectorFamily

所以 Recoil 本质更像一个模式化封装库,针对数据驱动易于数据原子化管理的场景,并做到高性能。

3 总结

无论你用不用 Recoil,我们都可以从 Recoil 这儿学到 React 状态管理的基本功:

  1. 对象的读与写分离,做到最优按需渲染。
  2. 派生的值必须严格缓存,并在命中缓存时引用保证严格相等。
  3. 原子存储的数据相互无关联,所有关联的数据都使用派生值方式推导。

讨论地址是:精读《recoil》· Issue ##251 · dt-fe/weekly

如果你想参与讨论,请 点击这里,每周都有新的主题,周末或周一发布。前端精读 - 帮你筛选靠谱的内容。

关注 前端精读微信公众号

版权声明:自由转载-非商用-非衍生-保持署名(创意共享 3.0 许可证


本站由 钟意 使用 Stellar 1.28.1 主题创建。
又拍云 提供CDN加速/云存储服务
vercel 提供托管服务
湘ICP备2023019799号-1
总访问 次 | 本页访问