Recoil 教程 - Facebook 开发的 React 状态管理库

项目概述

Recoil 是 Facebook 开发的 React 状态管理库,专为 React 设计,提供了一种基于原子的状态管理方案。

核心概念

  1. **原子 (Atom)**:状态的基本单位,可被订阅和更新
  2. **选择器 (Selector)**:基于原子或其他选择器计算得出的值
  3. RecoilRoot:Recoil 状态的根组件,包裹应用的顶层
  4. useRecoilState:用于读取和更新原子状态的 hook
  5. useRecoilValue:用于只读取原子或选择器值的 hook
  6. useSetRecoilState:用于只更新原子状态的 hook

核心功能

  1. 原子化状态:将状态分解为小的、独立的原子
  2. 派生状态:通过选择器创建基于其他状态的派生状态
  3. 状态 persistence:支持状态持久化
  4. 时间旅行调试:可以回溯和重放状态变化
  5. 并发模式支持:支持 React 18 并发特性
  6. 与 React 深度集成:与 React 的渲染周期和 Suspense 等特性深度集成
  7. 适合复杂状态依赖:处理复杂的状态依赖关系
  8. 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');
  }
});

最佳实践

  1. 合理设计原子:将状态分解为小的、独立的原子
  2. 使用选择器处理派生状态:对于计算值,使用选择器而不是存储在组件状态中
  3. 使用合适的 hook:根据需要选择 useRecoilState、useRecoilValue 或 useSetRecoilState
  4. 处理异步操作:使用异步选择器结合 React Suspense 处理异步操作
  5. 状态持久化:对于需要持久化的状态,使用 recoil-persist 等库
  6. TypeScript 类型定义:为原子和选择器添加类型定义,提高代码可维护性
  7. 避免在渲染函数中创建原子:原子应该在组件外部创建,以确保一致性
  8. 合理使用 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 应用性能问题

解决方案

  • 使用 useRecoilValueuseSetRecoilState 分离读取和写入,减少不必要的重渲染
  • 使用选择器缓存计算结果
  • 对于复杂的计算,考虑使用 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,但风格不同

参考资源

  1. 官方文档https://recoiljs.org/docs/introduction/getting-started
  2. GitHub 仓库https://github.com/facebookexperimental/Recoil
  3. Recoil 示例https://recoiljs.org/docs/introduction/examples
  4. React Suspense 文档https://react.dev/reference/react/Suspense
  5. React 18 并发特性https://react.dev/blog/2022/03/29/react-v18
  6. recoil-persisthttps://github.com/polemius/recoil-persist
« 上一篇 Jotai 教程 - 基于原子的状态管理库 下一篇 » Lodash 教程 - 现代化的 JavaScript 实用工具库