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()
。