Recoil 教程 - Facebook 开发的 React 状态管理库
项目概述
Recoil 是 Facebook 开发的 React 状态管理库,专为 React 设计,提供了一种基于原子的状态管理方案。
- 项目链接:https://github.com/facebookexperimental/Recoil
- 官方网站:https://recoiljs.org/
- GitHub Stars:19k+
核心概念
- **原子 (Atom)**:状态的基本单位,可被订阅和更新
- **选择器 (Selector)**:基于原子或其他选择器计算得出的值
- RecoilRoot:Recoil 状态的根组件,包裹应用的顶层
- useRecoilState:用于读取和更新原子状态的 hook
- useRecoilValue:用于只读取原子或选择器值的 hook
- useSetRecoilState:用于只更新原子状态的 hook
核心功能
- 原子化状态:将状态分解为小的、独立的原子
- 派生状态:通过选择器创建基于其他状态的派生状态
- 状态 persistence:支持状态持久化
- 时间旅行调试:可以回溯和重放状态变化
- 并发模式支持:支持 React 18 并发特性
- 与 React 深度集成:与 React 的渲染周期和 Suspense 等特性深度集成
- 适合复杂状态依赖:处理复杂的状态依赖关系
- TypeScript 支持:良好的 TypeScript 类型定义
安装与设置
基本安装
# 安装 Recoil
npm install recoil基本设置
在应用的顶层包裹 RecoilRoot 组件:
// src/index.js
import React from 'react';
import ReactDOM from 'react-dom';
import { RecoilRoot } from 'recoil';
import App from './App';
ReactDOM.render(
<RecoilRoot>
<App />
</RecoilRoot>,
document.getElementById('root')
);基本使用
创建原子
// src/atoms.js
import { atom } from 'recoil';
// 创建一个基本原子
export const countAtom = atom({
key: 'countAtom', // 唯一标识符
default: 0 // 默认值
});
// 创建一个带有默认值的原子
export const userAtom = atom({
key: 'userAtom',
default: null
});在组件中使用
// src/App.js
import React from 'react';
import { useRecoilState, useRecoilValue, useSetRecoilState } from 'recoil';
import { countAtom, userAtom } from './atoms';
function App() {
// 使用 useRecoilState 读取和更新原子
const [count, setCount] = useRecoilState(countAtom);
// 使用 useRecoilValue 只读取原子
const user = useRecoilValue(userAtom);
// 使用 useSetRecoilState 只更新原子
const setUser = useSetRecoilState(userAtom);
return (
<div>
<h1>Count: {count}</h1>
<button onClick={() => setCount(count + 1)}>Increment</button>
<button onClick={() => setCount(count - 1)}>Decrement</button>
<button onClick={() => setCount(0)}>Reset</button>
<div>
<h2>User: {user ? user.name : 'Not logged in'}</h2>
<button onClick={() => setUser({ name: 'John Doe' })}>Login</button>
<button onClick={() => setUser(null)}>Logout</button>
</div>
</div>
);
}
export default App;创建选择器
// src/selectors.js
import { selector } from 'recoil';
import { countAtom, userAtom } from './atoms';
// 创建一个基于原子的选择器
export const doubleCountAtom = selector({
key: 'doubleCountAtom',
get: ({ get }) => {
return get(countAtom) * 2;
}
});
// 创建一个可写入的选择器
export const countWithResetAtom = selector({
key: 'countWithResetAtom',
get: ({ get }) => {
return get(countAtom);
},
set: ({ set }, newValue) => {
if (newValue === 'reset') {
set(countAtom, 0);
} else {
set(countAtom, newValue);
}
}
});
// 创建一个基于多个原子的选择器
export const greetingAtom = selector({
key: 'greetingAtom',
get: ({ get }) => {
const user = get(userAtom);
const count = get(countAtom);
if (user) {
return `Hello, ${user.name}! You've clicked ${count} times.`;
} else {
return `Hello! You've clicked ${count} times.`;
}
}
});使用选择器
// src/App.js
import React from 'react';
import { useRecoilState, useRecoilValue } from 'recoil';
import { countAtom, userAtom } from './atoms';
import { doubleCountAtom, greetingAtom } from './selectors';
function App() {
const [count, setCount] = useRecoilState(countAtom);
const [user, setUser] = useRecoilState(userAtom);
const doubleCount = useRecoilValue(doubleCountAtom);
const greeting = useRecoilValue(greetingAtom);
return (
<div>
<h1>Count: {count}</h1>
<h2>Double Count: {doubleCount}</h2>
<h3>{greeting}</h3>
<button onClick={() => setCount(count + 1)}>Increment</button>
<button onClick={() => setCount(count - 1)}>Decrement</button>
<button onClick={() => setCount(0)}>Reset</button>
<div>
<h2>User: {user ? user.name : 'Not logged in'}</h2>
<button onClick={() => setUser({ name: 'John Doe' })}>Login</button>
<button onClick={() => setUser(null)}>Logout</button>
</div>
</div>
);
}
export default App;高级特性
异步操作
Recoil 支持异步选择器,结合 React Suspense 使用:
// src/atoms.js
import { atom, selector } from 'recoil';
// 异步选择器
export const usersSelector = selector({
key: 'usersSelector',
get: async () => {
const response = await fetch('https://jsonplaceholder.typicode.com/users');
const data = await response.json();
return data;
}
});
// 加载状态原子
export const isLoadingAtom = atom({
key: 'isLoadingAtom',
default: false
});// src/UserList.js
import React, { Suspense } from 'react';
import { useRecoilValue } from 'recoil';
import { usersSelector } from './atoms';
function UserListContent() {
const users = useRecoilValue(usersSelector);
return (
<ul>
{users.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
function UserList() {
return (
<Suspense fallback={<div>Loading users...</div>}>
<UserListContent />
</Suspense>
);
}
export default UserList;状态持久化
使用 recoil-persist 库实现状态持久化:
# 安装 recoil-persist
npm install recoil-persist// src/index.js
import React from 'react';
import ReactDOM from 'react-dom';
import { RecoilRoot } from 'recoil';
import { recoilPersist } from 'recoil-persist';
import App from './App';
// 配置持久化
const { persistAtom } = recoilPersist({
key: 'recoil-persist', // 存储的键名
storage: localStorage // 使用 localStorage
});
ReactDOM.render(
<RecoilRoot
atomEffects={[persistAtom]}
>
<App />
</RecoilRoot>,
document.getElementById('root')
);复杂状态依赖
Recoil 适合处理复杂的状态依赖关系:
// src/atoms.js
import { atom, selector } from 'recoil';
// 产品列表
export const productsAtom = atom({
key: 'productsAtom',
default: [
{ id: 1, name: 'Product 1', price: 10, category: 'electronics' },
{ id: 2, name: 'Product 2', price: 20, category: 'clothing' },
{ id: 3, name: 'Product 3', price: 30, category: 'electronics' },
{ id: 4, name: 'Product 4', price: 40, category: 'clothing' }
]
});
// 选中的分类
export const selectedCategoryAtom = atom({
key: 'selectedCategoryAtom',
default: 'all'
});
// 过滤后的产品
export const filteredProductsSelector = selector({
key: 'filteredProductsSelector',
get: ({ get }) => {
const products = get(productsAtom);
const category = get(selectedCategoryAtom);
if (category === 'all') {
return products;
} else {
return products.filter(product => product.category === category);
}
}
});
// 产品总数
export const productCountSelector = selector({
key: 'productCountSelector',
get: ({ get }) => {
return get(filteredProductsSelector).length;
}
});
// 产品总价
export const totalPriceSelector = selector({
key: 'totalPriceSelector',
get: ({ get }) => {
return get(filteredProductsSelector)
.reduce((total, product) => total + product.price, 0);
}
});实用场景
表单状态管理
// src/atoms.js
import { atom, selector } from 'recoil';
// 表单字段
export const nameAtom = atom({
key: 'nameAtom',
default: ''
});
export const emailAtom = atom({
key: 'emailAtom',
default: ''
});
export const passwordAtom = atom({
key: 'passwordAtom',
default: ''
});
// 表单验证错误
export const errorsSelector = selector({
key: 'errorsSelector',
get: ({ get }) => {
const errors = {};
const name = get(nameAtom);
const email = get(emailAtom);
const password = get(passwordAtom);
if (!name) {
errors.name = 'Name is required';
}
if (!email) {
errors.email = 'Email is required';
} else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
errors.email = 'Email is invalid';
}
if (!password) {
errors.password = 'Password is required';
} else if (password.length < 6) {
errors.password = 'Password must be at least 6 characters';
}
return errors;
}
});
// 表单是否有效
export const isValidSelector = selector({
key: 'isValidSelector',
get: ({ get }) => {
return Object.keys(get(errorsSelector)).length === 0;
}
});
// 表单数据
export const formDataSelector = selector({
key: 'formDataSelector',
get: ({ get }) => {
return {
name: get(nameAtom),
email: get(emailAtom),
password: get(passwordAtom)
};
}
});全局状态管理
// src/atoms.js
import { atom, selector } from 'recoil';
// 用户状态
export const userAtom = atom({
key: 'userAtom',
default: null
});
// 认证状态
export const isAuthenticatedSelector = selector({
key: 'isAuthenticatedSelector',
get: ({ get }) => {
return get(userAtom) !== null;
}
});
// 权限状态
export const permissionsSelector = selector({
key: 'permissionsSelector',
get: ({ get }) => {
const user = get(userAtom);
if (user) {
return user.permissions || [];
}
return [];
}
});
// 是否有管理员权限
export const isAdminSelector = selector({
key: 'isAdminSelector',
get: ({ get }) => {
return get(permissionsSelector).includes('admin');
}
});最佳实践
- 合理设计原子:将状态分解为小的、独立的原子
- 使用选择器处理派生状态:对于计算值,使用选择器而不是存储在组件状态中
- 使用合适的 hook:根据需要选择 useRecoilState、useRecoilValue 或 useSetRecoilState
- 处理异步操作:使用异步选择器结合 React Suspense 处理异步操作
- 状态持久化:对于需要持久化的状态,使用 recoil-persist 等库
- TypeScript 类型定义:为原子和选择器添加类型定义,提高代码可维护性
- 避免在渲染函数中创建原子:原子应该在组件外部创建,以确保一致性
- 合理使用 key:为原子和选择器提供唯一的 key,避免冲突
常见问题与解决方案
1. 状态更新后组件不重新渲染
问题:更新原子后,组件没有重新渲染
解决方案:
- 确保组件在
RecoilRoot的范围内 - 检查是否使用了正确的 hook(useRecoilState、useRecoilValue 等)
- 确保更新的是原子的状态,而不是修改原子的默认值
2. 异步操作错误处理
问题:异步选择器抛出错误时如何处理
解决方案:
- 使用 React 的 Error Boundary 捕获错误
- 在异步选择器中添加错误处理逻辑
import React, { ErrorBoundary } from 'react';
import { useRecoilValue } from 'recoil';
import { usersSelector } from './atoms';
function UserList() {
const users = useRecoilValue(usersSelector);
return (
<ul>
{users.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
function UserListWithErrorBoundary() {
return (
<ErrorBoundary fallback={<div>Error loading users</div>}>
<UserList />
</ErrorBoundary>
);
}3. 性能优化
问题:Recoil 应用性能问题
解决方案:
- 使用
useRecoilValue和useSetRecoilState分离读取和写入,减少不必要的重渲染 - 使用选择器缓存计算结果
- 对于复杂的计算,考虑使用 memoization
- 合理设计原子的粒度,避免过于细粒度或过于粗粒度
与其他状态管理库的比较
Recoil vs Redux
- API 复杂度:Recoil API 更简洁,与 React 更集成
- 性能:Recoil 提供细粒度更新,性能更好
- 灵活性:Recoil 更灵活,支持局部状态管理
- 生态系统:Redux 生态系统更丰富,有更多的中间件和工具
- 调试工具:两者都有良好的调试工具
Recoil vs Jotai
- 状态模型:两者都使用原子模型,但 API 和实现细节不同
- Provider:Recoil 需要 RecoilRoot Provider,Jotai 不需要
- 生态系统:Recoil 由 Facebook 维护,生态系统正在发展中
- 并发模式支持:两者都支持 React 18 并发特性
- API 风格:两者都提供简洁的 API,但风格不同
Recoil vs Zustand
- 状态模型:Recoil 使用原子模型,Zustand 使用 store 模型
- Provider:Recoil 需要 RecoilRoot Provider,Zustand 不需要
- 粒度:Recoil 提供更细粒度的状态管理
- 生态系统:Zustand 生态系统更成熟
- API 风格:两者都提供简洁的 API,但风格不同
参考资源
- 官方文档:https://recoiljs.org/docs/introduction/getting-started
- GitHub 仓库:https://github.com/facebookexperimental/Recoil
- Recoil 示例:https://recoiljs.org/docs/introduction/examples
- React Suspense 文档:https://react.dev/reference/react/Suspense
- React 18 并发特性:https://react.dev/blog/2022/03/29/react-v18
- recoil-persist:https://github.com/polemius/recoil-persist