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