预取策略与 Suspense
TanStack Query 的预取策略:在用户实际需要数据之前提前获取,消除 loading 状态,提升感知性能。
#type / howto
#status / evergreen
#tech / dev / frontend
#resource / react
[!info] related notes
- 前置: TanStack Query 服务端状态
- 所属 MOC: TanStack Query 知识地图
- 关联: Suspense 集成
- 关联: useInfiniteQuery 分页与无限滚动
- 关联: placeholderData 与 keepPreviousData
预取策略与 Suspense
核心问题
用户点击列表项查看详情时,需要先加载数据,出现 loading 状态。如果能提前预取数据,用户点击时直接从缓存读取,体验会好很多。
预取方式
queryClient.prefetchQuery
const queryClient = useQueryClient();
// 预取用户详情
await queryClient.prefetchQuery({
queryKey: ['user', userId],
queryFn: () => fetchUser(userId),
staleTime: 60_000, // 预取的数据 1 分钟内不重新请求
});
prefetchQuery 的行为:
- 如果缓存中已有新鲜数据 → 什么都不做
- 如果缓存中没有或已 stale → 发请求并写入缓存
- 返回 Promise,可以 await
Hover 预取
function UserList({ users }) {
const queryClient = useQueryClient();
return (
<ul>
{users.map(user => (
<li
key={user.id}
// 鼠标悬停时预取详情
onMouseEnter={() => {
queryClient.prefetchQuery({
queryKey: ['user', user.id],
queryFn: () => fetchUser(user.id),
staleTime: 60_000,
});
}}
>
<Link to={`/users/${user.id}`}>{user.name}</Link>
</li>
))}
</ul>
);
}
路由级预取(React Router)
// 在路由配置中预取
const router = createBrowserRouter([
{
path: '/users/:id',
element: <UserProfile />,
loader: async ({ params }) => {
// 路由 loader 中预取
await queryClient.prefetchQuery({
queryKey: ['user', params.id],
queryFn: () => fetchUser(params.id),
});
return null;
},
},
]);
分页预取
function PaginatedList() {
const [page, setPage] = useState(1);
const queryClient = useQueryClient();
const { data, isPlaceholderData } = useQuery({
queryKey: ['items', page],
queryFn: () => fetchItems(page),
placeholderData: keepPreviousData,
});
// 预取下一页
useEffect(() => {
if (!isPlaceholderData) {
queryClient.prefetchQuery({
queryKey: ['items', page + 1],
queryFn: () => fetchItems(page + 1),
});
}
}, [page, isPlaceholderData, queryClient]);
// ...
}
无限滚动预取
function InfiniteList() {
const { data, fetchNextPage, hasNextPage } = useInfiniteQuery({
queryKey: ['posts'],
queryFn: ({ pageParam }) => fetchPosts({ cursor: pageParam }),
initialPageParam: undefined,
getNextPageParam: (lastPage) => lastPage.nextCursor,
});
// 当快到底部时预取下一页
const lastPostIndex = data?.pages.flatMap(p => p.posts).length ?? 0;
useEffect(() => {
if (hasNextPage && lastPostIndex > 0) {
// 最后 3 个元素内就开始预取
queryClient.prefetchInfiniteQuery({
queryKey: ['posts'],
initialPageParam: undefined,
queryFn: ({ pageParam }) => fetchPosts({ cursor: pageParam }),
});
}
}, [lastPostIndex, hasNextPage]);
// ...
}
预取配置
staleTime 控制预取有效期
// 预取时设置较长的 staleTime
queryClient.prefetchQuery({
queryKey: ['user', id],
queryFn: () => fetchUser(id),
staleTime: 5 * 60 * 1000, // 5 分钟内不重新请求
});
全局默认 staleTime
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 60_000, // 全局默认 1 分钟
},
},
});
与 React Router Data Router 集成
import { createBrowserRouter, RouterProvider } from 'react-router-dom';
const queryClient = new QueryClient();
const router = createBrowserRouter([
{
path: '/',
element: <Layout />,
children: [
{
index: true,
element: <Home />,
},
{
path: 'users',
element: <UserList />,
// loader 自动在路由匹配时执行
loader: async () => {
await queryClient.ensureQueryData({
queryKey: ['users'],
queryFn: fetchUsers,
});
return null;
},
},
{
path: 'users/:id',
element: <UserProfile />,
loader: async ({ params }) => {
await queryClient.ensureQueryData({
queryKey: ['user', params.id],
queryFn: () => fetchUser(params.id!),
});
return null;
},
},
],
},
]);
function App() {
return (
<QueryClientProvider client={queryClient}>
<RouterProvider router={router} />
</QueryClientProvider>
);
}
ensureQueryData vs prefetchQuery
// prefetchQuery:只预取,不等待
queryClient.prefetchQuery({ queryKey: ['user', id], queryFn: () => fetchUser(id) });
// ensureQueryData:确保有数据,返回数据
// 如果缓存新鲜 → 直接返回缓存
// 如果缓存 stale → 等待请求完成后返回
const data = await queryClient.ensureQueryData({
queryKey: ['user', id],
queryFn: () => fetchUser(id),
});
| 方法 | 等待? | 用途 |
|---|---|---|
prefetchQuery | 后台预取,不等待 | hover 预取、分页预取 |
ensureQueryData | 等待数据就绪 | 路由 loader、SSR |
常见错误
预取但不消费
// ❌ 预取了但组件没用这个 queryKey
queryClient.prefetchQuery({ queryKey: ['user', id], queryFn: ... });
// 组件里用的是不同的 key
useQuery({ queryKey: ['userDetail', id], ... }); // 不会命中缓存
// ✅ key 必须一致
queryClient.prefetchQuery({ queryKey: ['user', id], queryFn: ... });
useQuery({ queryKey: ['user', id], ... });
预取时机太晚
// ❌ 点击时才预取,和不预取一样
onClick={() => {
queryClient.prefetchQuery({ ... });
navigate(`/users/${id}`);
}}
// ✅ hover 时预取
onMouseEnter={() => {
queryClient.prefetchQuery({ ... });
}}
预取但 staleTime 为 0
// ❌ 预取后立即 stale,用到时又重新请求
queryClient.prefetchQuery({
queryKey: ['user', id],
queryFn: () => fetchUser(id),
staleTime: 0, // 默认值
});
// ✅ 设置合理的 staleTime
queryClient.prefetchQuery({
queryKey: ['user', id],
queryFn: () => fetchUser(id),
staleTime: 60_000,
});