背景
偶然的一次项目开发中发现 Recoil
有内存泄漏的情况,一次几 M
,虽然影响不大,但是还是具有一定的隐患,特别是随着项目的体量增加,后期带来的替换成本也会随之增高;
当然这是原因之一,下面会跟大家聊聊 Recoil
的现状。
现状
Recoil
仍旧处于实验性质;Meta
官方宣布将Recoil
开放给外部维护者;(来自Discord
,尚未证实)- 核心开发者
drarmstr
已于2023
年1
月被解雇;(已证实LinkedIn) FaceBook
官网仍在使用Recoil
,并且将其作为生产应用来使用;
其实抛开上述现状来讲,内部项目在生产使用 Recoil
并无什么大的问题,偶现场景的内存泄漏,基本属于极端场景,但是基于长远发展的方向而言,库的选型上可能略有不妥;
但是不的不吐槽 Recoil
的包体积确实很大,这也是我下决心替换库的最大动机。
Recoil VS Jotai
参考:
Jotai
相比较 Recoil
主要有几个区别:
atom
作为key
比起 唯一string
key,样板代码变少了、内存泄漏风险减少了;- 包体积更小;
API
更简单容易理解,可拔插、集成能力更优异;- 维护者更积极,
BUG
&Feature
响应更快; - 性质不同,一个是稳定性质一个是实验性质;
Jotai
新的 原子型
状态管理库的后备方案;
核心维护者是 Dai-shi
,目前有 14.9k+
星星,受众公司也蛮多;
聊到这里,咱们可以步入正题了,我准备从 原子型 状态的思想、recoil
到 jotai
的 API
迁移指南入题讲解。
原子哲学
以 jotai
为例,底层还是依赖了 React Provider
(这里解释不包含 provider less mode
)作为原子范围的隔离;
下面是 GC
原理,区别于 Recoil
(String key
作为键更稳定)
图片来自于:excalidraw.com/
动机:
- 解决
provider
嵌套地狱 &渲染地狱
; - 跨组件状态共享更加便捷;
- 精准渲染订阅组件,避免
reRender
;
Provider Less Mode
无 Provider
模式,有点类似于 React 18
中的 useSyncExternalStore
本质上还是借助 发布订阅模式 |代理模式 ,对订阅组件执行 forceUpdate
操作。
jotai
自己手撸了一个 源码,借助 WeakMap
实现的。
ts
// 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
>()
// ....
}
那么最终的原子哲学是如何工作的呢?
浅看一下 useAtomValue
的源码:github.com/pmndrs/jota...
ts
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
recoil
ts
import { atom } from 'recoil';
const todosAtom = atom({
key: 'TODO_STORE',
default: {
data: []
}
})
jotai
ts
import { atom } from 'jotai';
const todosAtom = atom({
data: []
})
useRecoilState -> useAtom
recoil
ts
import { useRecoilState } from 'recoil';
const [data, setData] = useRecoilState(todosAtom);
jotai
ts
import { useAtom } from 'jotai';
const [data, setData] = useRecoilState(todosAtom);
useSetRecoilState -> useSetAtom
recoil
ts
import { useSetRecoilState } from 'recoil';
const setData = useRecoilState(todosAtom);
jotai
ts
import { useSetAtom } from 'jotai';
const setData = useSetAtom(todosAtom);
useRecoilValue -> useAtomValue
recoil
ts
import { useRecoilValue } from 'recoil';
const data = useRecoilValue(todosAtom);
jotai
ts
import { useAtomValue } from 'jotai';
const data = useAtomValue(todosAtom);
useResetRecoilState -> useResetAtom
recoil
ts
import { useResetRecoilState } from 'recoil';
const reset = useResetRecoilState(todosAtom);
jotai
如果你在 jotai
中定义的原子需要具备 resettable
能力,这里有两个注意事项:
- 原子定义方式需要变化
API
导入变化
原子定义
ts
import { atomWithReset, useResetAtom, RESET } from 'jotai/utils'
const todosAtom = atomWithReset({ data: [] })
API导入
ts
import { useResetAtom } from 'jotai/utils'
const reset = useResetAtom(todosAtom)
useRecoilCallback -> useAtomCallback
recoil
ts
import { useRecoilCallback } from 'recoil';
const lazyLoad = useRecoilCallback(({ set, reset, snapshot }) => async () => {
const data = snapshot.getPromise(todosAtom);
// 打印 atom data
}, [])
jotai
ts
import { useCallback } from 'react';
import { useAtomCallback } from 'jotai/utils'
const lazyload = useAtomCallback(useCallback(async (get, set) => {
const data = await get(todosAtom);
}, []));
到这里,从 recoil
到 jotai
迁移的上篇就结束了,从 API
的迁移上看,核心的 API
迁移成本还是很小的,代码略微改造即可。
下篇预告:下篇则会着重介绍 异步原子 的迁移,各种实战 hack
写法,我们下期再见!