Appearance
Recoil原子状态
概述
Recoil是Facebook开发的React状态管理库,采用原子化状态管理理念。它将状态拆分为独立的原子(atoms),通过自底向上的方式构建复杂的状态图,提供了优秀的性能和并发模式支持。
为什么选择Recoil
Recoil的优势
- 原子化设计:状态被拆分为最小的可复用单元
- 依赖追踪:自动追踪状态依赖关系
- 并发模式:完美支持React 18+的并发特性
- 时间旅行:内置调试和时间旅行功能
- 异步支持:原生支持异步状态和Suspense
- 细粒度更新:只有依赖的组件才会重渲染
- 开发工具:强大的调试和可视化工具
核心概念
jsx
import { atom, selector, useRecoilState, useRecoilValue } from 'recoil';
// Atom - 状态的最小单元
const counterAtom = atom({
key: 'counter', // 全局唯一key
default: 0 // 默认值
});
// Selector - 派生状态
const doubledCounterSelector = selector({
key: 'doubledCounter',
get: ({get}) => {
const count = get(counterAtom);
return count * 2;
}
});
// 在组件中使用
function Counter() {
const [count, setCount] = useRecoilState(counterAtom);
const doubledCount = useRecoilValue(doubledCounterSelector);
return (
<div>
<p>Count: {count}</p>
<p>Doubled: {doubledCount}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
}安装和基础设置
安装
bash
# npm
npm install recoil
# yarn
yarn add recoil
# pnpm
pnpm add recoilRecoilRoot设置
jsx
import { RecoilRoot } from 'recoil';
function App() {
return (
<RecoilRoot>
<MainApp />
</RecoilRoot>
);
}
// 使用initializeState初始化状态
function AppWithInitialState() {
return (
<RecoilRoot initializeState={({set}) => {
set(counterAtom, 10);
set(userAtom, { name: 'John', email: 'john@example.com' });
}}>
<MainApp />
</RecoilRoot>
);
}Atoms详解
基础Atom
jsx
import { atom } from 'recoil';
// 基本类型
const countAtom = atom({
key: 'count',
default: 0
});
const nameAtom = atom({
key: 'name',
default: 'John Doe'
});
const activeAtom = atom({
key: 'active',
default: false
});
// 对象类型
const userAtom = atom({
key: 'user',
default: {
id: null,
name: '',
email: '',
preferences: {}
}
});
// 数组类型
const todosAtom = atom({
key: 'todos',
default: []
});
// 复杂默认值
const configAtom = atom({
key: 'config',
default: {
theme: 'light',
language: 'en-US',
features: {
darkMode: true,
notifications: true
},
cache: new Map(),
lastUpdated: new Date().toISOString()
}
});Atom Effects
Atom Effects提供了强大的副作用处理能力:
jsx
import { atom } from 'recoil';
// 持久化Effect
const persistEffect = (key) => ({onSet}) => {
onSet(newValue => {
if (newValue instanceof DefaultValue) {
localStorage.removeItem(key);
} else {
localStorage.setItem(key, JSON.stringify(newValue));
}
});
};
// 同步Effect
const syncEffect = (key) => ({setSelf, onSet}) => {
// 初始化时从localStorage读取
const savedValue = localStorage.getItem(key);
if (savedValue != null) {
setSelf(JSON.parse(savedValue));
}
// 监听变化并保存
onSet(newValue => {
if (newValue instanceof DefaultValue) {
localStorage.removeItem(key);
} else {
localStorage.setItem(key, JSON.stringify(newValue));
}
});
};
// 使用Effects的Atom
const persistentCountAtom = atom({
key: 'persistentCount',
default: 0,
effects: [
syncEffect('persistentCount')
]
});
// 网络同步Effect
const networkSyncEffect = (endpoint) => ({setSelf, onSet, trigger}) => {
// 只在用户操作时触发
if (trigger === 'get') {
// 从服务器获取初始值
fetch(endpoint)
.then(res => res.json())
.then(data => setSelf(data))
.catch(err => console.error('Failed to load:', err));
}
// 监听变化并同步到服务器
onSet(newValue => {
if (!(newValue instanceof DefaultValue)) {
fetch(endpoint, {
method: 'PUT',
body: JSON.stringify(newValue),
headers: { 'Content-Type': 'application/json' }
}).catch(err => console.error('Failed to sync:', err));
}
});
};
const serverSyncedAtom = atom({
key: 'serverSynced',
default: null,
effects: [
networkSyncEffect('/api/user-preferences')
]
});
// 日志Effect
const loggerEffect = (label) => ({onSet}) => {
onSet(newValue => {
console.log(`[${label}] State changed:`, newValue);
});
};
// 验证Effect
const validationEffect = (validator) => ({setSelf, onSet}) => {
onSet(newValue => {
if (!(newValue instanceof DefaultValue)) {
const isValid = validator(newValue);
if (!isValid) {
console.warn('Invalid value rejected:', newValue);
return; // 阻止状态更新
}
}
});
};
const validatedAtom = atom({
key: 'validated',
default: '',
effects: [
validationEffect(value => typeof value === 'string' && value.length > 0),
loggerEffect('ValidatedAtom')
]
});Atom Family
Atom Family用于创建参数化的atoms:
jsx
import { atomFamily } from 'recoil';
// 基础Atom Family
const userAtomFamily = atomFamily({
key: 'user',
default: (userId) => ({
id: userId,
name: '',
email: '',
loading: false
})
});
// 使用
function UserProfile({ userId }) {
const [user, setUser] = useRecoilState(userAtomFamily(userId));
return (
<div>
<h2>{user.name}</h2>
<p>{user.email}</p>
</div>
);
}
// 带异步默认值的Atom Family
const userDataFamily = atomFamily({
key: 'userData',
default: async (userId) => {
const response = await fetch(`/api/users/${userId}`);
return response.json();
}
});
// 复杂参数的Atom Family
const itemFamily = atomFamily({
key: 'item',
default: ({id, type}) => ({
id,
type,
data: null,
lastUpdated: null
})
});
// 使用复杂参数
function ItemComponent() {
const [item, setItem] = useRecoilState(itemFamily({id: 1, type: 'product'}));
return <div>{item.data?.name}</div>;
}
// 带Effects的Atom Family
const cachedDataFamily = atomFamily({
key: 'cachedData',
default: null,
effects: (param) => [
({setSelf, onSet}) => {
// 从缓存加载
const cached = localStorage.getItem(`cache-${param}`);
if (cached) {
setSelf(JSON.parse(cached));
}
// 保存到缓存
onSet(newValue => {
if (newValue instanceof DefaultValue) {
localStorage.removeItem(`cache-${param}`);
} else {
localStorage.setItem(`cache-${param}`, JSON.stringify(newValue));
}
});
}
]
});使用Atoms
基础Hooks
jsx
import {
useRecoilState,
useRecoilValue,
useSetRecoilState,
useResetRecoilState
} from 'recoil';
function TodoApp() {
// 读写状态
const [todos, setTodos] = useRecoilState(todosAtom);
// 只读状态
const todoCount = useRecoilValue(todoCountSelector);
// 只写状态
const setFilter = useSetRecoilState(filterAtom);
// 重置状态
const resetTodos = useResetRecoilState(todosAtom);
const addTodo = (text) => {
setTodos(prev => [
...prev,
{ id: Date.now(), text, completed: false }
]);
};
return (
<div>
<p>Total todos: {todoCount}</p>
<button onClick={() => addTodo('New todo')}>Add Todo</button>
<button onClick={resetTodos}>Clear All</button>
{todos.map(todo => (
<div key={todo.id}>
<span>{todo.text}</span>
<input
type="checkbox"
checked={todo.completed}
onChange={(e) => {
setTodos(prev => prev.map(t =>
t.id === todo.id
? { ...t, completed: e.target.checked }
: t
));
}}
/>
</div>
))}
</div>
);
}回调Hooks
jsx
import { useRecoilCallback } from 'recoil';
function TodoManager() {
// 批量操作
const batchUpdateTodos = useRecoilCallback(({snapshot, set}) =>
async (updates) => {
const currentTodos = await snapshot.getPromise(todosAtom);
const newTodos = currentTodos.map(todo => {
const update = updates.find(u => u.id === todo.id);
return update ? { ...todo, ...update.changes } : todo;
});
set(todosAtom, newTodos);
}
);
// 异步操作
const syncTodosToServer = useRecoilCallback(({snapshot}) =>
async () => {
const todos = await snapshot.getPromise(todosAtom);
try {
await fetch('/api/todos', {
method: 'POST',
body: JSON.stringify(todos)
});
} catch (error) {
console.error('Sync failed:', error);
}
}
);
// 获取快照
const logCurrentState = useRecoilCallback(({snapshot}) =>
async () => {
const todos = await snapshot.getPromise(todosAtom);
const filter = await snapshot.getPromise(filterAtom);
console.log('Current state:', { todos, filter });
}
);
return (
<div>
<button onClick={() => batchUpdateTodos([
{ id: 1, changes: { completed: true } },
{ id: 2, changes: { text: 'Updated text' } }
])}>
Batch Update
</button>
<button onClick={syncTodosToServer}>
Sync to Server
</button>
<button onClick={logCurrentState}>
Log State
</button>
</div>
);
}实战案例
案例1:用户管理系统
jsx
import { atom, selector, atomFamily, useRecoilState, useRecoilValue } from 'recoil';
// 用户列表atom
const usersAtom = atom({
key: 'users',
default: []
});
// 当前用户ID atom
const currentUserIdAtom = atom({
key: 'currentUserId',
default: null
});
// 用户搜索查询atom
const userSearchQueryAtom = atom({
key: 'userSearchQuery',
default: ''
});
// 用户排序atom
const userSortAtom = atom({
key: 'userSort',
default: { field: 'name', direction: 'asc' }
});
// 单个用户atom family
const userAtomFamily = atomFamily({
key: 'user',
default: (userId) => {
// 从用户列表中查找
return selector({
key: `userFromList-${userId}`,
get: ({get}) => {
const users = get(usersAtom);
return users.find(user => user.id === userId) || null;
}
});
}
});
// 过滤后的用户列表selector
const filteredUsersSelector = selector({
key: 'filteredUsers',
get: ({get}) => {
const users = get(usersAtom);
const searchQuery = get(userSearchQueryAtom);
const sort = get(userSortAtom);
// 搜索过滤
let filtered = users.filter(user =>
user.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
user.email.toLowerCase().includes(searchQuery.toLowerCase())
);
// 排序
filtered.sort((a, b) => {
const aValue = a[sort.field];
const bValue = b[sort.field];
if (aValue < bValue) return sort.direction === 'asc' ? -1 : 1;
if (aValue > bValue) return sort.direction === 'asc' ? 1 : -1;
return 0;
});
return filtered;
}
});
// 当前用户selector
const currentUserSelector = selector({
key: 'currentUser',
get: ({get}) => {
const userId = get(currentUserIdAtom);
if (!userId) return null;
return get(userAtomFamily(userId));
}
});
// 用户统计selector
const userStatsSelector = selector({
key: 'userStats',
get: ({get}) => {
const users = get(usersAtom);
return {
total: users.length,
active: users.filter(u => u.active).length,
inactive: users.filter(u => !u.active).length,
admins: users.filter(u => u.role === 'admin').length
};
}
});
// 组件
function UserManagement() {
const [users, setUsers] = useRecoilState(usersAtom);
const [searchQuery, setSearchQuery] = useRecoilState(userSearchQueryAtom);
const [sort, setSort] = useRecoilState(userSortAtom);
const filteredUsers = useRecoilValue(filteredUsersSelector);
const stats = useRecoilValue(userStatsSelector);
const addUser = (userData) => {
const newUser = {
id: Date.now(),
...userData,
createdAt: new Date().toISOString(),
active: true
};
setUsers(prev => [...prev, newUser]);
};
const deleteUser = (userId) => {
setUsers(prev => prev.filter(user => user.id !== userId));
};
return (
<div>
<div className="user-stats">
<h2>User Statistics</h2>
<p>Total: {stats.total}</p>
<p>Active: {stats.active}</p>
<p>Inactive: {stats.inactive}</p>
<p>Admins: {stats.admins}</p>
</div>
<div className="user-controls">
<input
type="text"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
placeholder="Search users..."
/>
<select
value={`${sort.field}-${sort.direction}`}
onChange={(e) => {
const [field, direction] = e.target.value.split('-');
setSort({ field, direction });
}}
>
<option value="name-asc">Name (A-Z)</option>
<option value="name-desc">Name (Z-A)</option>
<option value="email-asc">Email (A-Z)</option>
<option value="createdAt-desc">Newest First</option>
<option value="createdAt-asc">Oldest First</option>
</select>
</div>
<UserList users={filteredUsers} onDelete={deleteUser} />
<AddUserForm onAdd={addUser} />
</div>
);
}
function UserList({ users, onDelete }) {
return (
<div className="user-list">
{users.map(user => (
<UserCard key={user.id} userId={user.id} onDelete={onDelete} />
))}
</div>
);
}
function UserCard({ userId, onDelete }) {
const user = useRecoilValue(userAtomFamily(userId));
const [currentUserId, setCurrentUserId] = useRecoilState(currentUserIdAtom);
if (!user) return null;
return (
<div className={`user-card ${currentUserId === userId ? 'selected' : ''}`}>
<h3>{user.name}</h3>
<p>{user.email}</p>
<p>Role: {user.role}</p>
<p>Status: {user.active ? 'Active' : 'Inactive'}</p>
<div className="user-actions">
<button onClick={() => setCurrentUserId(userId)}>
Select
</button>
<button onClick={() => onDelete(userId)}>
Delete
</button>
</div>
</div>
);
}
function CurrentUserProfile() {
const currentUser = useRecoilValue(currentUserSelector);
if (!currentUser) {
return <div>No user selected</div>;
}
return (
<div className="current-user-profile">
<h2>Current User Profile</h2>
<p>Name: {currentUser.name}</p>
<p>Email: {currentUser.email}</p>
<p>Role: {currentUser.role}</p>
<p>Created: {new Date(currentUser.createdAt).toLocaleDateString()}</p>
</div>
);
}案例2:购物车系统
jsx
import { atom, selector, atomFamily, selectorFamily } from 'recoil';
// 产品数据atom family
const productFamily = atomFamily({
key: 'product',
default: async (productId) => {
const response = await fetch(`/api/products/${productId}`);
return response.json();
}
});
// 购物车items atom
const cartItemsAtom = atom({
key: 'cartItems',
default: [],
effects: [
({setSelf, onSet}) => {
// 从localStorage加载
const saved = localStorage.getItem('cartItems');
if (saved) {
setSelf(JSON.parse(saved));
}
// 保存到localStorage
onSet(newValue => {
if (newValue instanceof DefaultValue) {
localStorage.removeItem('cartItems');
} else {
localStorage.setItem('cartItems', JSON.stringify(newValue));
}
});
}
]
});
// 折扣代码atom
const discountCodeAtom = atom({
key: 'discountCode',
default: ''
});
// 运费选项atom
const shippingOptionAtom = atom({
key: 'shippingOption',
default: null
});
// 购物车商品详情selector
const cartItemsWithDetailsSelector = selector({
key: 'cartItemsWithDetails',
get: async ({get}) => {
const items = get(cartItemsAtom);
const itemsWithDetails = await Promise.all(
items.map(async (item) => {
const product = await get(productFamily(item.productId));
return {
...item,
product,
subtotal: product.price * item.quantity
};
})
);
return itemsWithDetails;
}
});
// 购物车统计selector
const cartStatsSelector = selector({
key: 'cartStats',
get: ({get}) => {
const items = get(cartItemsWithDetailsSelector);
const subtotal = items.reduce((sum, item) => sum + item.subtotal, 0);
const itemCount = items.reduce((sum, item) => sum + item.quantity, 0);
return {
itemCount,
subtotal,
uniqueItems: items.length
};
}
});
// 折扣计算selector
const discountSelector = selector({
key: 'discount',
get: async ({get}) => {
const code = get(discountCodeAtom);
const stats = get(cartStatsSelector);
if (!code) {
return { amount: 0, percentage: 0, valid: false };
}
try {
const response = await fetch(`/api/discounts/${code}`);
const discount = await response.json();
if (discount.valid) {
const amount = discount.type === 'percentage'
? stats.subtotal * (discount.value / 100)
: discount.value;
return {
amount: Math.min(amount, stats.subtotal),
percentage: discount.type === 'percentage' ? discount.value : 0,
valid: true,
code: discount.code
};
}
} catch (error) {
console.error('Discount validation failed:', error);
}
return { amount: 0, percentage: 0, valid: false };
}
});
// 运费计算selector
const shippingCostSelector = selector({
key: 'shippingCost',
get: ({get}) => {
const option = get(shippingOptionAtom);
const stats = get(cartStatsSelector);
if (!option) return 0;
// 免费运费门槛
if (stats.subtotal >= 100) {
return 0;
}
return option.cost;
}
});
// 总计selector
const cartTotalSelector = selector({
key: 'cartTotal',
get: ({get}) => {
const stats = get(cartStatsSelector);
const discount = get(discountSelector);
const shippingCost = get(shippingCostSelector);
const tax = (stats.subtotal - discount.amount) * 0.08; // 8% tax
return {
subtotal: stats.subtotal,
discount: discount.amount,
shipping: shippingCost,
tax: Math.max(0, tax),
total: stats.subtotal - discount.amount + shippingCost + Math.max(0, tax)
};
}
});
// 购物车操作hooks
function useCartActions() {
const setCartItems = useSetRecoilState(cartItemsAtom);
const addToCart = useRecoilCallback(({set, snapshot}) =>
async (productId, quantity = 1, options = {}) => {
const currentItems = await snapshot.getPromise(cartItemsAtom);
const existingItemIndex = currentItems.findIndex(
item => item.productId === productId &&
JSON.stringify(item.options) === JSON.stringify(options)
);
if (existingItemIndex !== -1) {
const newItems = [...currentItems];
newItems[existingItemIndex].quantity += quantity;
set(cartItemsAtom, newItems);
} else {
const newItem = {
id: Date.now(),
productId,
quantity,
options,
addedAt: new Date().toISOString()
};
set(cartItemsAtom, [...currentItems, newItem]);
}
}
);
const removeFromCart = (itemId) => {
setCartItems(prev => prev.filter(item => item.id !== itemId));
};
const updateQuantity = (itemId, quantity) => {
if (quantity <= 0) {
removeFromCart(itemId);
return;
}
setCartItems(prev =>
prev.map(item =>
item.id === itemId ? { ...item, quantity } : item
)
);
};
const clearCart = () => {
setCartItems([]);
};
return {
addToCart,
removeFromCart,
updateQuantity,
clearCart
};
}
// 组件
function ShoppingCart() {
const items = useRecoilValue(cartItemsWithDetailsSelector);
const stats = useRecoilValue(cartStatsSelector);
const total = useRecoilValue(cartTotalSelector);
const { removeFromCart, updateQuantity, clearCart } = useCartActions();
if (items.length === 0) {
return (
<div className="cart-empty">
<p>Your cart is empty</p>
</div>
);
}
return (
<div className="shopping-cart">
<div className="cart-header">
<h2>Shopping Cart ({stats.itemCount} items)</h2>
<button onClick={clearCart}>Clear Cart</button>
</div>
<div className="cart-items">
{items.map(item => (
<CartItem
key={item.id}
item={item}
onUpdateQuantity={updateQuantity}
onRemove={removeFromCart}
/>
))}
</div>
<CartSummary total={total} />
<DiscountSection />
<ShippingSection />
</div>
);
}
function CartItem({ item, onUpdateQuantity, onRemove }) {
return (
<div className="cart-item">
<img src={item.product.image} alt={item.product.name} />
<div className="item-details">
<h4>{item.product.name}</h4>
<p>${item.product.price}</p>
{Object.entries(item.options).map(([key, value]) => (
<p key={key}>{key}: {value}</p>
))}
</div>
<div className="quantity-controls">
<button onClick={() => onUpdateQuantity(item.id, item.quantity - 1)}>
-
</button>
<span>{item.quantity}</span>
<button onClick={() => onUpdateQuantity(item.id, item.quantity + 1)}>
+
</button>
</div>
<div className="item-total">
${item.subtotal.toFixed(2)}
</div>
<button onClick={() => onRemove(item.id)}>Remove</button>
</div>
);
}
function CartSummary({ total }) {
return (
<div className="cart-summary">
<div className="summary-line">
<span>Subtotal:</span>
<span>${total.subtotal.toFixed(2)}</span>
</div>
{total.discount > 0 && (
<div className="summary-line discount">
<span>Discount:</span>
<span>-${total.discount.toFixed(2)}</span>
</div>
)}
<div className="summary-line">
<span>Shipping:</span>
<span>${total.shipping.toFixed(2)}</span>
</div>
<div className="summary-line">
<span>Tax:</span>
<span>${total.tax.toFixed(2)}</span>
</div>
<div className="summary-line total">
<span>Total:</span>
<span>${total.total.toFixed(2)}</span>
</div>
</div>
);
}
function DiscountSection() {
const [code, setCode] = useRecoilState(discountCodeAtom);
const discount = useRecoilValue(discountSelector);
return (
<div className="discount-section">
<h3>Discount Code</h3>
<div className="discount-input">
<input
type="text"
value={code}
onChange={(e) => setCode(e.target.value)}
placeholder="Enter discount code"
/>
</div>
{discount.valid && (
<div className="discount-applied">
<p>Discount applied: {discount.code}</p>
<p>Save ${discount.amount.toFixed(2)}</p>
</div>
)}
{code && !discount.valid && (
<div className="discount-invalid">
<p>Invalid discount code</p>
</div>
)}
</div>
);
}最佳实践
1. Key命名规范
jsx
// 好的命名
const userAtom = atom({ key: 'user', default: null });
const todosAtom = atom({ key: 'todos', default: [] });
const isLoadingAtom = atom({ key: 'isLoading', default: false });
// 使用命名空间
const authUserAtom = atom({ key: 'auth/user', default: null });
const uiThemeAtom = atom({ key: 'ui/theme', default: 'light' });
// Family命名
const userFamily = atomFamily({
key: 'user/byId',
default: id => ({ id, name: '', email: '' })
});2. 状态结构设计
jsx
// 好:原子化设计
const userAtom = atom({ key: 'user', default: null });
const postsAtom = atom({ key: 'posts', default: [] });
const uiAtom = atom({ key: 'ui', default: { theme: 'light' } });
// 避免:过大的单一atom
const appStateAtom = atom({
key: 'appState',
default: {
user: null,
posts: [],
ui: { theme: 'light' },
// ... 大量状态
}
});3. 异步处理
jsx
// 使用Suspense处理异步selector
const asyncDataSelector = selector({
key: 'asyncData',
get: async ({get}) => {
const userId = get(currentUserIdAtom);
const response = await fetch(`/api/users/${userId}/data`);
return response.json();
}
});
function AsyncComponent() {
return (
<Suspense fallback={<div>Loading...</div>}>
<AsyncDataDisplay />
</Suspense>
);
}
function AsyncDataDisplay() {
const data = useRecoilValue(asyncDataSelector);
return <div>{JSON.stringify(data)}</div>;
}4. 性能优化
jsx
// 使用React.memo避免不必要渲染
const OptimizedComponent = React.memo(() => {
const data = useRecoilValue(specificDataAtom);
return <div>{data}</div>;
});
// 合理使用selector
const expensiveSelector = selector({
key: 'expensive',
get: ({get}) => {
const data = get(dataAtom);
// 复杂计算只在data变化时执行
return expensiveCalculation(data);
}
});总结
Recoil提供了强大的原子化状态管理方案:
- 原子化设计:将状态拆分为独立的atoms
- 依赖追踪:自动优化组件重渲染
- 异步支持:原生支持异步状态和Suspense
- 强大工具:Effects、Family、Callback等
- 并发模式:完美支持React 18+特性
Recoil特别适合需要复杂状态依赖和异步处理的大型应用。