如何在 React Router Loaders 中使用 React Query(预取和缓存数据)
Source: Dev.to
问题
当你导航到一个页面时,通常会有数据获取的延迟。用户会看到加载指示器,内容在请求完成后才出现。体验并不理想。
如果数据在页面加载时已经准备好怎么办?
将 React Query 与 React Router loaders 结合即可实现这一点。
简明说明
-
Loader 在组件挂载 之前 运行(React Router 在导航时调用它)。
-
在 Loader 中,我们询问 React Query:“你已经缓存了这些数据吗?”
- 是 → 立即使用。无需网络请求。
- 否 → 立即获取,等待完成,然后缓存。
当组件最终挂载时,它会使用相同的查询调用 useQuery。由于数据已经缓存,组件会 立即 渲染——没有加载状态。
关键方法是 queryClient.ensureQueryData(queryOptions)。可以把它理解为:“确保该数据存在——从缓存获取或去获取。”
步骤示例:一个简单的宝可梦页面
1. 设置查询
// pages/Pokemon.jsx
import { useQuery } from '@tanstack/react-query';
import { useLoaderData } from 'react-router-dom';
import axios from 'axios';
// A function that returns the query config (key + fetch function).
// We reuse this in BOTH the loader and the component.
const pokemonQuery = (name) => ({
queryKey: ['pokemon', name],
queryFn: async () => {
const response = await axios.get(
`https://pokeapi.co/api/v2/pokemon/${name}`
);
return response.data;
},
});
2. 创建 Loader
// The loader receives queryClient from the router setup (see step 4).
// It runs BEFORE the component mounts.
export const loader = (queryClient) => {
return async ({ params }) => {
const { name } = params;
// ensureQueryData checks the cache first:
// - cached? → returns it instantly
// - not cached? → fetches, caches, and returns it
await queryClient.ensureQueryData(pokemonQuery(name));
// We only return the param — the actual data lives in React Query's cache
return { name };
};
};
3. 构建组件
const Pokemon = () => {
// Get the param that the loader returned
const { name } = useLoaderData();
// useQuery uses the SAME query config as the loader.
// Since ensureQueryData already cached it, this renders instantly.
const { data: pokemon } = useQuery(pokemonQuery(name));
return (
<>
<h2>{pokemon.name}</h2>
<p>Height: {pokemon.height}</p>
<p>Weight: {pokemon.weight}</p>
</>
);
};
export default Pokemon;
4. 在路由中连接
// App.jsx
import {
createBrowserRouter,
RouterProvider,
} from 'react-router-dom';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import Pokemon, { loader as pokemonLoader } from './pages/Pokemon';
const queryClient = new QueryClient();
const router = createBrowserRouter([
{
path: '/pokemon/:name',
element: <Pokemon />,
// Pass queryClient into the loader
loader: pokemonLoader(queryClient),
},
]);
function App() {
return (
<QueryClientProvider client={queryClient}>
<RouterProvider router={router} />
</QueryClientProvider>
);
}
export default App;
整体流程
User clicks link to /pokemon/pikachu
│
▼
Router calls loader BEFORE mounting the component
│
▼
loader calls: await queryClient.ensureQueryData(pokemonQuery("pikachu"))
│
├── Cache HIT? → Returns cached data instantly (no fetch)
└── Cache MISS? → Fetches from API, caches result, then returns
│
▼
Component mounts → useQuery(pokemonQuery("pikachu"))
│
▼
Data is already in cache → Renders IMMEDIATELY (no loading spinner)
为什么不只使用 useQuery?
| 方法 | 会发生什么 |
|---|---|
useQuery only | 组件挂载 → 开始获取 → 显示加载中 → 显示数据 |
ensureQueryData in loader + useQuery | 在挂载前获取数据 → 组件立即渲染并显示数据 |
使用 loader 方法可以提供更流畅、更快的用户体验,尤其是在页面导航时。
关键要点
ensureQueryData= “如果已缓存,则使用缓存;如果未缓存,则获取并缓存。”- 创建一个 共享查询配置函数(例如
pokemonQuery),并在 loader 和组件中都使用它。 - loader 预先填充缓存,使组件中的
useQuery能立即找到数据。 - 从 loader 只返回 params —— 不返回数据本身。数据存放在 React Query 的缓存中。
你的页面现在在导航时会瞬间加载,React Query 会免费处理缓存、后台重新获取以及陈旧数据。