Skip to content

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 本身的状态。

安装

bash
npm install recoil

注意

Recoil 构建是没有转译成 ES5 的,也就是说如果你需要兼容 ES5,需要安装 Babel 进行转译,但并不建议,因为 Recoil 依赖于 MapSet 类型以及其他 ES6 的特性。使用 polyfill 来模拟这些特性可能会导致性能大大降低。

核心概念

Atom 是组件可以订阅的 state 单位。selector 可以同步或异步改变此 state。

key 值必须保证在整个应用中相对 atom 和 selector 里是唯一的

Atom

Atom 是状态的单位,可订阅也可更新;当 Atom 更新时,每个被订阅的组件都将重新渲染

💡定义一个 atom

js
// 你可以把它理解为 React 中的 state,即能更新(非使用 setState)也具有默认值。
const count = atom({
  key: 'count', // 唯一 key 值
  default: 0 // 默认值
})

💡使用 atom

jsx
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当上游 atomselector 更新时(通过创建相应依赖),将重新执行 selector 函数

如果只提供 get 方法,则 selector 是只读的,返回一个 RecoilValueReadOnly 对象。如果还提供了一个 set 方法,它的返回值将是一个可写的 RecoilState 对象

Selector 被用于计算基于 state 的派生数据。避免了冗余 state,将最小粒度的状态存储在 atom 中,而其它所有内容根据最小粒度的状态进行有效计算。由于 selector 会追踪需要哪些组件使用了相关的状态,因此它们使这种方式更加有效。

💡定义一个 selector

js
const countLabel = selector({
  key: 'countLabel', // 唯一 key 值
  get: ({get}) => {
    const count = get(count)
    return `${count}次`
  }
})

get 属性用于计算的函数,可以使用 get 入参来访问 atom 或其他 selector 的值,每当访问一个 atomselector 时就会创建相应的依赖,并会对结果缓存下来。(概念上类似 vue 中的 computed 计算属性,只有依赖的值改变的时候才会改变。

💡使用selector

jsx
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 (同时具有 getset 属性的 selector)
  • useRecoilValue()只读 atomselector
  • useSetRecoilState()只写 atom 或 某些 selector (具有 set 属性的 selector)
  • useResetRecoilState()重置 atom 为默认值

高级

异步查询

Recoil 允许 selector 的数据流图中无缝混合同步和异步函数,无需返回值本身,只需要在 get 回调中返回一个值的 Promise

使用这个 selector 的组件无需关心它是同步 atom 状态、或派生 selector 状态或异步查询来实现的,都是使用相同的 hooks。(即无需 await useRecoilValue())

💡

jsx
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.

💡正确用法:

jsx
function App() {
  return (
    <RecoilRoot>
      <Suspense fallback={<div>加载中...</div>}>
        <UserInfo />
      </Suspense>
    </RecoilRoot>
  )
}

2️⃣ 第二种方法 使用 useRecoilValueLoadable() hooks 来手动确定渲染期间的状态

jsx
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> 来捕获。

💡

jsx
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 来实现带参数查询。

jsx
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 批量查询并返回信息。

💡

jsx
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

jsx
const multUserNameQuery = selectorFamily({
  key: 'multUserName',
  get: uIds => ({get}) => {
    const userNameArr = get(waitForAll(uIds.map(uid => userNameQuery(uid))))
    return userNameArr
  }
})

同时,Recoil 提供了 waitForNone helper 来支持增量更新(即谁先查询完毕先返回),用法差不多。

jsx
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 在每个根中拥有不同的值,内部根会覆盖外部根

解析

假如有如下结构

jsx
<RecoilRoot>
  <UserInfo1 />
  <RecoilRoot>
    <UserInfo2>
  <RecoilRoot/>
<RecoilRoot/>

外部 <RecoilRoot> 包含的组件(这里指 <UserInfo1>)使用了一个 keyUserId 的 atom,默认值为1。

内部 <RecoilRoot> 包含的组件(这里指 <UserInfo2>)也使用了一个 keyUserId 的 atom,默认值为2。

注意这两个 atom 是不一样的,只是 key 一致,但结果会是内部的 atom 覆盖 外部的 atom,即都会默认显示为2。

<RecoilRoot /> 有一个 override props,默认为 true ,它的作用是隔离作用域(创建多个作用域),只有在被嵌套的 <RecoilRoot /> 才生效。overridefalse 时,<RecoilRoot> 除了渲染它的子代外,将不会执行任何额外功能,这个根的子代将访问最近的祖先节点 <RecoilRoot> 作用域中 Recoil 的值

解析

同上一个例子,将它们修改为使用同一个 atom,默认值为1。包裹 <UserInfo2 /><RecoilRoot /> 的组件overridetrue这时在 <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 对象。

jsx
// 来自官方实例
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()

Released under the MIT License.