Jotai学习心得

了解和使用Jotai之前,我对跨组件状态管理的主要手段是useState + props、Context,以及在部分场景下用EventBus做组件间通信。但在项目复杂度逐渐上升后,这些方案要么带来明显的prop drilling,要么导致组件订阅范围过大、渲染不可控。

Jotai 采用原子方法进行全局 React 状态管理。

Jotai 给我的一个最直观感受是:它不是在引入一个“更大的状态中心”,而是在把状态拆得足够小。

Atoms are the building blocks of universe and clump together into molecules–

原子是宇宙的构建模块,聚集成分子——

Jotai 原子是小而孤立的状态片段。理想情况下,一个原子包含非常小的数据(虽然这只是个惯例。仍然可以把所有状态放到一个原子里,虽然那样性能会非常慢)

import { atom } from 'jotai';

// 原始原子可以是任何类型:布尔、数字、字符串、对象、数组、集合、映射等等。
const themeAtom = atom('light');
const countryAtom = atom('Japan')
const citiesAtom = atom(['Tokyo', 'Kyoto', 'Osaka'])

export const animeAtom = atom([
{
title: 'Ghost in the Shell',
year: 1995,
watched: true
},
{
title: 'Serial Experiments Lain',
year: 1998,
watched: false
}
])

// 衍生原子可以读取其他原子的信号,然后返回自身的数值
const progressAtom = atom((get) => {
const anime = get(animeAtom)
return anime.filter((item) => item.watched).length / anime.length
})

atom 是“最小可共享状态单元”,而不是全局变量

一开始我担心atom会不会变成新的“全局变量污染”,但实际使用下来发现并不是这样。

atom本身并不会主动影响任何组件,只有显式使用 useAtom / useAtomValue / useSetAtom 的组件才会订阅它

这和传统EventBus或Context的“广播式更新”有本质区别——状态变化只会影响真正关心它的组件,渲染范围更可控。

很多 EventBus 场景,本质上是“状态同步”而不是“事件”

在回顾之前使用EventBus的场景时,我发现大量所谓的“跨组件通信”,其实是在同步一些可持续存在的状态,比如:

  • 是否打开某个弹窗
  • 当前选中的对象
  • 列表是否需要刷新

这些场景用EventBus往往会引入额外的时序问题,而用Jotai表达为atom后,状态来源和消费关系变得更清楚,也更容易调试

  • 状态型通信 → Jotai
  • 一次性行为 / 命令式操作 → 普通事件或函数调用

什么时候用Jotai

Jotai 不是“我要一个全局变量”,而是“我要一个可以被多个组件共享的最小状态单元”

面对以下场景:

“这个状态是不是应该被多个地方同时读 / 改?”

  • 是 → Jotai
  • 否 → useState

Jotai 更适合“局部全局”,而不是无脑全局

通过学习和了解,总结了一个结论是:不是所有state都值得atom化

组件内部的临时状态(如hover、输入框值)继续使用useState反而更简单。

Jotai 真正的优势在于:

  • 多个组件确实需要共享
  • 或者希望避免状态层层传递
  • 或者需要精确控制谁会因状态变化而更新

在这个前提下,atom更像是“业务域内的共享状态”,而不是应用级的万能全局状态。

怎么用Jotai

当原子在同一组件内同时读写时,为了简化使用结合的useAtom 钩子。

import { atom, useAtom } from 'jotai';

const themeAtom = atom('light');

export const ThemeSwitcher = () => {
const [theme, setTheme] = useAtom(themeAtom);

return (
<main>
<p>Theme is {theme}</p>

<button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>Toggle Theme</button>
</main>
);
};

看起来很像useState,但唯一的区别是我们创建的原子要传递给 useState。useAtom返回一个大小为 2 的数组,其中第一个元素是一个值,第二个元素是一个函数,用于设置原子的值。这使得所有依赖该原子的组件都会更新并重新渲染。

当从不同的组件读取和写入时,使用分别的useAtomValueuseSetAtom 钩子来优化重新渲染。

import { useAtomValue, useSetAtom } from 'jotai'

import { animeAtom } from './atoms'

const AnimeList = () => {
const anime = useAtomValue(animeAtom)

return (
<ul>
{anime.map((item) => (
<li key={item.title}>{item.title}</li>
))}
</ul>
)
}

const AddAnime = () => {
const setAnime = useSetAtom(animeAtom)

return (
<button onClick={() => {
setAnime((anime) => [
...anime,
{
title: 'Cowboy Bebop',
year: 1998,
watched: false
}
])
}}>
Add Cowboy Bebop
</button>
)
}

const ProgressTracker = () => {
const progress = useAtomValue(progressAtom)

return (
<div>{Math.trunc(progress * 100)}% watched</div>
)
}

const AnimeApp = () => {
return (
<>
<AnimeList />
<AddAnime />
<ProgressTracker />
</>
)
}

好用的atomWithImmer

atomWithImmer 不是让状态“更全局”,而是让“共享的复杂状态”写起来更安全、更清晰。

React状态最大的痛点:不可变性。

不可变性很棒,也让人很容易理解React状态,但当状态是对象时,事情会变得非常困难。然后就得做一整套流程,展开对象并与想更新的属性合并。

而使用Immer允许你直接变异状态

import { atomWithImmer } from 'jotai/immer';

const userAtom = atomWithImmer({
id: 23,
name: 'Luke Skywalker',
dob: new Date('25 December, 19 BBY'),
});

function UpdateUser() {
const [user, setUser] = useAtom(userAtom);

// Update the dob
const updateDob = () =>
setUser((user) => {
user.dob = new Date('25 November, 200ABY');
return user;
});

return <button onClick={updateDob}>Update DOB</button>;
}

✅ 适合场景

  • 深层嵌套对象
  • 列表、树结构
  • 表单状态
  • 需要频繁局部修改的共享状态

❌ 不太适合

  • 简单标量值(number / boolean)
  • 非共享的局部状态(用useImmer或useState更简单)
  • 极度追求极致性能的热点路径(Immer有微小开销)