簡介
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() {// 使用原子狀態,類似于 useStateconst [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><inputtype="checkbox"checked={todo.completed}onChange={() => setTodo({ ...todo, completed: !todo.completed })}/><inputvalue={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"><inputtype="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"/><buttononClick={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"><buttononClick={() => 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><buttononClick={() => 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><buttononClick={() => 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"><inputtype="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><buttononClick={() => 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"><buttononClick={() => markAllAs(true)}className="text-sm text-blue-500 hover:text-blue-700 focus:outline-none transition-colors">全部完成</button><buttononClick={() => markAllAs(false)}className="text-sm text-blue-500 hover:text-blue-700 focus:outline-none transition-colors">全部取消</button><buttononClick={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輕量級原子化狀態管理,告別重渲染困擾 - 高質量源碼分享平臺-免費下載各類網站源碼與模板及前沿技術分享