Jotai 是一个原子化的React状态管理工具,和Zustand、valtio作者是同一个人(一个人能做出多个爆款状态库简直是神人)。
Jotai具有以下特性:
- 原子状态(Atoms):Jotai的核心概念是“原子”,每个原子代表一个独立的状态单元。原子可以被多个组件订阅和更新,从而实现了状态的共享。
- 简洁易用:相比其他复杂的状态管理方案,如Redux或MobX,Jotai的设计更加轻量级,API也更为简单直观。你不需要编写大量的样板代码就能快速上手。
- 性能优化:通过细粒度的依赖跟踪机制,只有当相关联的原子发生变化时,才会触发组件重新渲染,这有助于提高应用的整体性能。
- 与React高度集成:作为专门为React设计的状态管理工具,Jotai能够很好地利用React的新特性,例如Hooks、Context等,使得开发者可以在不改变现有代码结构的情况下引入Jotai。
原子
原子是Jotai的最小状态单位,可以是一个数字、字符串、布尔值、数组、对象等等任何的JS数据类型
const ageAtom = atom(23)
在定义原子的时候尽量使用最小范围,有复杂对象数据需求时我们可以通过组合的方式来创建新的原子
const ageAtom = atom(23)
const nameAtom = atom('tom')
const personAtom = atom(get => {
return {
name: get(nameAtom),
age: get(ageAtom)
}
})
这就非常容易的实现了computed功能,我们可以根据实际需求来尽情地组合原子,当子原子更新时,上层的原子也会同步更新。
这里引入读写控制的概念,像上面我们组合出来的原子,通过get不同原子获得返回值,但是没有对应的set方法,没法直接更新原来的子原子,这种称为只读原子(Read-only atom),当组合原子的时候没有传入get方法,只传入了set方法,例如
const writeOnlyAtom = atom(
null,
(get, set, update) => {
set(priceAtom, get(priceAtom) - update.discount)
set(priceAtom, (price) => price - update.discount)
},
)
这种称为只写原子(Write-only atom),同时传入读写方法的就称为读写原子(Read-Write atom),最基础的原子就可以看作是一个读写原子。
读写原子上还可以绑定一些附加功能,如果需要在原子绑定的时候执行一些操作,可以添加onMount
句柄,返回值是一个函数,会在原子onUnMount的时候执行
ageAtom.onMount = (setAtom) => {
console.log('ageAtom onMount')
setAtom(age => {
console.log('newVal', age + 1)
return age + 1
})
return () => {}
}
原子组合还支持异步的方式,这意味着我们可以通过请求进行原子的初始化,并且支持通过signal终止请求
const readOnlyDerivedAtom = atom(async (get, { signal }) => {
const id = get(idAtom)
const response = await fetch(
`http://example.com/api/v1/test/${id}`,
{ signal },
)
return response.json()
})
const writableDerivedAtom = atom(
async (get, { signal }) => {
// ...
},
(get, set, arg) => {
// ...
}
)
可以利用官方工具loadable
来控制异步原子的渲染
const loadableAtom = loadable(readOnlyDerivedAtom)
const Component = () => {
const [value] = useAtom(loadableAtom)
if (value.state === 'hasError') {
return <Text>{value.error}</Text>
}
if (value.state === 'loading') {
return <Text>Loading...</Text>
}
return <Text>Value: {value.data}</Text>
}
另外可以配合第三方库jotai-cache来对异步原子进行缓存,避免每一次绑定原子都重新请求
const cachedAtom = atomWithCache(async (get) => {
const id = get(idAtom)
const response = await fetch(`http://example.com/api/v1/test/${id}`)
return response.json()
})
Store
Store API用于创建和管理原子集合,可以通过createStore
创建一个store,如果不创建store则使用默认store即defaultStore,defaultStore可以通过getDefaultStore
获取。
创建的store可以传递给不同的<Provider>
组件来在不同的上下文中使用不同的数据。
export const store1 = createStore()
export const store2 = createStore()
export const numAtom = atom(0)
function Section1() {
const [num, setNum] = useAtom(numAtom)
return <>
{num}
<Button onClick={() => setNum(num + 1)}>add</Button>
</>}
function Section2() {
const [num, setNum] = useAtom(numAtom)
return <>
{num}
<Button onClick={() => setNum(num + 1)}>add</Button>
</>}
export function JotaiStore() {
return <>
<Provider store={store1}>
<Section1 />
</Provider>
<Provider store={store2}>
<Section2 />
</Provider>
</>}
此时在两个子组件中的状态就互相隔离了。
在React中使用
在React中使用主要依赖三个hook,使用方式如下(使用第一小节定义的几个原子进行演示)
export function JotaiInReact() {
const person = useAtomValue(personAtom)
const setAge = useSetAtom(ageAtom)
return <div className="text-center mt-6">
<div onClick={() => setAge(age => age + 1)}>
{person.age}-{person.name}
</div>
</div>}
- useAtom:使用方式类似于useState,返回值是一个数组,第一个是原子的值,第二个是更新原子的方法;
- useAtomValue:返回原子的值
- useSetAtom:返回更新原子值的方法
我们可以根据需求来使用这三个hook
在React组件之外使用
Jotai v2已经支持在React组件以外使用了,通过Store API进行操作,
const defaultStore = getDefaultStore()
export function outReactFunc() {
const data = defaultStore.get(willDoList)
console.log(data)
defaultStore.set(willDoList, [...data, createTodoItem('test')])
}
然后我们出发这个函数的时候可以看见控制台输出了最新的state,并且更新后的数据也同步渲染到了页面上
进阶使用
storage
我们的状态都是在内存中的一次性的数据,如果需要在浏览器对数据进行缓存,可以使用atomWithStorage
来进行持久化存储
export const numAtom = atomWithStorage('num', 0)
但是这里要注意,存在多个store的时候,storageKey会冲突,只会保留最后一次更新的值
开发工具
和其他几款主流的状态库一样,Jotai也提供了开发者工具,可以直观地对当前页面状态进行可视化查看。
$ pnpm install jotai-devtools
安装完成之后添加babel配置,例如我使用的是vite,就在vite.config.ts中添加以下内容
export default defineConfig({
plugins: [react(
{
babel: {
presets: ['jotai/babel/preset'],
},
}
)]
})
如果你使用的是其他的工具,可以参考官网文档的使用姿势,然后在入口文件中添加组件即可
export default function App() {
return (
<>
<DevTools />
// ...
</>
}
然后就可以开始在页面上进行调试了
可以通过原子的debugLabel属性来自定义调试工具显示的原子名称,例如
ageAtom.debugLabel = 'ageAtomCustomLabel'