异步数据流 #

Recoil 的异步支持 #

Recoil 原生支持异步操作,无需额外中间件。主要通过以下方式处理异步:

  • 异步 Selector
  • Atom 的异步默认值
  • Atom Effects
  • useRecoilValueLoadable

异步 Selector #

基本用法 #

jsx
import { selector } from 'recoil';

const userQueryState = selector({
  key: 'userQuery',
  get: async ({ get }) => {
    const userId = get(userIdState);
    const response = await fetch(`/api/users/${userId}`);
    return response.json();
  },
});

错误处理 #

jsx
const userQueryState = selector({
  key: 'userQuery',
  get: async ({ get }) => {
    const userId = get(userIdState);
    
    const response = await fetch(`/api/users/${userId}`);
    
    if (!response.ok) {
      if (response.status === 404) {
        throw new Error('User not found');
      }
      throw new Error(`Failed to fetch user: ${response.status}`);
    }
    
    return response.json();
  },
});

依赖其他状态 #

jsx
const userIdState = atom({
  key: 'userId',
  default: 1,
});

const userQueryState = selector({
  key: 'userQuery',
  get: async ({ get }) => {
    const userId = get(userIdState);
    const response = await fetch(`/api/users/${userId}`);
    return response.json();
  },
});

userIdState 变化时,userQueryState 会自动重新获取数据。

数据获取模式 #

1. 简单查询 #

jsx
const dataQueryState = selector({
  key: 'dataQuery',
  get: async () => {
    const response = await fetch('/api/data');
    return response.json();
  },
});

2. 参数化查询 #

jsx
import { selectorFamily } from 'recoil';

const userQueryState = selectorFamily({
  key: 'userQuery',
  get: (userId) => async () => {
    const response = await fetch(`/api/users/${userId}`);
    return response.json();
  },
});

function UserProfile({ userId }) {
  const user = useRecoilValue(userQueryState(userId));
  return <div>{user.name}</div>;
}

3. 分页查询 #

jsx
const pageState = atom({
  key: 'page',
  default: 1,
});

const pageSizeState = atom({
  key: 'pageSize',
  default: 10,
});

const paginatedQueryState = selector({
  key: 'paginatedQuery',
  get: async ({ get }) => {
    const page = get(pageState);
    const pageSize = get(pageSizeState);
    
    const response = await fetch(`/api/data?page=${page}&size=${pageSize}`);
    return response.json();
  },
});

4. 搜索查询 #

jsx
const searchQueryState = atom({
  key: 'searchQuery',
  default: '',
});

const searchResultsState = selector({
  key: 'searchResults',
  get: async ({ get }) => {
    const query = get(searchQueryState);
    
    if (!query.trim()) {
      return [];
    }
    
    const response = await fetch(`/api/search?q=${encodeURIComponent(query)}`);
    return response.json();
  },
});

5. 防抖搜索 #

jsx
const searchQueryState = atom({
  key: 'searchQuery',
  default: '',
});

const debouncedSearchQueryState = selector({
  key: 'debouncedSearchQuery',
  get: async ({ get }) => {
    const query = get(searchQueryState);
    
    await new Promise(resolve => setTimeout(resolve, 300));
    
    return query;
  },
});

const searchResultsState = selector({
  key: 'searchResults',
  get: async ({ get }) => {
    const query = get(debouncedSearchQueryState);
    
    if (!query.trim()) {
      return [];
    }
    
    const response = await fetch(`/api/search?q=${encodeURIComponent(query)}`);
    return response.json();
  },
});

使用异步数据 #

方式一:Suspense #

jsx
import { Suspense } from 'react';
import { useRecoilValue } from 'recoil';

function UserProfile() {
  const user = useRecoilValue(userQueryState);
  
  return (
    <div>
      <h2>{user.name}</h2>
      <p>{user.email}</p>
    </div>
  );
}

function App() {
  return (
    <Suspense fallback={<div>Loading...</div>}>
      <UserProfile />
    </Suspense>
  );
}

方式二:useRecoilValueLoadable #

jsx
import { useRecoilValueLoadable } from 'recoil';

function UserProfile() {
  const userLoadable = useRecoilValueLoadable(userQueryState);
  
  switch (userLoadable.state) {
    case 'loading':
      return <div>Loading...</div>;
    case 'hasError':
      return <div>Error: {userLoadable.contents.message}</div>;
    case 'hasValue':
      return (
        <div>
          <h2>{userLoadable.contents.name}</h2>
          <p>{userLoadable.contents.email}</p>
        </div>
      );
  }
}

数据刷新 #

使用 refresh #

jsx
import { useRecoilRefresher_UNSTABLE } from 'recoil';

function UserProfile() {
  const user = useRecoilValue(userQueryState);
  const refresh = useRecoilRefresher_UNSTABLE(userQueryState);
  
  return (
    <div>
      <h2>{user.name}</h2>
      <button onClick={refresh}>Refresh</button>
    </div>
  );
}

使用 useRecoilCallback #

jsx
import { useRecoilCallback } from 'recoil';

function RefreshButton() {
  const refresh = useRecoilCallback(({ refresh }) => () => {
    refresh(userQueryState);
  });
  
  return <button onClick={refresh}>Refresh Data</button>;
}

数据预取 #

jsx
function prefetchUser(userId) {
  return useRecoilCallback(({ snapshot }) => async () => {
    snapshot.getPromise(userQueryState(userId));
  });
}

function UserListItem({ userId }) {
  const prefetch = prefetchUser(userId);
  
  return (
    <div onMouseEnter={prefetch}>
      User {userId}
    </div>
  );
}

并行数据获取 #

jsx
const userQueryState = selectorFamily({
  key: 'userQuery',
  get: (id) => async () => {
    const response = await fetch(`/api/users/${id}`);
    return response.json();
  },
});

const allUsersQueryState = selector({
  key: 'allUsersQuery',
  get: async ({ get }) => {
    const userIds = [1, 2, 3, 4, 5];
    
    const users = await Promise.all(
      userIds.map(id => get(userQueryState(id)))
    );
    
    return users;
  },
});

依赖链 #

jsx
const userIdState = atom({
  key: 'userId',
  default: 1,
});

const userQueryState = selector({
  key: 'userQuery',
  get: async ({ get }) => {
    const userId = get(userIdState);
    const response = await fetch(`/api/users/${userId}`);
    return response.json();
  },
});

const userPostsQueryState = selector({
  key: 'userPostsQuery',
  get: async ({ get }) => {
    const user = get(userQueryState);
    const response = await fetch(`/api/users/${user.id}/posts`);
    return response.json();
  },
});

缓存策略 #

默认缓存 #

jsx
const queryState = selector({
  key: 'query',
  get: async () => {
    const response = await fetch('/api/data');
    return response.json();
  },
});

LRU 缓存 #

jsx
const queryState = selector({
  key: 'query',
  get: async () => {
    const response = await fetch('/api/data');
    return response.json();
  },
  cachePolicy_UNSTABLE: {
    eviction: 'lru',
    maxSize: 10,
  },
});

禁用缓存 #

jsx
const queryState = selector({
  key: 'query',
  get: async () => {
    const response = await fetch('/api/data');
    return response.json();
  },
  cachePolicy_UNSTABLE: {
    eviction: 'most-recent',
  },
});

实战示例:完整的数据获取方案 #

jsx
import { atom, selector, useRecoilValueLoadable, useRecoilState } from 'recoil';

const filterState = atom({
  key: 'filter',
  default: {
    category: 'all',
    sortBy: 'date',
    page: 1,
  },
});

const dataQueryState = selector({
  key: 'dataQuery',
  get: async ({ get }) => {
    const filter = get(filterState);
    
    const params = new URLSearchParams({
      category: filter.category,
      sortBy: filter.sortBy,
      page: filter.page,
    });
    
    const response = await fetch(`/api/data?${params}`);
    
    if (!response.ok) {
      throw new Error(`Failed to fetch: ${response.status}`);
    }
    
    return response.json();
  },
});

function DataList() {
  const [filter, setFilter] = useRecoilState(filterState);
  const dataLoadable = useRecoilValueLoadable(dataQueryState);
  
  const handleCategoryChange = (category) => {
    setFilter(prev => ({ ...prev, category, page: 1 }));
  };
  
  const handlePageChange = (page) => {
    setFilter(prev => ({ ...prev, page }));
  };
  
  return (
    <div>
      <div className="filters">
        <select
          value={filter.category}
          onChange={(e) => handleCategoryChange(e.target.value)}
        >
          <option value="all">All</option>
          <option value="electronics">Electronics</option>
          <option value="clothing">Clothing</option>
        </select>
      </div>
      
      {dataLoadable.state === 'loading' && (
        <div className="loading">Loading...</div>
      )}
      
      {dataLoadable.state === 'hasError' && (
        <div className="error">
          Error: {dataLoadable.contents.message}
        </div>
      )}
      
      {dataLoadable.state === 'hasValue' && (
        <>
          <ul className="data-list">
            {dataLoadable.contents.items.map(item => (
              <li key={item.id}>{item.name}</li>
            ))}
          </ul>
          
          <div className="pagination">
            <button
              disabled={filter.page === 1}
              onClick={() => handlePageChange(filter.page - 1)}
            >
              Previous
            </button>
            <span>Page {filter.page}</span>
            <button
              disabled={dataLoadable.contents.items.length < 10}
              onClick={() => handlePageChange(filter.page + 1)}
            >
              Next
            </button>
          </div>
        </>
      )}
    </div>
  );
}

总结 #

Recoil 异步数据流的核心要点:

特性 说明
异步 Selector 原生支持 async/await
自动依赖追踪 依赖变化自动重新获取
缓存 自动缓存查询结果
加载状态 Loadable 或 Suspense

下一步,让我们学习 状态持久化,了解如何持久化存储状态。

最后更新:2026-03-28