Navigate
Search topics, hooks, and fundamentals
Fetching data manually is painful. There has to be a better way.
Three useState hooks, a useEffect, error handling, and no caching. Every time you navigate back, it re-fetches from scratch.
One hook that handles loading, errors, caching, and background refetching. The same data, but smarter.
One hook. Auto caching, loading states, error handling, and background refetching β all built in.
const { data, isPending, isError, error, refetch } = useQuery({
queryKey: ["post", 1],
queryFn: () =>
fetch("https://jsonplaceholder.typicode.com/posts/1")
.then(res => res.json()),
});
// That's it! Loading, error, caching, refetching β all handled.
// Navigate away and come back? Data is cached. Instant.useQuery replaces the fetch-loading-error dance with one declarative hook call.
useQuery does more than basic fetching β query keys, caching, and automatic polling. Try these examples.
const { data: posts, isPending } = useQuery({
queryKey: ["posts"],
queryFn: () =>
fetch("https://jsonplaceholder.typicode.com/posts?_limit=5")
.then(res => res.json()),
});
// Click a post β new query with different key
const { data: detail } = useQuery({
queryKey: ["post", selectedId],
queryFn: () => fetch(`/posts/${selectedId}`).then(r => r.json()),
enabled: !!selectedId, // only fetch when ID is set
});Select a user β previously fetched users load instantly from cache!
Query key: ["user", 1]
const [userId, setUserId] = useState(1);
const { data: user, isPending, isFetching } = useQuery({
queryKey: ["user", userId], // Different key per user!
queryFn: () =>
fetch(`https://jsonplaceholder.typicode.com/users/${userId}`)
.then(res => res.json()),
});
// Switch to user 2 β new query key β fetch
// Switch BACK to user 1 β cached! Instant.Toggle polling on β the data refreshes automatically every 3 seconds.
β
Last updated: β
const [polling, setPolling] = useState(false);
const { data } = useQuery({
queryKey: ["time"],
queryFn: () => ({
time: new Date().toLocaleTimeString(),
timestamp: Date.now(),
}),
refetchInterval: polling ? 3000 : false,
// When enabled: auto-refetch every 3 seconds!
});Each page is a separate cache entry. Navigate back to a visited page β it loads instantly from cache.
const [page, setPage] = useState(1);
const { data, isPlaceholderData, isFetching } = useQuery({
queryKey: ["posts", "paginated", page],
queryFn: () =>
fetch(`https://jsonplaceholder.typicode.com/posts?_page=${page}&_limit=5`)
.then(res => res.json()),
placeholderData: keepPreviousData,
// Keeps the old page visible while the new page loads
// Each page number = unique cache entry
});These are the patterns that trip up developers most often. Switch between Wrong and Fixed to compare the code side by side.
function UserProfile({ userId }) {
const { data } = useQuery({
queryKey: ["user", userId],
queryFn: () => fetchUser(userId),
});
const [user, setUser] = useState(null);
// Shadow copy β goes stale on background refetch!
useEffect(() => {
if (data) setUser(data);
}, [data]);
return <div>{user?.name}</div>;
}// In ComponentA:
useQuery({ queryKey: ["user", id], queryFn: () => fetchUser(id) });
// In ComponentB (typo!):
useQuery({ queryKey: ["users", id], queryFn: () => fetchUser(id) });
// Invalidation misses ComponentA:
queryClient.invalidateQueries({ queryKey: ["users"] });function Todos() {
const { data } = useQuery({
queryKey: ["todos"],
queryFn: fetchTodos,
});
// Crashes on first render: data is undefined
return <ul>{data.map(t => <li key={t.id}>{t.title}</li>)}</ul>;
}