Jotai:React轻量级状态管理新选择
Jotai 是一个受 Recoil 启发的 React 状态管理库,采用"原子化"的状态管理理念,让状态管理变得简单、灵活且可预测。本教程将从零开始,带你全面掌握 Jotai 的使用方法。
什么是 Jotai?
Jotai 发音为 “joe-tie”,源自日语"状態"(じょうたい - jōtai),意为"状态"。它的核心思想是将状态分割成一个个独立的"原子"(atom),组件可以精确订阅所需的状态,避免不必要的重渲染。
与 Redux 等传统状态管理库相比,Jotai 具有以下优势:
- 无需大量模板代码
- 原子化设计,精确更新
- 简洁的 API,易于学习
- 优秀的 TypeScript 支持
- 与 React 生态无缝集成
安装 Jotai
首先,我们需要安装 Jotai 包:
# 使用 npm
npm install jotai# 使用 yarn
yarn add jotai# 使用 pnpm
pnpm add jotai
核心概念:原子(Atom)
在 Jotai 中,“原子”(atom)是状态管理的基本单位。一个原子代表一个可共享的状态片段,可以是任何类型的值(数字、字符串、对象等)。
创建第一个原子
使用 atom
函数可以创建一个原子:
import { atom } from 'jotai';// 创建一个初始值为 0 的计数器原子
const countAtom = atom(0);
这是一个最基本的"可写原子"(writable atom),既可以读取其值,也可以修改它。
在组件中使用原子
要在组件中使用原子,我们需要用到 useAtom
钩子,它返回一个数组 [value, setValue]
,类似于 React 的 useState
:
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
钩子只返回原子的值,不提供修改方法,适合只读场景:
import { useAtomValue } from 'jotai';function CountDisplay() {// 只获取值,不获取更新函数const count = useAtomValue(countAtom);return <div>当前计数: {count}</div>;
}
useSetAtom:只写操作
useSetAtom
钩子只返回修改原子值的函数,不获取当前值,适合只需要修改状态的场景:
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
方法,用于获取其他原子的值:
// 派生原子:计算计数的两倍
const doubleCountAtom = atom((get) => {const count = get(countAtom);return count * 2;
});// 使用派生原子
function DoubleCountDisplay() {const doubleCount = useAtomValue(doubleCountAtom);return <div>计数的两倍: {doubleCount}</div>;
}
当 countAtom
的值发生变化时,doubleCountAtom
的值会自动重新计算,所有使用 doubleCountAtom
的组件都会更新。
组合多个原子
派生原子可以依赖多个原子,形成复杂的状态计算:
// 创建两个原子
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><inputplaceholder="名"value={firstName}onChange={(e) => setFirstName(e.target.value)}/><inputplaceholder="姓"value={lastName}onChange={(e) => setLastName(e.target.value)}/><p>全名: {fullName}</p></div>);
}
异步原子(Async Atoms)
Jotai 对异步操作有很好的支持,可以轻松创建依赖异步数据的原子。
创建异步原子
异步原子的创建方式与普通派生原子类似,但返回一个 Promise:
// 异步获取用户数据的原子
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>);
}
带参数的异步原子
我们可以创建接收参数的异步原子,实现更灵活的数据获取:
// 创建一个接收 ID 参数的原子
const postAtom = atom((get) => get, // 这是一个占位符,实际我们会在使用时传递参数async (get, set, id) => { // 第三个参数是我们传递的 IDconst 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
可以帮助我们实现状态的持久化。
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 的一大优势是可以轻松组合多个原子,形成复杂的状态依赖关系。
// 基础原子
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
来缓存计算结果:
import { atomWithCache } from 'jotai/utils';// 计算成本高的派生原子
const expensiveCalculationAtom = atomWithCache((get) => {const data = get(largeDataSetAtom);// 执行复杂计算...return result;
});
选择性订阅
当原子存储对象时,可以使用 selectAtom
只订阅对象的特定属性:
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>;
}
实际应用示例:待办事项应用
让我们综合运用所学知识,创建一个完整的待办事项应用:
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}><inputtype="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}><inputtype="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 吧,体验原子化状态管理的便捷与高效!