@@ -0,0 +1,284 @@
## 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 ] = useRecoilValue ( useRecoilState ) ;
}
```
### 修改数据
与 Redux 集中定义纯函数 `reducer` 修改数据不同,Recoil 采用 Hooks 方式写数据。
除了上面提到的 `useRecoilState` 之外,还有一个 `useSetRecoilState` 可以仅获取写函数:
``` jsx
import { useSetRecoilState } from "recoil" ;
function App ( ) {
const setText = useSetRecoilValue ( 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))