从 Recoil 到 Jotai (上)

·

3 min read

背景

偶然的一次项目开发中发现 Recoil 有内存泄漏的情况,一次几 M ,虽然影响不大,但是还是具有一定的隐患,特别是随着项目的体量增加,后期带来的替换成本也会随之增高;

当然这是原因之一,下面会跟大家聊聊 Recoil 的现状。

现状

  • Recoil 仍旧处于实验性质

  • Meta 官方宣布将 Recoil 开放给外部维护者;(来自 Discord,尚未证实)

  • 核心开发者 drarmstr 已于 20231 月被解雇;(已证实LinkedIn[1]

  • FaceBook 官网仍在使用 Recoil,并且将其作为生产应用来使用;


其实抛开上述现状来讲,内部项目在生产使用 Recoil 并无什么大的问题,偶现场景的内存泄漏,基本属于极端场景,但是基于长远发展的方向而言,库的选型上可能略有不妥;

但是不的不吐槽 Recoil 的包体积确实很大,这也是我下决心替换库的最大动机。


Recoil VS Jotai

参考:

  • Jotai 讨论区[2]

  • VS Blog[3]

Jotai 相比较 Recoil 主要有几个区别:

  • atom 作为 key 比起 唯一string key,样板代码变少了、内存泄漏风险减少了;

  • 包体积更小;

  • API 更简单容易理解,可拔插、集成能力更优异;

  • 维护者更积极,BUG & Feature 响应更快;

  • 性质不同,一个是稳定性质一个是实验性质;

Jotai

新的 原子型 状态管理库的后备方案;

核心维护者是 Dai-shi,目前有 14.9k+ 星星,受众公司也蛮多;


聊到这里,咱们可以步入正题了,我准备从 原子型 状态的思想、recoiljotaiAPI 迁移指南入题讲解。

原子哲学

jotai 为例,底层还是依赖了 React Provider (这里解释不包含 provider less mode)作为原子范围的隔离;

下面是 GC 原理,区别于 RecoilString key 作为键更稳定)

图片来自于:https://excalidraw.com/

动机:

  • 解决 provider 嵌套地狱 & 渲染地狱

  • 跨组件状态共享更加便捷;

  • 精准渲染订阅组件,避免 reRender

jotai 基础课程[4]

Provider Less Mode

Provider 模式,有点类似于 React 18 中的 useSyncExternalStore[5]

本质上还是借助 发布订阅模式代理模式,对订阅组件执行 forceUpdate 操作。

jotai 自己手撸了一个 源码[6],借助 WeakMap 实现的。


// source code from https://github.com/pmndrs/jotai/blob/main/src/vanilla/store.ts

export const createStore = () => {
  const atomStateMap = new WeakMap<AnyAtom, AtomState>()
  const mountedMap = new WeakMap<AnyAtom, Mounted>()
  const pendingMap = new Map<
    AnyAtom,
    AtomState /* prevAtomState */ | undefined
  >()
 // ....
}

那么最终的原子哲学是如何工作的呢?

图片来自于:https://blog.bitsrc.io/redux-free-state-management-with-jotai-2c8f34a6a4a

浅看一下 useAtomValue 的源码:github.com/pmndrs/jotai/blob/2526039ea4da08..[7]

useEffect(() => {
    const unsub = store.sub(atom, () => {
      if (typeof delay === 'number') {
        // delay rerendering to wait a promise possibly to resolve
        setTimeout(rerender, delay)
        return
      }
      rerender()
    })
    rerender()
    return unsub
  }, [store, atom, delay])

本质就是发布订阅、观察者模式那一套。

Recoil -> Jotai

atom

  1. recoil
import { atom } from 'recoil';

const todosAtom = atom({
    key: 'TODO_STORE',
    default: {
        data: []
    }
})
  1. jotai
import { atom } from 'jotai';

const todosAtom = atom({
    data: []
})

useRecoilState -> useAtom

  1. recoil
import { useRecoilState } from 'recoil';

const [data, setData] = useRecoilState(todosAtom);
  1. jotai
import { useAtom } from 'jotai';

const [data, setData] = useRecoilState(todosAtom);

useSetRecoilState -> useSetAtom

  1. recoil
import { useSetRecoilState } from 'recoil';

const setData = useRecoilState(todosAtom);
  1. jotai
import { useSetAtom } from 'jotai';

const setData = useSetAtom(todosAtom);

useRecoilValue -> useAtomValue

  1. recoil
import { useRecoilValue } from 'recoil';

const data = useRecoilValue(todosAtom);
  1. jotai
import { useAtomValue } from 'jotai';

const data = useAtomValue(todosAtom);

useResetRecoilState -> useResetAtom

  1. recoil
import { useResetRecoilState } from 'recoil';

const reset = useResetRecoilState(todosAtom);
  1. jotai

如果你在 jotai 中定义的原子需要具备 resettable 能力,这里有两个注意事项:

  • 原子定义方式需要变化

  • API 导入变化

原子定义

import { atomWithReset, useResetAtom, RESET } from 'jotai/utils'

const todosAtom = atomWithReset({ data: [] })

API导入

import { useResetAtom } from 'jotai/utils'

const reset = useResetAtom(todosAtom)

useRecoilCallback -> useAtomCallback

  1. recoil
import { useRecoilCallback } from 'recoil';


const lazyLoad = useRecoilCallback(({ set, reset, snapshot }) => async () => {
    const data = snapshot.getPromise(todosAtom);
    // 打印 atom data
}, [])
  1. jotai
import { useCallback } from 'react';
import { useAtomCallback } from 'jotai/utils'

const lazyload = useAtomCallback(useCallback(async (get, set) => {
    const data = await get(todosAtom);
}, []));

到这里,从 recoiljotai 迁移的上篇就结束了,从 API 的迁移上看,核心的 API 迁移成本还是很小的,代码略微改造即可。

下篇预告:下篇则会着重介绍 异步原子 的迁移,各种实战 hack 写法,我们下期再见!

参考资料

[1]LinkedIn: linkedin.com/posts/douglas-armstrong-384a34..

[2]Jotai 讨论区: github.com/pmndrs/jotai/discussions/849

[3]VS Blog: blog.logrocket.com/jotai-vs-recoil-what-are..

[4]jotai 基础课程: egghead.io/lessons/react-isolate-state-in-a..

[5]useSyncExternalStore: react.dev/reference/react/useSyncExternalSt..

[6]源码: github.com/pmndrs/jotai/blob/main/src/vanil..

[7]https://github.com/pmndrs/jotai/blob/2526039ea4da082749adc8a449c33422c53d9819/src/react/useAtomValue.ts: github.com/pmndrs/jotai/blob/2526039ea4da08..