在前端开发中,状态管理是绕不开的核心问题。无论是简单的组件间状态共享,还是复杂的跨页面数据流管理,选择一个合适的工具至关重要。Jotai 作为一款轻量、灵活且与 React 深度集成的状态管理库,凭借其“原子化”设计思想,正在成为 React 生态中越来越受欢迎的解决方案。
本文将结合实际业务场景,从基础概念到高级用法,带你全面掌握 Jotai 的核心能力,并通过一个用户管理系统的实战案例,展示如何用 Jotai 构建高效、可维护的状态管理方案。
一、为什么选择 Jotai?
在 React 状态管理领域,Redux、Recoil、Zustand 等工具各有优劣。Jotai 的独特优势在于:
- 轻量高效:核心库仅约 2KB,无额外依赖,打包体积友好;
- 原子化设计:将状态拆分为最小单元(原子),通过组合实现复杂状态,降低耦合;
- React 原生兼容:基于 React Hooks(
useAtom),无需额外学习成本; - 灵活的派生能力:支持基于其他原子计算的派生原子,天然支持状态聚合;
- 持久化支持:内置
atomWithStorage等工具,轻松实现状态本地存储。
这些特性使 Jotai 特别适合中后台系统、复杂表单、多页面状态共享等场景。
二、Jotai 核心概念:原子(Atom)
Jotai 的核心是“原子”(Atom)。原子是状态的最小单元,可独立创建、更新和订阅。理解原子的各种形态和使用方式,是掌握 Jotai 的关键。
1. 基本原子:状态的“最小单元”
基本原子是最基础的原子类型,用于存储不可变数据。它通过 atom(initialValue) 创建,初始值可以是任意类型(对象、数组、基本类型等)。
示例:计数器原子
import { atom } from 'jotai';
// 定义一个存储数字的原子,初始值为 0
const countAtom = atom(0);
// 在组件中使用
function Counter() {
const [count, setCount] = useAtom(countAtom);
return (
<div>
<p>当前计数:{count}</p>
<button onClick={() => setCount(c => c + 1)}>+1</button>
</div>
);
}
特点:
- 初始值在首次订阅时初始化;
- 不可直接修改,需通过
setAtom或useSetAtom触发更新; - 支持函数式更新(如
c => c + 1),避免闭包陷阱。
2. 派生原子:状态的“计算视图”
派生原子基于其他原子动态计算状态,适用于需要聚合或转换数据的场景。它通过 atom(getter) 创建,其中 getter 是一个接收 get 函数的回调,用于读取其他原子的当前值。
示例:组合原子与计算属性
// 组合多个原子生成全名
const firstNameAtom = atom('张');
const lastNameAtom = atom('三');
const fullNameAtom = atom(
(get) => `${get(firstNameAtom)}${get(lastNameAtom)}`
);
// 计算属性(如双倍计数)
const doubleCountAtom = atom(
(get) => get(countAtom) * 2
);
特点:
- 自动依赖追踪:仅当依赖的原子变化时重新计算;
- 不可变输出:派生原子的值由 getter 函数返回,不可直接修改(除非是可写派生原子)。
3. 可写原子:状态的“修改入口”
可写原子允许直接修改状态,通常用于封装业务逻辑(如 API 调用后更新状态)。它通过 atom(getter, setter) 创建,包含两个回调:getter 用于读取状态,setter 用于更新状态。
示例:自定义递增逻辑
const incrementAtom = atom(
(get) => get(countAtom), // getter:读取当前值
(get, set) => set(countAtom, get(countAtom) + 1) // setter:更新值
);
// 组件中使用
function Counter() {
const [, increment] = useAtom(incrementAtom);
return <button onClick={increment}>递增</button>;
}
特点:
- 封装副作用:可将 API 请求、数据转换等逻辑内置到 setter 中;
- 支持异步操作:setter 可以是异步函数(需配合
async/await)。
4. 异步原子:处理异步状态
Jotai 原生支持异步原子,适用于需要处理异步操作(如 API 请求)的状态管理。异步原子的 getter 返回一个 Promise,组件会自动处理加载、成功、失败状态。
示例:用户信息加载
const userAtom = atom(async () => {
const response = await fetch('/api/user');
if (!response.ok) throw new Error('加载失败');
return response.json();
});
// 组件中使用
function UserProfile() {
const [user, setUser] = useAtom(userAtom);
if (user === undefined) return <div>加载中...</div>;
if (user instanceof Error) return <div>错误:{user.message}</div>;
return (
<div>
<h1>{user.name}</h1>
<button onClick={() => setUser({ name: '新名字' })}>修改名字</button>
</div>
);
}
特点:
- 自动跟踪异步状态:
undefined表示加载中,Error表示失败; - 支持手动更新:通过
setAtom覆盖异步结果。
5. 持久化原子:状态的“本地存储”
通过 atomWithStorage,可以轻松实现状态的本地持久化(如主题模式、用户偏好)。它会自动同步原子状态到 localStorage,并在页面刷新后恢复状态。
示例:主题模式持久化
import { atomWithStorage } from 'jotai/utils';
// 定义持久化原子(键名 'theme',默认值 'light')
const themeAtom = atomWithStorage('theme', 'light');
// 组件中使用
function ThemeSwitcher() {
const [theme, setTheme] = useAtom(themeAtom);
return (
<button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
切换主题(当前:{theme})
</button>
);
}
三、进阶:动态状态管理(atomFamily)
当需要管理一组结构相似但标识不同的状态时(如待办事项列表中的每个待办项),atomFamily 是更高效的选择。它通过一个工厂函数生成多个原子,每个原子对应一个唯一标识(如 ID)。
1. 基础用法:创建原子家族
atomFamily 来自 jotai/utils,接收一个工厂函数,该函数的参数是唯一标识(如 todoId),返回一个原子。
示例:待办事项原子家族
import { atomFamily } from 'jotai/utils';
// 创建原子家族:每个 todoId 对应一个原子
const todoAtomFamily = atomFamily((todoId) =>
atom({
id: todoId,
text: '',
completed: false
})
);
特点:
- 每个
todoId对应独立的原子,状态隔离; - 支持动态增删:通过
add/remove管理原子集合。
2. 实战:待办事项列表
结合 atomFamily 和组件,可以实现一个动态的待办事项列表,每个事项独立管理状态。
步骤 1:定义原子家族和状态集合
// 存储所有 todoId 的原子(用于遍历)
export const todoIdsAtom = atom<string[]>([]);
// 原子家族:每个 todoId 对应一个原子
export const todoAtomFamily = atomFamily((todoId) =>
atom({ id: todoId, text: '', completed: false })
);
步骤 2:添加/删除待办事项
// 添加待办事项
const addTodo = (text: string) => {
const newId = Date.now().toString(); // 生成唯一 ID
set(todoIdsAtom, (prev) => [...prev, newId]);
set(todoAtomFamily(newId), { id: newId, text, completed: false });
};
// 删除待办事项
const deleteTodo = (todoId: string) => {
set(todoIdsAtom, (prev) => prev.filter(id => id !== todoId));
todoAtomFamily.remove(todoId); // 移除原子
};
步骤 3:组件中渲染待办列表
function TodoList() {
const todoIds = useAtomValue(todoIdsAtom);
return (
<ul>
{todoIds.map((todoId) => {
const [todo, setTodo] = useAtom(todoAtomFamily(todoId));
return (
<li key={todoId}>
<input
type="checkbox"
checked={todo.completed}
onChange={(e) => setTodo({ ...todo, completed: e.target.checked })}
/>
<span>{todo.text}</span>
<button onClick={() => deleteTodo(todoId)}>删除</button>
</li>
);
})}
</ul>
);
}
四、实战:用户管理系统(完整案例)
通过一个用户管理系统的实战,我们将综合运用 Jotai 的各种能力,展示如何构建一个结构清晰、可维护的状态管理方案。
1. 状态设计
我们需要管理以下状态:
- 用户列表数据(
usersAtom) - 搜索参数(姓名/状态,
searchParamsAtom) - 分页参数(当前页/每页大小/总条数,
paginationAtom) - 加载状态(
loadingAtom) - 当前编辑的用户(
currentEditUserAtom)
代码实现(src/stores/userStore.ts):
import { atom, atomWithImmer, atomWithStorage } from 'jotai';
import { produce } from 'immer';
import { User } from '@/types'; // 假设已定义 User 类型
// --------------------------
// 基础状态原子
// --------------------------
// 用户列表(使用 Immer 简化数组操作)
export const usersAtom = atomWithImmer<User[]>([]);
// 搜索参数(姓名/状态)
export const searchParamsAtom = atom<{ name?: string; status?: 'active' | 'inactive' }>({});
// 分页参数(当前页/每页大小/总条数)
export const paginationAtom = atom<{
current: number;
pageSize: number;
total: number;
}>({ current: 1, pageSize: 10, total: 0 });
// 加载状态
export const loadingAtom = atom<boolean>(false);
// 当前编辑的用户(表单初始化)
export const currentEditUserAtom = atom<User | null>(null);
// --------------------------
// 派生原子(自动计算请求参数)
// --------------------------
// 合并搜索+分页参数(用于接口请求)
export const fetchParamsAtom = atom((get) => ({
...get(searchParamsAtom),
page: get(paginationAtom).current,
pageSize: get(paginationAtom).pageSize,
}));
// --------------------------
// 自定义钩子(封装业务逻辑)
// --------------------------
/**
* 加载用户列表(自动触发)
*/
export const useLoadUsers = () => {
const setUsers = useSetAtom(usersAtom);
const setLoading = useSetAtom(loadingAtom);
const fetchParams = useAtomValue(fetchParamsAtom);
useEffect(() => {
const loadUsers = async () => {
setLoading(true);
try {
// 模拟 API 请求(替换为真实接口)
const res = await fetch(`/api/users?page=${fetchParams.page}&pageSize=${fetchParams.pageSize}`);
const data = await res.json();
// 使用 Immer 安全更新列表和总条数
setUsers(produce((draft) => {
draft.length = 0; // 清空旧数据
draft.push(...data.list);
get(paginationAtom).total = data.total; // 更新总条数
}));
} catch (err) {
message.error('加载用户失败');
} finally {
setLoading(false);
}
};
loadUsers();
}, [fetchParams.current, fetchParams.pageSize]); // 依赖分页变化
};
/**
* 搜索用户(手动触发)
*/
export const useSearchUsers = () => {
const setSearchParams = useSetAtom(searchParamsAtom);
const loadUsers = useLoadUsers();
return (params: { name?: string; status?: 'active' | 'inactive' }) => {
setSearchParams(params);
loadUsers(); // 搜索时重置分页到第一页
};
};
/**
* 删除用户
*/
export const useDeleteUser = () => {
const setLoading = useSetAtom(loadingAtom);
const loadUsers = useLoadUsers();
return async (userId: string) => {
setLoading(true);
try {
await fetch(`/api/users/${userId}`, { method: 'DELETE' });
message.success('删除成功');
loadUsers(); // 删除后刷新列表
} catch (err) {
message.error('删除失败');
} finally {
setLoading(false);
}
};
};
/**
* 修改用户(打开表单并初始化)
*/
export const useEditUser = () => {
const setCurrentEditUser = useSetAtom(currentEditUserAtom);
const [form] = Form.useForm(); // Ant Design 表单实例
return {
open: (user: User) => {
setCurrentEditUser(user);
form.setFieldsValue(user); // 初始化表单值
},
close: () => setCurrentEditUser(null),
form,
};
};
2. 组件集成
在组件中,我们只需订阅相关原子,即可实时响应状态变化。以下是用户列表组件(src/components/UserList.tsx)的实现:
import { Card, Table, Button, message, Space } from 'antd';
import { useAtomValue, useSetAtom } from 'jotai';
import {
usersAtom,
paginationAtom,
loadingAtom,
useLoadUsers,
useDeleteUser,
useEditUser
} from '../stores/userStore';
const UserList = () => {
// 订阅状态原子
const users = useAtomValue(usersAtom);
const pagination = useAtomValue(paginationAtom);
const loading = useAtomValue(loadingAtom);
const loadUsers = useLoadUsers();
const deleteUser = useDeleteUser();
const { open: openEditModal } = useEditUser();
// 表格列定义
const columns = [
{ title: '姓名', dataIndex: 'name' },
{ title: '状态', dataIndex: 'status', render: (s) => s === 'active' ? '启用' : '禁用' },
{ title: '邮箱', dataIndex: 'email' },
{
title: '操作',
key: 'action',
render: (_, record) => (
<Space>
<Button type="link" onClick={() => openEditModal(record)}>编辑</Button>
<Button type="link" danger onClick={() => deleteUser(record.id)}>删除</Button>
</Space>
),
},
];
// 分页配置
const paginationConfig = {
current: pagination.current,
pageSize: pagination.pageSize,
total: pagination.total,
onChange: (page, pageSize) => {
set(paginationAtom, { ...pagination, current: page, pageSize }); // 分页变化时更新原子
},
};
return (
<Card
title="用户列表"
extra={
<Button type="primary" onClick={() => openEditModal(null)}>新增用户</Button>
}
>
<Table
loading={loading}
dataSource={users}
columns={columns}
rowKey="id"
pagination={paginationConfig}
/>
</Card>
);
};
export default UserList;
3. 关键优势总结
在这个实战案例中,Jotai 展现了以下优势:
- 状态解耦:通过原子拆分,用户列表、搜索参数、分页等状态独立管理,降低耦合;
- 自动更新:派生原子
fetchParamsAtom自动合并搜索和分页参数,无需手动拼接; - 高效更新:
atomWithImmer允许直接修改数组(如清空旧数据),Immer 自动转换为不可变更新; - 逻辑复用:自定义钩子(如
useLoadUsers)封装业务逻辑,避免重复代码; - 组件简洁:组件只需订阅原子,无需关心状态来源,专注渲染逻辑。
五、最佳实践与注意事项
1. 原子命名规范
为原子添加明确的命名空间(如 usersAtom、searchParamsAtom),避免命名冲突,提高代码可读性。
2. 避免过度派生
派生原子适用于简单的计算(如拼接字符串、过滤数组),复杂的业务逻辑(如 API 请求)应封装在自定义钩子中,避免原子成为“逻辑黑箱”。
3. 异步原子的错误处理
异步原子需显式处理错误状态(如 user instanceof Error),并在组件中展示友好的错误提示,避免应用崩溃。
4. 持久化原子的键名管理
使用 atomWithStorage 时,确保键名唯一(如 'theme'、'userInfo'),避免与其他库或模块的存储键冲突。
5. 性能优化
- 对于大数据量的列表,使用
atomWithImmer配合 Immer 的高效不可变更新; - 避免在
useEffect中订阅过多原子,可通过useMemo缓存派生值; - 动态原子(如
atomFamily)需及时清理不再使用的原子(如todoAtomFamily.remove(todoId))。
结语
Jotai 凭借其原子化的设计思想和灵活的 API,为 React 状态管理提供了一种轻量、高效的解决方案。无论是简单的计数器,还是复杂的多页面状态共享,Jotai 都能通过原子的组合与派生,帮助开发者构建清晰、可维护的状态管理架构。