Jotai 是一个原子化的React状态管理工具,和Zustand、valtio作者是同一个人(一个人能做出多个爆款状态库简直是神人)。
Jotai具有以下特性:

  1. 原子状态(Atoms):Jotai的核心概念是“原子”,每个原子代表一个独立的状态单元。原子可以被多个组件订阅和更新,从而实现了状态的共享。
  2. 简洁易用:相比其他复杂的状态管理方案,如Redux或MobX,Jotai的设计更加轻量级,API也更为简单直观。你不需要编写大量的样板代码就能快速上手。
  3. 性能优化:通过细粒度的依赖跟踪机制,只有当相关联的原子发生变化时,才会触发组件重新渲染,这有助于提高应用的整体性能。
  4. 与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>
    </>}

此时在两个子组件中的状态就互相隔离了。
image.png

在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>}
  1. useAtom:使用方式类似于useState,返回值是一个数组,第一个是原子的值,第二个是更新原子的方法;
  2. useAtomValue:返回原子的值
  3. 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,并且更新后的数据也同步渲染到了页面上
image.png

进阶使用

storage

我们的状态都是在内存中的一次性的数据,如果需要在浏览器对数据进行缓存,可以使用atomWithStorage来进行持久化存储

export const numAtom = atomWithStorage('num', 0)

但是这里要注意,存在多个store的时候,storageKey会冲突,只会保留最后一次更新的值
image.png

开发工具

和其他几款主流的状态库一样,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 />
      // ...
    </>
}

然后就可以开始在页面上进行调试了
image.png

可以通过原子的debugLabel属性来自定义调试工具显示的原子名称,例如ageAtom.debugLabel = 'ageAtomCustomLabel'


前端小白