Jotai 是一个受 Recoil 启发的 React 状态管理库,采用"原子化"的状态管理理念,让状态管理变得简单、灵活且可预测。本教程将从零开始,带你全面掌握 Jotai 的使用方法。
什么是 Jotai?
Jotai 发音为 "joe-tie",源自日语"状態"(じょうたい - jōtai),意为"状态"。它的核心思想是将状态分割成一个个独立的"原子"(atom),组件可以精确订阅所需的状态,避免不必要的重渲染。
与 Redux 等传统状态管理库相比,Jotai 具有以下优势:
- 无需大量模板代码
- 原子化设计,精确更新
- 简洁的 API,易于学习
- 优秀的 TypeScript 支持
- 与 React 生态无缝集成
安装 Jotai
首先,我们需要安装 Jotai 包:
bash
# 使用 npm
npm install jotai
# 使用 yarn
yarn add jotai
# 使用 pnpm
pnpm add jotai
核心概念:原子(Atom)
在 Jotai 中,"原子"(atom)是状态管理的基本单位。一个原子代表一个可共享的状态片段,可以是任何类型的值(数字、字符串、对象等)。
创建第一个原子
使用 atom
函数可以创建一个原子:
js
import { atom } from 'jotai';
// 创建一个初始值为 0 的计数器原子
const countAtom = atom(0);
这是一个最基本的"可写原子"(writable atom),既可以读取其值,也可以修改它。
在组件中使用原子
要在组件中使用原子,我们需要用到 useAtom
钩子,它返回一个数组 [value, setValue]
,类似于 React 的 useState
:
js
import { useAtom } from 'jotai';
function Counter() {
// 解构出值和更新函数
const [count, setCount] = useAtom(countAtom);
return (
<div>
<p>计数: {count}</p>
<button onClick={() => setCount(c => c + 1)}>加 1</button>
<button onClick={() => setCount(c => c - 1)}>减 1</button>
<button onClick={() => setCount(0)}>重置</button>
</div>
);
}
setCount
可以直接接收新值,也可以接收一个函数,该函数接收当前值并返回新值。
只读与只写操作
在很多场景下,组件可能只需要读取状态或只需要修改状态。Jotai 提供了专门的钩子来处理这些情况。
useAtomValue:只读操作
useAtomValue
钩子只返回原子的值,不提供修改方法,适合只读场景:
js
import { useAtomValue } from 'jotai';
function CountDisplay() {
// 只获取值,不获取更新函数
const count = useAtomValue(countAtom);
return <div>当前计数: {count}</div>;
}
useSetAtom:只写操作
useSetAtom
钩子只返回修改原子值的函数,不获取当前值,适合只需要修改状态的场景:
js
import { useSetAtom } from 'jotai';
function CountControls() {
// 只获取更新函数
const setCount = useSetAtom(countAtom);
return (
<div>
<button onClick={() => setCount(c => c + 1)}>加 1</button>
<button onClick={() => setCount(c => c - 1)}>减 1</button>
</div>
);
}
这种分离不仅让代码意图更清晰,还能避免不必要的重渲染。
派生原子(Derived Atoms)
派生原子是基于其他原子计算得出的原子,它的值会随着依赖原子的变化而自动更新。
创建派生原子
通过向 atom
函数传递一个函数,可以创建派生原子。这个函数接收一个 get
方法,用于获取其他原子的值:
js
// 派生原子:计算计数的两倍
const doubleCountAtom = atom((get) => {
const count = get(countAtom);
return count * 2;
});
// 使用派生原子
function DoubleCountDisplay() {
const doubleCount = useAtomValue(doubleCountAtom);
return <div>计数的两倍: {doubleCount}</div>;
}
当 countAtom
的值发生变化时,doubleCountAtom
的值会自动重新计算,所有使用 doubleCountAtom
的组件都会更新。
组合多个原子
派生原子可以依赖多个原子,形成复杂的状态计算:
js
// 创建两个原子
const firstNameAtom = atom('');
const lastNameAtom = atom('');
// 派生原子:组合姓名
const fullNameAtom = atom((get) => {
const firstName = get(firstNameAtom);
const lastName = get(lastNameAtom);
return `${firstName} ${lastName}`;
});
// 使用这些原子
function NameForm() {
const [firstName, setFirstName] = useAtom(firstNameAtom);
const [lastName, setLastName] = useAtom(lastNameAtom);
const fullName = useAtomValue(fullNameAtom);
return (
<div>
<input
placeholder="名"
value={firstName}
onChange={(e) => setFirstName(e.target.value)}
/>
<input
placeholder="姓"
value={lastName}
onChange={(e) => setLastName(e.target.value)}
/>
<p>全名: {fullName}</p>
</div>
);
}
异步原子(Async Atoms)
Jotai 对异步操作有很好的支持,可以轻松创建依赖异步数据的原子。
创建异步原子
异步原子的创建方式与普通派生原子类似,但返回一个 Promise:
js
// 异步获取用户数据的原子
const userAtom = atom(async () => {
const response = await fetch('https://api.example.com/user');
const user = await response.json();
return user;
});
// 使用异步原子
function UserProfile() {
const user = useAtomValue(userAtom);
// 异步原子的初始状态是 Promise,所以需要处理加载和错误状态
if (user === undefined) {
return <div>加载中...</div>;
}
if (user instanceof Error) {
return <div>出错了: {user.message}</div>;
}
return (
<div>
<h2>{user.name}</h2>
<p>{user.email}</p>
</div>
);
}
带参数的异步原子
我们可以创建接收参数的异步原子,实现更灵活的数据获取:
js
// 创建一个接收 ID 参数的原子
const postAtom = atom(
(get) => get, // 这是一个占位符,实际我们会在使用时传递参数
async (get, set, id) => { // 第三个参数是我们传递的 ID
const response = await fetch(`https://api.example.com/posts/${id}`);
return response.json();
}
);
// 使用带参数的原子
function Post({ postId }) {
// 使用一个派生原子来传递参数
const specificPostAtom = atom((get) => get(postAtom, postId));
const post = useAtomValue(specificPostAtom);
if (!post) return <div>加载中...</div>;
return (
<article>
<h2>{post.title}</h2>
<p>{post.content}</p>
</article>
);
}
原子的持久化
Jotai 提供了 jotai/utils
模块,包含一些实用工具,其中 persistAtom
可以帮助我们实现状态的持久化。
js
import { atomWithStorage } from 'jotai/utils';
// 创建一个会自动持久化到 localStorage 的原子
const themeAtom = atomWithStorage('theme', 'light'); // 第一个参数是存储键名,第二个是默认值
function ThemeToggle() {
const [theme, setTheme] = useAtom(themeAtom);
return (
<button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
当前主题: {theme},点击切换
</button>
);
}
atomWithStorage
会自动将状态保存到 localStorage,并在应用重新加载时恢复。
原子的组合与依赖管理
Jotai 的一大优势是可以轻松组合多个原子,形成复杂的状态依赖关系。
js
// 基础原子
const todosAtom = atom([]);
const filterAtom = atom('all'); // 'all', 'active', 'completed'
// 派生原子:根据筛选条件过滤 todos
const filteredTodosAtom = atom((get) => {
const todos = get(todosAtom);
const filter = get(filterAtom);
switch (filter) {
case 'active':
return todos.filter(todo => !todo.completed);
case 'completed':
return todos.filter(todo => todo.completed);
default:
return todos;
}
});
// 派生原子:计算未完成的 todo 数量
const remainingTodosCountAtom = atom((get) => {
const todos = get(todosAtom);
return todos.filter(todo => !todo.completed).length;
});
这种设计让每个状态片段保持独立,同时又能灵活组合,大大提高了代码的可维护性。
性能优化
Jotai 的原子化设计本身就有助于性能优化,因为组件只会在其订阅的原子发生变化时重新渲染。不过,我们还可以进一步优化。
避免不必要的计算
对于计算成本较高的派生原子,可以使用 atomWithCache
来缓存计算结果:
js
import { atomWithCache } from 'jotai/utils';
// 计算成本高的派生原子
const expensiveCalculationAtom = atomWithCache((get) => {
const data = get(largeDataSetAtom);
// 执行复杂计算...
return result;
});
选择性订阅
当原子存储对象时,可以使用 selectAtom
只订阅对象的特定属性:
js
import { selectAtom } from 'jotai/utils';
// 用户信息原子
const userAtom = atom({
name: '张三',
age: 30,
address: '北京市'
});
// 只订阅用户的姓名
const userNameAtom = selectAtom(userAtom, (user) => user.name);
// 这个组件只会在用户姓名变化时重新渲染
function UserNameDisplay() {
const name = useAtomValue(userNameAtom);
return <div>姓名: {name}</div>;
}
实际应用示例:待办事项应用
让我们综合运用所学知识,创建一个完整的待办事项应用:
js
import { atom, useAtom, useAtomValue } from 'jotai';
import { atomWithStorage } from 'jotai/utils';
// 1. 定义原子
// 持久化存储的待办事项原子
const todosAtom = atomWithStorage('todos', []);
// 筛选条件原子
const filterAtom = atomWithStorage('todoFilter', 'all');
// 派生原子:过滤后的待办事项
const filteredTodosAtom = atom((get) => {
const todos = get(todosAtom);
const filter = get(filterAtom);
switch (filter) {
case 'active':
return todos.filter(todo => !todo.completed);
case 'completed':
return todos.filter(todo => todo.completed);
default:
return todos;
}
});
// 派生原子:待办事项统计
const todoStatsAtom = atom((get) => {
const todos = get(todosAtom);
const completed = todos.filter(todo => todo.completed).length;
const total = todos.length;
return {
total,
completed,
remaining: total - completed
};
});
// 2. 组件
function TodoInput() {
const [text, setText] = useState('');
const [todos, setTodos] = useAtom(todosAtom);
const handleSubmit = (e) => {
e.preventDefault();
if (!text.trim()) return;
// 添加新的待办事项
setTodos(prev => [
...prev,
{
id: Date.now(),
text,
completed: false
}
]);
setText('');
};
return (
<form onSubmit={handleSubmit}>
<input
type="text"
value={text}
onChange={(e) => setText(e.target.value)}
placeholder="添加新的待办事项..."
/>
<button type="submit">添加</button>
</form>
);
}
function TodoList() {
const todos = useAtomValue(filteredTodosAtom);
const [, setTodos] = useAtom(todosAtom);
const toggleTodo = (id) => {
setTodos(prev =>
prev.map(todo =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
)
);
};
const deleteTodo = (id) => {
setTodos(prev => prev.filter(todo => todo.id !== id));
};
if (todos.length === 0) {
return <p>没有待办事项</p>;
}
return (
<ul>
{todos.map(todo => (
<li key={todo.id}>
<input
type="checkbox"
checked={todo.completed}
onChange={() => toggleTodo(todo.id)}
/>
<span style={{ textDecoration: todo.completed ? 'line-through' : 'none' }}>
{todo.text}
</span>
<button onClick={() => deleteTodo(todo.id)}>删除</button>
</li>
))}
</ul>
);
}
function TodoFilters() {
const [filter, setFilter] = useAtom(filterAtom);
return (
<div>
<button
onClick={() => setFilter('all')}
style={{ fontWeight: filter === 'all' ? 'bold' : 'normal' }}
>
全部
</button>
<button
onClick={() => setFilter('active')}
style={{ fontWeight: filter === 'active' ? 'bold' : 'normal' }}
>
未完成
</button>
<button
onClick={() => setFilter('completed')}
style={{ fontWeight: filter === 'completed' ? 'bold' : 'normal' }}
>
已完成
</button>
</div>
);
}
function TodoStats() {
const { total, completed, remaining } = useAtomValue(todoStatsAtom);
const [, setTodos] = useAtom(todosAtom);
const clearCompleted = () => {
setTodos(prev => prev.filter(todo => !todo.completed));
};
return (
<div>
<p>
共 {total} 项,已完成 {completed} 项,剩余 {remaining} 项
</p>
{completed > 0 && (
<button onClick={clearCompleted}>清除已完成</button>
)}
</div>
);
}
// 3. 组合应用
function TodoApp() {
return (
<div>
<h1>待办事项</h1>
<TodoInput />
<TodoFilters />
<TodoList />
<TodoStats />
</div>
);
}
总结
Jotai 以其简洁的 API 和原子化的设计理念,为 React 应用提供了一种直观而高效的状态管理方案。通过本教程,你应该已经掌握了 Jotai 的核心概念和使用方法:
- 原子(atom)是状态管理的基本单位
- 使用
useAtom
、useAtomValue
和useSetAtom
与原子交互 - 派生原子可以基于其他原子计算得出
- 异步原子支持处理异步数据
- 原子可以轻松组合,形成复杂的状态依赖
- 内置工具支持状态持久化和性能优化
Jotai 的学习曲线平缓,但功能强大,适合各种规模的 React 应用。开始在你的项目中尝试使用 Jotai 吧,体验原子化状态管理的便捷与高效!