## 1 引言

[Recoil](https://recoiljs.org/) 是 Facebook 公司出的数据流管理方案，有一定思考的价值。

Recoil 是基于 Immutable 的数据流管理方案，这也是它值得被拿出来看的最重要原因，如果要用 Mutable 方式管理 React 数据流，直接看 [mobx-react](https://github.com/mobxjs/mobx-react) 就足够了。

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

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

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

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

## 2 简介

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

### 状态作用域

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

```jsx
import React from "react";
import { RecoilRoot } from "recoil";

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

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

### 定义数据

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

```jsx
const textState = atom({
  key: "textState",
  default: "",
});
```

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

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

### 读取数据

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

```jsx
import { useRecoilValue } from "recoil";

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

`useRecoilValue` 与 `useSetRecoilState` 都可以获取数据，区别是 `useRecoilState` 还可以获取写数据的函数：

```jsx
import { useRecoilState } from "recoil";

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

### 修改数据

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

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

```jsx
import { useSetRecoilState } from "recoil";

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

`useSetRecoilState` 与 `useRecoilState`、`useRecoilValue` 的不同之处在于，数据流的变化不会导致组件 Rerender，因为 `useSetRecoilState` 仅写不读。

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

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

### 仅读不订阅

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

```jsx
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` 支持派生值，这是比较有特色的功能:

```jsx
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` 提供了 `get`、`set` 分别定义如何赋值与取值，所以其与 `atom` 定义一样可以被 `useRecoilState` 等三套 API 操作，这里甚至不用看源码就能猜到，`atom` 应该是基于 `selector` 的一个特定封装。

### 异步读取

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

```jsx
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 在当前组件内管理异步状态：

```jsx
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 本身是原子化状态管理的，所以这个问题相对好解决：

```jsx
const myMultipliedState = selectorFamily({
  key: "MyMultipliedNumber",
  get: (multiplier) => ({ get }) => {
    return get(myNumberState) * multiplier;
  },
});

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

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

Recoil 在 `get` 与 `set` 函数定义 `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` 模式：

```jsx
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` 已经足够轻量易用，可以认为 `atom` 与 `useRecoilState`、`useRecoilValue`、`useSetRecoilValue` 分别对应封装后的 `createContext` 与 `useContext`。

再看 `useMemo`，大部分情况我们可以利用 `useMemo` 造出派生值，这对应了 Recoil 的 `selector` 和 `selectorFamily`。

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

## 3 总结

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

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

> 讨论地址是：[精读《recoil》· Issue #251 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/251)

**如果你想参与讨论，请 [点击这里](https://github.com/dt-fe/weekly)，每周都有新的主题，周末或周一发布。前端精读 - 帮你筛选靠谱的内容。**

> 关注 **前端精读微信公众号**

<img width=200 src="https://img.alicdn.com/tfs/TB165W0MCzqK1RjSZFLXXcn2XXa-258-258.jpg">

> 版权声明：自由转载-非商用-非衍生-保持署名（[创意共享 3.0 许可证](https://creativecommons.org/licenses/by-nc-nd/3.0/deed.zh)）
