简介
Jotai 是一个为 React 提供的原子化状态管理库,采用自下而上的方法来进行状态管理。Jotai 受 Recoil 启发,通过组合原子来构建状态,并且渲染基于原子依赖性进行优化。这解决了 React 上下文的额外重新渲染问题,并消除了对 memoization 技术的需要。
核心特性
- 原子化状态 – 状态被分解为原子单元,可以独立管理和组合
- 零配置 – 无需像 Redux 那样的复杂配置和样板代码
- 类型安全 – 完全支持 TypeScript,提供良好的类型推断
- 高性能 – 自动优化渲染,避免不必要的组件重渲染
- 轻量级 – 核心包仅 2.4kB,API 简洁易用
- 灵活性 – 支持同步和异步状态,易于派生和组合
快速开始
安装
npm install jotai
# 或
yarn add jotai
# 或
pnpm add jotai
基础用法
import { atom, useAtom } from 'jotai'
// 创建一个原子状态
const countAtom = atom(0)
function Counter() {
// 使用原子状态,类似于 useState
const [count, setCount] = useAtom(countAtom)
return (
<div className="flex flex-col items-center justify-center min-h-[300px] p-6">
<h1 className="text-3xl font-medium mb-6 text-gray-800">
Count: <span className="text-4xl font-bold">{count}</span>
</h1>
<div className="flex gap-3">
<button
onClick={() => setCount(count + 1)}
className="px-4 py-2 bg-blue-500 text-white font-medium rounded hover:bg-blue-600"
>
Increment
</button>
<button
onClick={() => setCount(count - 1)}
className="px-4 py-2 bg-red-500 text-white font-medium rounded hover:bg-red-600"
>
Decrement
</button>
</div>
</div>
)
}
export default Counter
原子类型
基础原子
基础原子是最简单的状态单元,可以存储任何类型的值。
import { atom } from 'jotai'
// 基础类型
const boolAtom = atom(true)
const numberAtom = atom(42)
const stringAtom = atom('hello')
// 复杂类型
const objectAtom = atom({ name: 'John', age: 30 })
const arrayAtom = atom(['apple', 'banana', 'orange'])
派生原子
派生原子可以基于其他原子计算出新的状态,类似于 Vue 的计算属性或 MobX 的计算值。
import { atom } from 'jotai'
const countAtom = atom(0)
// 只读派生原子
const doubleCountAtom = atom((get) => get(countAtom) * 2)
// 可读写派生原子
const countryAtom = atom('Japan')
const citiesAtom = atom(['Tokyo', 'Kyoto', 'Osaka'])
const derivedAtom = atom(
(get) => ({
country: get(countryAtom),
cities: get(citiesAtom),
}),
(get, set, newValue) => {
// 可以同时更新多个原子
set(countryAtom, newValue.country)
set(citiesAtom, newValue.cities)
}
)
使用原子
Jotai 提供了几种使用原子的方式,根据不同的使用场景选择合适的 Hook。
useAtom
最基本的 Hook,类似于 React 的 useState
,用于读取和更新原子状态。
import { atom, useAtom } from 'jotai'
const textAtom = atom('hello')
function TextInput() {
const [text, setText] = useAtom(textAtom)
return (
<input
value={text}
onChange={(e) => setText(e.target.value)}
/>
)
}
useAtomValue 和 useSetAtom
当组件只需要读取或只需要写入原子状态时,可以使用这两个 Hook 来优化性能。
import { atom, useAtomValue, useSetAtom } from 'jotai'
const countAtom = atom(0)
// 只读组件
function DisplayCount() {
const count = useAtomValue(countAtom)
return <div>Count: {count}</div>
}
// 只写组件
function Controls() {
const setCount = useSetAtom(countAtom)
return (
<div>
<button onClick={() => setCount(c => c + 1)}>+1</button>
<button onClick={() => setCount(c => c - 1)}>-1</button>
<button onClick={() => setCount(0)}>Reset</button>
</div>
)
}
高级用法
异步原子
Jotai 支持异步原子,可以处理异步数据获取和更新。
import { atom, useAtom } from 'jotai'
// 异步读取原子
const userAtom = atom(async () => {
const response = await fetch('https://api.example.com/user')
return response.json()
})
// 异步写入原子
const postAtom = atom(
null,
async (get, set, newPost) => {
const response = await fetch('https://api.example.com/posts', {
method: 'POST',
body: JSON.stringify(newPost),
})
const result = await response.json()
// 可以更新其他原子
set(postsAtom, [...get(postsAtom), result])
return result
}
)
function AsyncComponent() {
const [user, setUser] = useAtom(userAtom)
const [, createPost] = useAtom(postAtom)
// 使用 React Suspense 处理加载状态
return (
<div>
<h1>Welcome, {user.name}</h1>
<button onClick={() => createPost({ title: 'New Post' })}>Create Post</button>
</div>
)
}
持久化
Jotai 提供了 atomWithStorage
工具函数,可以轻松实现状态的持久化。
import { useAtom } from 'jotai'
import { atomWithStorage } from 'jotai/utils'
// 自动保存到 localStorage
const darkModeAtom = atomWithStorage('darkMode', false)
function ThemeToggle() {
const [darkMode, setDarkMode] = useAtom(darkModeAtom)
return (
<div>
<h1>Current theme: {darkMode ? 'Dark' : 'Light'}</h1>
<button onClick={() => setDarkMode(!darkMode)}>
Toggle theme
</button>
</div>
)
}
原子族 (atomFamily)
原子族用于创建一组相关的原子,每个原子都有自己的状态,但共享相同的行为。
import { useAtom } from 'jotai'
import { atomFamily } from 'jotai/utils'
// 创建一个原子族,每个 ID 对应一个原子
const todoAtomFamily = atomFamily(
(id) => atom({ id, text: '', completed: false }),
(a, b) => a === b
)
function TodoItem({ id }) {
const [todo, setTodo] = useAtom(todoAtomFamily(id))
return (
<div>
<input
type="checkbox"
checked={todo.completed}
onChange={() => setTodo({ ...todo, completed: !todo.completed })}
/>
<input
value={todo.text}
onChange={(e) => setTodo({ ...todo, text: e.target.value })}
/>
</div>
)
}
实际应用示例
import { atom, useAtom } from 'jotai';
import { atomWithStorage } from 'jotai/utils';
// 创建原子状态
const todosAtom = atomWithStorage('todos', []); // 存储所有待办事项,使用 atomWithStorage 自动持久化到 localStorage
const todoInputAtom = atom(''); // 存储输入框的值
// 过滤类型:全部、已完成、未完成
const filterTypeAtom = atomWithStorage('filterType', 'all');
// 派生状态 - 计算已完成和未完成的任务数量
const todoStatsAtom = atom((get) => {
const todos = get(todosAtom);
const total = todos.length;
const completed = todos.filter(todo => todo.completed).length;
const uncompleted = total - completed;
return { total, completed, uncompleted };
});
// 派生状态 - 根据过滤类型筛选任务
const filteredTodosAtom = atom((get) => {
const todos = get(todosAtom);
const filterType = get(filterTypeAtom);
switch (filterType) {
case 'completed':
return todos.filter(todo => todo.completed);
case 'active':
return todos.filter(todo => !todo.completed);
default:
return todos;
}
});
export default function CssDemo() {
const [todos, setTodos] = useAtom(todosAtom);
const [filteredTodos] = useAtom(filteredTodosAtom);
const [todoInput, setTodoInput] = useAtom(todoInputAtom);
const [todoStats] = useAtom(todoStatsAtom);
const [filterType, setFilterType] = useAtom(filterTypeAtom);
// 添加新的待办事项
const addTodo = () => {
if (todoInput.trim() === '') return;
const newTodo = {
id: Date.now(),
text: todoInput,
completed: false
};
setTodos([...todos, newTodo]);
setTodoInput('');
};
// 切换待办事项的完成状态
const toggleTodo = (id) => {
setTodos(
todos.map(todo =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
)
);
};
// 删除待办事项
const deleteTodo = (id) => {
setTodos(todos.filter(todo => todo.id !== id));
};
// 清除所有已完成的任务
const clearCompleted = () => {
setTodos(todos.filter(todo => !todo.completed));
};
// 全部标记为已完成/未完成
const markAllAs = (completed) => {
setTodos(todos.map(todo => ({ ...todo, completed })));
};
return (
<div className="max-w-md mx-auto p-6 bg-white rounded-lg shadow-sm">
<h1 className="text-2xl font-bold text-gray-800 mb-4 text-center">Todo List</h1>
{/* 任务统计信息 */}
<div className="flex justify-between text-sm text-gray-500 mb-5 bg-gray-50 p-2 rounded">
<span className="px-2 py-1 bg-white rounded shadow-sm">总计: {todoStats.total}</span>
<span className="px-2 py-1 bg-white rounded shadow-sm">已完成: {todoStats.completed}</span>
<span className="px-2 py-1 bg-white rounded shadow-sm">未完成: {todoStats.uncompleted}</span>
</div>
{/* 添加待办事项表单 */}
<div className="flex mb-5">
<input
type="text"
value={todoInput}
onChange={(e) => setTodoInput(e.target.value)}
onKeyPress={(e) => e.key === 'Enter' && addTodo()}
placeholder="添加新的待办事项..."
className="flex-1 px-4 py-2 border border-gray-300 rounded-l focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
/>
<button
onClick={addTodo}
className="px-4 py-2 bg-blue-500 text-white font-medium rounded-r hover:bg-blue-600 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-opacity-50 transition-colors"
>
添加
</button>
</div>
{/* 过滤选项 */}
<div className="flex justify-center space-x-2 mb-4">
<button
onClick={() => setFilterType('all')}
className={`px-4 py-1.5 text-sm rounded-full transition-colors ${filterType === 'all' ? 'bg-blue-500 text-white' : 'bg-gray-100 text-gray-700 hover:bg-gray-200'}`}
>
全部
</button>
<button
onClick={() => setFilterType('active')}
className={`px-4 py-1.5 text-sm rounded-full transition-colors ${filterType === 'active' ? 'bg-blue-500 text-white' : 'bg-gray-100 text-gray-700 hover:bg-gray-200'}`}
>
未完成
</button>
<button
onClick={() => setFilterType('completed')}
className={`px-4 py-1.5 text-sm rounded-full transition-colors ${filterType === 'completed' ? 'bg-blue-500 text-white' : 'bg-gray-100 text-gray-700 hover:bg-gray-200'}`}
>
已完成
</button>
</div>
{/* 待办事项列表 */}
<ul className="space-y-2 mb-4 max-h-60 overflow-y-auto pr-1">
{filteredTodos.length === 0 ? (
<li className="text-gray-500 text-center py-6 border border-dashed border-gray-200 rounded-lg bg-gray-50">
{todos.length === 0 ? '暂无待办事项' : '没有符合条件的待办事项'}
</li>
) : (
filteredTodos.map(todo => (
<li
key={todo.id}
className="flex items-center justify-between p-3 border border-gray-200 rounded-lg hover:bg-gray-50 transition-colors"
>
<div className="flex items-center flex-1 min-w-0">
<input
type="checkbox"
checked={todo.completed}
onChange={() => toggleTodo(todo.id)}
className="h-5 w-5 text-blue-500 rounded focus:ring-blue-500"
/>
<span
className={`ml-3 truncate ${todo.completed ? 'line-through text-gray-400' : 'text-gray-800'}`}
>
{todo.text}
</span>
</div>
<button
onClick={() => deleteTodo(todo.id)}
className="text-red-500 hover:text-red-700 focus:outline-none ml-2 flex-shrink-0"
>
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path fillRule="evenodd" d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z" clipRule="evenodd" />
</svg>
</button>
</li>
))
)}
</ul>
{/* 底部操作栏 */}
{todos.length > 0 && (
<div className="flex justify-between pt-4 border-t border-gray-200">
<button
onClick={() => markAllAs(true)}
className="text-sm text-blue-500 hover:text-blue-700 focus:outline-none transition-colors"
>
全部完成
</button>
<button
onClick={() => markAllAs(false)}
className="text-sm text-blue-500 hover:text-blue-700 focus:outline-none transition-colors"
>
全部取消
</button>
<button
onClick={clearCompleted}
className={`text-sm ${todoStats.completed === 0 ? 'text-gray-400 cursor-not-allowed' : 'text-red-500 hover:text-red-700'} focus:outline-none transition-colors`}
disabled={todoStats.completed === 0}
>
清除已完成
</button>
</div>
)}
</div>
);
}
与其他状态管理库的比较
Jotai vs Redux
- 复杂度: Jotai 更简单,没有 actions、reducers、middleware 等概念
- 样板代码: Jotai 几乎没有样板代码,而 Redux 需要大量样板代码
- 学习曲线: Jotai 的学习曲线更平缓,API 更接近 React 原生 hooks
- 适用场景: Jotai 适合中小型应用,Redux 适合大型、复杂的应用
Jotai vs Recoil
- API: Jotai 的 API 更简洁,不需要 key 字符串
- 大小: Jotai 更小巧 (2.4kB vs Recoil 的 ~20kB)
- 配置: Jotai 不需要 Provider 包裹(虽然 SSR 时推荐使用)
- TypeScript: Jotai 对 TypeScript 的支持更好
Jotai vs Zustand
- 模型: Jotai 是原子模型,Zustand 是单一 store 模型
- 集成: Jotai 与 React 集成更紧密,Zustand 可以在 React 外使用
- 选择: 如果喜欢原子化状态,选 Jotai;如果喜欢单一 store,选 Zustand
最佳实践
- 原子粒度: 保持原子粒度适中,既不要过大也不要过小
- 原子组织: 将相关原子放在同一个文件中,便于管理
- 派生优先: 尽量使用派生原子而不是手动同步状态
- Hook 选择: 根据需要选择合适的 Hook (useAtom/useAtomValue/useSetAtom)
- 异步处理: 对于异步操作,使用 React Suspense 和 ErrorBoundary
总结
Jotai 是一个轻量级、高性能的 React 状态管理库,采用原子化的方式管理状态。它简化了全局状态管理,提供了优秀的开发体验和运行时性能。特别适合:
- 中小型 React 应用
- 需要简单状态管理的项目
- 对性能有要求的应用
- 喜欢函数式和原子化思想的开发者
通过原子化的状态管理方式,Jotai 既保持了使用的简单性,又提供了强大的状态组合能力,是 React 应用状态管理的绝佳选择。
Jotai:React轻量级原子化状态管理,告别重渲染困扰 - 高质量源码分享平台-免费下载各类网站源码与模板及前沿技术分享