Recoil 浅学
Recoil 是 Facebook 推出的一款状态管理库(目前仍处于实验性 22.02),使用 React 内置的状态管理。
优点:
- 避免类似 Redux 和 Mobx 等状态管理库带来的开销。
- 规避 Context 的局限性
缺点:
- 目前只支持 hooks。
Recoil 定义了一个有向图 (directed graph),正交同时又天然连结于你的 React 树上。从 atom
出发,流经 selector
再传入组件。可以把 atom
想象为一组 state
的集合,改变一个 atom
只会渲染特定的子组件,并不会让整个父组件重新渲染。与 Redux 和 Mobx 相比,redux 与 Mobx 不能访问 React 内部调度的程序。而 recoil 在后台使用 React 本身的状态。
安装
npm install recoil
注意
Recoil 构建是没有转译成 ES5 的,也就是说如果你需要兼容 ES5,需要安装 Babel
进行转译,但并不建议,因为 Recoil 依赖于 Map
和 Set
类型以及其他 ES6 的特性。使用 polyfill 来模拟这些特性可能会导致性能大大降低。
核心概念
Atom 是组件可以订阅的 state 单位。selector 可以同步或异步改变此 state。
key 值必须保证在整个应用中相对 atom 和 selector 里是唯一的。
Atom
Atom 是状态的单位,可订阅也可更新;当 Atom 更新时,每个被订阅的组件都将重新渲染。
💡定义一个 atom
// 你可以把它理解为 React 中的 state,即能更新(非使用 setState)也具有默认值。
const count = atom({
key: 'count', // 唯一 key 值
default: 0 // 默认值
})
💡使用 atom
function Counter() {
const [count, setCount] = useRecoilState(count) // hooks 接收一个 atom 或 selector(某些特定selector) 的 key 值
return (
<>
<button onClick={() => setCount(count => count + 1)}>add count</button>
<p>{count}</p>
</>
)
}
Selector
selector 是一个纯函数,入参是一个 atom
或其他 selector
,当上游 atom
或 selector
更新时(通过创建相应依赖),将重新执行 selector
函数。
如果只提供 get
方法,则 selector 是只读的,返回一个 RecoilValueReadOnly
对象。如果还提供了一个 set
方法,它的返回值将是一个可写的 RecoilState
对象。
Selector 被用于计算基于 state 的派生数据。避免了冗余 state,将最小粒度的状态存储在 atom 中,而其它所有内容根据最小粒度的状态进行有效计算。由于 selector 会追踪需要哪些组件使用了相关的状态,因此它们使这种方式更加有效。
💡定义一个 selector
const countLabel = selector({
key: 'countLabel', // 唯一 key 值
get: ({get}) => {
const count = get(count)
return `${count}次`
}
})
get
属性用于计算的函数,可以使用 get
入参来访问 atom
或其他 selector
的值,每当访问一个 atom
或 selector
时就会创建相应的依赖,并会对结果缓存下来。(概念上类似 vue 中的 computed 计算属性,只有依赖的值改变的时候才会改变。)
💡使用selector
function Counter() {
const [count, setCount] = useRecoilState(count)
const countLabel = useRecoilValue(count) // hooks 使用 atom 或 selector 作为参数,返回相应的值
return (
<>
<button onClick={() => setCount(count => count + 1)}>add count</button>
<p>{countLabel}</p>
</>
)
}
简述几个常用 hooks
useRecoilState()
:可读可写atom
或 某些selector
(同时具有get
和set
属性的selector
)useRecoilValue()
:只读atom
或selector
useSetRecoilState()
:只写atom
或 某些selector
(具有set
属性的selector
)useResetRecoilState()
:重置atom
为默认值
高级
异步查询
Recoil 允许 selector 的数据流图中无缝混合同步和异步函数,无需返回值本身,只需要在 get
回调中返回一个值的 Promise
。
使用这个 selector 的组件无需关心它是同步 atom 状态、或派生 selector 状态或异步查询来实现的,都是使用相同的 hooks。(即无需 await useRecoilValue()
)
💡例:
const userIdState = atom({
key: 'UserId',
default: 1
})
const userNameQuery = selector({
key: 'UserName',
get: async ({get}) => {
// 发起异步请求,且与 userIdState 绑定依赖,当 userIdState 改变是自动重新请求
const res = await httpRequest({
uId: get(userIdState)
})
return res.name
}
})
// UserInfo 组件
function UserInfo() {
const userName = useRecoilValue(userNameQuery)
return <div>{userName}</div>
}
❗❗❗ 提醒
当使用这种异步 selector
的组件的时候
1️⃣ 第一种方法 需要 React Suspense,包裹你的组件,用于捕获组件 pending 来渲染回调 UI。
🚫若不使用的话会报错 !!!
Error
Uncaught Error: UserInfo suspended while rendering, but no fallback UI was specified.
Add a <Suspense fallback=...\>
component higher in the tree to provide a loading indicator or placeholder to display.
💡正确用法:
function App() {
return (
<RecoilRoot>
<Suspense fallback={<div>加载中...</div>}>
<UserInfo />
</Suspense>
</RecoilRoot>
)
}
2️⃣ 第二种方法 使用 useRecoilValueLoadable()
hooks 来手动确定渲染期间的状态。
function UserInfo() {
const userNameLoadable = useRecoilValueLoadable(userNameQuery)
switch (userNameLoadable.state) {
case 'hasValue':
return <div>{userNameLoadable.contents}</div>;
case 'loading':
return <div>加载中……</div>;
case 'hasError':
throw userNameLoadable.contents;
}
}
export default UserInfo
报错处理
Recoil selector 也可以抛出错误,可以使用 React <ErrorBoundary>
来捕获。
💡例:
const userNameQuery = selector({
key: 'UserNameQuery',
get: async ({get}) => {
const res = await httpRequest({
uId: get(userIdState)
})
if(res.error) throw new Error(res.error)
return res.name
}
})
带参查询
使用 selectorFamily
helper 来实现带参数查询。
const userNameQuery = selectorFamily({
key: 'UserName',
get: userId => async () => {
const res = await httpRequest({
uId: userId
})
if(res.error) throw new Error(res.error)
return res.name
}
})
// UserInfo 组件
function UserInfo() {
// 使用上是相同的,可以把 selectorFamily 理解为 一个返回函数的 selector(闭包)
const userName = useRecoilValue(userNameQuery(userIdState))
return <div>{userName}</div>
}
并行请求
在上面的例子中,我们只是单一查询一个 uId
的用户名,如果我们需要批量查询 多个 uId
,往往的做法是创建一个 selector
批量查询并返回信息。
💡如:
const multUserNameQuery = selectorFamily({
key: 'multUserName',
get: uIds => ({get}) => {
return uIds.map(uid => get(userNameQuery(uid))) // userNameQuery 是带参查询的selector
}
})
实际上这种是没有问题的,但前提需要这些查询非常的快,因为它是一个接一个的查询,上一个完成之后接着才查询第二个,耗时是非常的巨大,比如一个 查询需要 200 ms,那查询3次至少需要600ms。
在 Recoil 中 提供了 waitForAll
helper 来并行执行它们,helper 接受数组和指定的依赖对象。👍👍👍(可以理解为 Promise.all
)
const multUserNameQuery = selectorFamily({
key: 'multUserName',
get: uIds => ({get}) => {
const userNameArr = get(waitForAll(uIds.map(uid => userNameQuery(uid))))
return userNameArr
}
})
同时,Recoil 提供了 waitForNone
helper 来支持增量更新(即谁先查询完毕先返回),用法差不多。
const multUserNameQuery = selectorFamily({
key: 'multUserName',
get: uIds => ({get}) => {
const userNameLoadable = get(waitForNone(uIds.map(uid => userNameQuery(uid))))
// 类似 useRecoilValueLoadable,筛选出请求完成(state 为 hasValue)的信息
return userNameLoadable
.filter(({state}) => state === 'hasValue')
.map(({contents}) => contents);
}
})
API
这里只简述除了上面已经提到的API外,另外一些常用 API 的作用及注意事项,详细参数请查阅官方文档,没有必要复制粘贴一遍~
<RecoilRoot />
所有使用 Recoil hook 组件 的根组件。
可以存在多个 <RecoilRoot>
,atom 在每个根中拥有不同的值,内部根会覆盖外部根。
解析
假如有如下结构
<RecoilRoot>
<UserInfo1 />
<RecoilRoot>
<UserInfo2>
<RecoilRoot/>
<RecoilRoot/>
外部 <RecoilRoot>
包含的组件(这里指 <UserInfo1>
)使用了一个 key
为 UserId
的 atom,默认值为1。
内部 <RecoilRoot>
包含的组件(这里指 <UserInfo2>
)也使用了一个 key
为 UserId
的 atom,默认值为2。
注意这两个 atom 是不一样的,只是 key
一致,但结果会是内部的 atom 覆盖 外部的 atom,即都会默认显示为2。
<RecoilRoot />
有一个 override
props,默认为 true
,它的作用是隔离作用域(创建多个作用域),只有在被嵌套的 <RecoilRoot />
才生效。当 override
为 false
时,<RecoilRoot>
除了渲染它的子代外,将不会执行任何额外功能,这个根的子代将访问最近的祖先节点 <RecoilRoot>
作用域中 Recoil 的值。
解析
同上一个例子,将它们修改为使用同一个 atom,默认值为1。包裹 <UserInfo2 />
的 <RecoilRoot />
的组件override
为 true
,这时在 <UserInfo2 />
修改这个 atom 的值为2,这时 <UserInfo1 />
依旧会显示为1,因为它们的作用域是不一样的,内部的 <UserInfo2 />
是独立一个作用域。
若把内部的 <RecoilRoot />
override
修改为 false
,这时在 <UserInfo2 />
修改这个 atom 的值为2,这时 <UserInfo1 />
会改变为2。因为它们使用的是同一个 Recoil 的值。
Loadable
Loadable
对象代表 Recoil atom 或 selector 的当前状态。
state
:atom 或 selector 的当前状态。可能的值有'hasValue'
、'hasError'
或者'loading'
。contents
:此Loadable
表示的值。如果 state 的值是hasValue
,其值为实际值;如果 state 的值是hasError
,其值为被抛出Error
对象;如果 state 的值是loading
,那么你可以使用toPromise()
得到一个Promise
。
useRecoilStateLoadable()
用于读取异步 selector 的值,返回一个 Loadable 对象的值以及一个 setter 回调。
useRecoilValueLoadable()
同上,不过只返回一个 Loadable 对象的值。
noWait()
selector helper 方法,返回值为代表所提供的 atom 或 selector 当前状态的 Loadable 对象。
// 来自官方实例
const myQuery = selector({
key: 'MyQuery',
get: ({get}) => {
const loadable = get(noWait(dbQuerySelector));
return {
hasValue: {data: loadable.contents},
hasError: {error: loadable.contents},
loading: {data: 'placeholder while loading'},
}[loadable.state];
}
})
waitForAny()
返回一组表示请求依赖项当前状态的 Loadables 的并发 helper 方法。它将一直等待,直到至少有一个依赖项可用。类似 Promise.any()
。