Data that lives on a server is not application state. You do not own it, it goes stale without telling you, and two components asking for it at once should not produce two requests. The Effect-based fetch of Fetching Data in an Effect handles none of that; npm 2,036 install @tanstack/react-query handles all of it behind two Hooks. One QueryClient holds the cache and one provider puts it in context; each query is identified by a queryKey, an array that doubles as a dependency list — change the key and Query fetches the new entry while showing the old.
import { QueryClient, QueryClientProvider, useQuery, useMutation, useQueryClient }
from '@tanstack/react-query';
const client = new QueryClient();
const api = 'https://api.example.com/products';
function Products({ category }) {
const qc = useQueryClient();
const { data, isPending, isError, error } = useQuery({
queryKey: ['products', category], staleTime: 30_000,
queryFn: () => fetch(`${api}?category=${category}`).then((r) => r.json()),
});
const add = useMutation({
mutationFn: (p) => fetch(api, { method: 'POST', body: JSON.stringify(p) }),
onSuccess: () => qc.invalidateQueries({ queryKey: ['products'] }),
});
if (isPending) return <p>Loading...</p>;
if (isError) return <p>{error.message}</p>;
return (<>
<ul>{data.map((p) => <li key={p.id}>{p.name}</li>)}</ul>
<button onClick={() => add.mutate({ name: 'New', category })}>Add</button>
</>);
}
export default () => (
<QueryClientProvider client={client}><Products category="keyboards"/></QueryClientProvider>);What those lines buy: identical keys mounted anywhere share one request and one cache entry; a query older than staleTime refetches in the background on mount, on window focus and on reconnect while the stale data stays on screen; failures retry with exponential backoff; and invalidateQueries marks every key beginning with ['products'] stale, so the list refetches itself after a successful mutation.
The alternatives are SWR 639 (2.5.1, smaller) and RTK Query (Queries with RTK Query), the right answer when the project already runs Redux Toolkit 3,824 . Never run two: each keeps its own cache, and the same endpoint fetched through both will disagree with itself.