Data fetching libraries
The useFetch hook from the custom hooks lesson solves a simple task. A real app needs more: do not load the same post twice, refresh data when the user returns to the tab, retry a failed request. Writing all of that by hand takes long, so data fetching libraries are used. They keep responses in a cache — memory that returns data immediately, without a new request.
Popular libraries
| Library | What sets it apart |
|---|---|
| TanStack Query | The most widespread, works with any way of fetching |
| SWR | A similar idea with a smaller API |
| RTK Query | Built into Redux Toolkit |
Below is an example with TanStack Query (version 5).
Installing and connecting
npm install @tanstack/react-queryThe app is wrapped in a provider with a query client — in src/main.jsx:
// src/main.jsx
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
// imports of StrictMode, createRoot, BrowserRouter and App stay as before
const queryClient = new QueryClient();
createRoot(document.getElementById('root')).render(
<StrictMode>
<QueryClientProvider client={queryClient}>
<BrowserRouter>
<App />
</BrowserRouter>
</QueryClientProvider>
</StrictMode>,
);QueryClient holds the cache of all the app's queries.
A query with useQuery
Let us rewrite the post list — src/components/PostList.jsx:
// src/components/PostList.jsx
import { useQuery } from '@tanstack/react-query';
import { Link } from 'react-router';
async function fetchPosts() {
const response = await fetch('https://jsonplaceholder.typicode.com/posts');
if (!response.ok) throw new Error(`Error ${response.status}`);
return response.json();
}
export default function PostList() {
const { data, isPending, error } = useQuery({
queryKey: ['posts'],
queryFn: fetchPosts,
});
if (isPending) return <p>Loading…</p>;
if (error) return <p>Error: {error.message}</p>;
return (
<ul>
{data.map((post) => (
<li key={post.id}>
<Link to={`/posts/${post.id}`}>{post.title}</Link>
</li>
))}
</ul>
);
}queryFn is a function that loads the data and throws an error on failure. queryKey is the key under which the response is stored in the cache. Open a post and go back to the home page: the list appears at once, without "Loading…" — it comes from the cache and is quietly refreshed in the background.
A key with a parameter
If a query depends on a parameter, the parameter goes into the key:
// src/pages/PostPage.jsx
const { id } = useParams();
const { data: post, isPending } = useQuery({
queryKey: ['posts', id],
queryFn: () =>
fetch(`https://jsonplaceholder.typicode.com/posts/${id}`).then((r) => r.json()),
});Each post gets its own cache entry: ['posts', '1'], ['posts', '2']. Going back to a post you already opened shows it instantly.
What else the library does
| Feature | What it is called |
|---|---|
| Sending data to the server | useMutation |
| Refreshing the cache after a change | queryClient.invalidateQueries |
| Retrying a failed request | On by default, 3 attempts |
Common mistake
If a query parameter is missing from the key, the library treats all requests as one and shows the wrong data:
// Wrong: every post has the same key.
useQuery({ queryKey: ['post'], queryFn: () => fetchPost(id) });
// Correct:
useQuery({ queryKey: ['post', id], queryFn: () => fetchPost(id) });Everything queryFn depends on must be in queryKey.