useRecoilValueLoadable #

什么是 useRecoilValueLoadable? #

useRecoilValueLoadable 是一个 Hook,用于处理异步状态的读取。它返回一个 Loadable 对象,包含状态和内容,让你能够处理加载中、成功和错误三种状态。

jsx
const loadable = useRecoilValueLoadable(state);

Loadable 对象 #

Loadable 对象包含以下属性:

jsx
{
  state: 'loading' | 'hasValue' | 'hasError',
  contents: T | Error,
}
状态 说明 contents
loading 正在加载 Promise
hasValue 加载成功 实际值
hasError 加载失败 Error 对象

基本用法 #

处理异步 Selector #

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

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

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

简化处理 #

jsx
function UserProfile() {
  const userLoadable = useRecoilValueLoadable(userQueryState);
  
  if (userLoadable.state === 'loading') {
    return <Spinner />;
  }
  
  if (userLoadable.state === 'hasError') {
    return <ErrorMessage error={userLoadable.contents} />;
  }
  
  return <UserCard user={userLoadable.contents} />;
}

实战示例:用户数据加载 #

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

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}`);
    
    if (!response.ok) {
      throw new Error(`Failed to fetch user: ${response.status}`);
    }
    
    return response.json();
  },
});

function UserProfile() {
  const userLoadable = useRecoilValueLoadable(userQueryState);
  
  switch (userLoadable.state) {
    case 'loading':
      return (
        <div className="loading">
          <Spinner />
          <p>Loading user profile...</p>
        </div>
      );
      
    case 'hasError':
      return (
        <div className="error">
          <p>Failed to load user</p>
          <p>{userLoadable.contents.message}</p>
          <button onClick={() => window.location.reload()}>Retry</button>
        </div>
      );
      
    case 'hasValue':
      const user = userLoadable.contents;
      return (
        <div className="user-profile">
          <img src={user.avatar} alt={user.name} />
          <h2>{user.name}</h2>
          <p>{user.email}</p>
          <p>{user.bio}</p>
        </div>
      );
  }
}

实战示例:列表数据加载 #

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

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

const postsQueryState = selector({
  key: 'postsQuery',
  get: async ({ get }) => {
    const page = get(pageState);
    
    const response = await fetch(`/api/posts?page=${page}&limit=10`);
    return response.json();
  },
});

function PostList() {
  const postsLoadable = useRecoilValueLoadable(postsQueryState);
  const [page, setPage] = useRecoilState(pageState);
  
  switch (postsLoadable.state) {
    case 'loading':
      return (
        <div>
          <Spinner />
          <p>Loading posts...</p>
        </div>
      );
      
    case 'hasError':
      return (
        <div className="error">
          <p>Error: {postsLoadable.contents.message}</p>
        </div>
      );
      
    case 'hasValue':
      const { posts, totalPages } = postsLoadable.contents;
      
      return (
        <div>
          <ul>
            {posts.map(post => (
              <li key={post.id}>
                <h3>{post.title}</h3>
                <p>{post.excerpt}</p>
              </li>
            ))}
          </ul>
          
          <div className="pagination">
            <button
              disabled={page === 1}
              onClick={() => setPage(p => p - 1)}
            >
              Previous
            </button>
            <span>Page {page} of {totalPages}</span>
            <button
              disabled={page === totalPages}
              onClick={() => setPage(p => p + 1)}
            >
              Next
            </button>
          </div>
        </div>
      );
  }
}

实战示例:搜索结果 #

jsx
import { atom, selectorFamily, useRecoilValueLoadable } from 'recoil';

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

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

function SearchResults() {
  const query = useRecoilValue(searchQueryState);
  const resultsLoadable = useRecoilValueLoadable(searchResultsState(query));
  
  if (!query.trim()) {
    return <p>Enter a search query</p>;
  }
  
  switch (resultsLoadable.state) {
    case 'loading':
      return <p>Searching...</p>;
      
    case 'hasError':
      return <p>Search failed: {resultsLoadable.contents.message}</p>;
      
    case 'hasValue':
      const results = resultsLoadable.contents;
      
      if (results.length === 0) {
        return <p>No results found for "{query}"</p>;
      }
      
      return (
        <ul>
          {results.map(result => (
            <li key={result.id}>
              <h4>{result.title}</h4>
              <p>{result.description}</p>
            </li>
          ))}
        </ul>
      );
  }
}

与 Suspense 对比 #

使用 useRecoilValueLoadable #

jsx
function UserProfile() {
  const userLoadable = useRecoilValueLoadable(userQueryState);
  
  if (userLoadable.state === 'loading') {
    return <Spinner />;
  }
  
  if (userLoadable.state === 'hasError') {
    return <Error error={userLoadable.contents} />;
  }
  
  return <UserCard user={userLoadable.contents} />;
}

使用 Suspense #

jsx
function UserProfile() {
  const user = useRecoilValue(userQueryState);
  return <UserCard user={user} />;
}

function App() {
  return (
    <Suspense fallback={<Spinner />}>
      <ErrorBoundary>
        <UserProfile />
      </ErrorBoundary>
    </Suspense>
  );
}

选择建议 #

场景 推荐
组件内处理加载状态 useRecoilValueLoadable
全局加载状态 Suspense
自定义加载 UI useRecoilValueLoadable
错误边界处理 Suspense + ErrorBoundary

自定义 Hook 封装 #

jsx
function useAsyncRecoilValue(state) {
  const loadable = useRecoilValueLoadable(state);
  
  return {
    data: loadable.state === 'hasValue' ? loadable.contents : null,
    loading: loadable.state === 'loading',
    error: loadable.state === 'hasError' ? loadable.contents : null,
  };
}

function UserProfile() {
  const { data: user, loading, error } = useAsyncRecoilValue(userQueryState);
  
  if (loading) return <Spinner />;
  if (error) return <Error error={error} />;
  
  return <UserCard user={user} />;
}

组合多个异步状态 #

jsx
function Dashboard() {
  const userLoadable = useRecoilValueLoadable(userQueryState);
  const statsLoadable = useRecoilValueLoadable(statsQueryState);
  const notificationsLoadable = useRecoilValueLoadable(notificationsQueryState);
  
  const isLoading = 
    userLoadable.state === 'loading' ||
    statsLoadable.state === 'loading' ||
    notificationsLoadable.state === 'loading';
  
  if (isLoading) {
    return <Spinner />;
  }
  
  const hasError = 
    userLoadable.state === 'hasError' ||
    statsLoadable.state === 'hasError' ||
    notificationsLoadable.state === 'hasError';
  
  if (hasError) {
    return <Error />;
  }
  
  return (
    <div>
      <UserCard user={userLoadable.contents} />
      <StatsCard stats={statsLoadable.contents} />
      <NotificationsList notifications={notificationsLoadable.contents} />
    </div>
  );
}

TypeScript 支持 #

tsx
import { selector, useRecoilValueLoadable } from 'recoil';

interface User {
  id: number;
  name: string;
  email: string;
}

const userQueryState = selector<User>({
  key: 'userQuery',
  get: async () => {
    const response = await fetch('/api/user');
    return response.json();
  },
});

function UserProfile() {
  const userLoadable = useRecoilValueLoadable(userQueryState);
  
  if (userLoadable.state === 'hasValue') {
    const user: User = userLoadable.contents;
    return <div>{user.name}</div>;
  }
  
  return null;
}

完整示例:数据表格 #

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

const tableParamsState = atom({
  key: 'tableParams',
  default: {
    page: 1,
    pageSize: 10,
    sortBy: 'id',
    sortOrder: 'asc',
    filter: '',
  },
});

const tableDataState = selector({
  key: 'tableData',
  get: async ({ get }) => {
    const params = get(tableParamsState);
    const query = new URLSearchParams({
      page: params.page,
      pageSize: params.pageSize,
      sortBy: params.sortBy,
      sortOrder: params.sortOrder,
      filter: params.filter,
    });
    
    const response = await fetch(`/api/data?${query}`);
    if (!response.ok) {
      throw new Error('Failed to fetch data');
    }
    
    return response.json();
  },
});

function DataTable() {
  const [params, setParams] = useRecoilState(tableParamsState);
  const dataLoadable = useRecoilValueLoadable(tableDataState);
  
  const handleSort = (column) => {
    setParams(prev => ({
      ...prev,
      sortBy: column,
      sortOrder: prev.sortBy === column && prev.sortOrder === 'asc' ? 'desc' : 'asc',
    }));
  };
  
  const handlePageChange = (newPage) => {
    setParams(prev => ({ ...prev, page: newPage }));
  };
  
  switch (dataLoadable.state) {
    case 'loading':
      return (
        <div className="table-loading">
          <Spinner />
          <p>Loading data...</p>
        </div>
      );
      
    case 'hasError':
      return (
        <div className="table-error">
          <p>Error: {dataLoadable.contents.message}</p>
          <button onClick={() => setParams(p => ({ ...p }))}>Retry</button>
        </div>
      );
      
    case 'hasValue':
      const { data, total, totalPages } = dataLoadable.contents;
      
      return (
        <div className="data-table">
          <table>
            <thead>
              <tr>
                <th onClick={() => handleSort('id')}>ID</th>
                <th onClick={() => handleSort('name')}>Name</th>
                <th onClick={() => handleSort('email')}>Email</th>
              </tr>
            </thead>
            <tbody>
              {data.map(row => (
                <tr key={row.id}>
                  <td>{row.id}</td>
                  <td>{row.name}</td>
                  <td>{row.email}</td>
                </tr>
              ))}
            </tbody>
          </table>
          
          <div className="pagination">
            <button
              disabled={params.page === 1}
              onClick={() => handlePageChange(params.page - 1)}
            >
              Previous
            </button>
            <span>Page {params.page} of {totalPages}</span>
            <button
              disabled={params.page === totalPages}
              onClick={() => handlePageChange(params.page + 1)}
            >
              Next
            </button>
          </div>
        </div>
      );
  }
}

总结 #

useRecoilValueLoadable 的核心要点:

特点 说明
异步处理 处理异步状态的加载
三种状态 loading、hasValue、hasError
灵活控制 组件内处理加载和错误状态
类型安全 支持 TypeScript 类型推断

下一步,让我们学习 异步数据流,深入了解 Recoil 的异步处理能力。

最后更新:2026-03-28