Why Not Just useEffect?
Fetching in useEffect means writing loading and error state by hand, handling race conditions, and refetching the same data on every mount. TanStack Query handles caching, deduplication, and background refetching for you.
Your First Query
A query needs a unique key and a function that returns a promise. The hook gives you the data along with its loading and error state.
tsx
import { useQuery } from '@tanstack/react-query';
export function Projects() {
const { data, isPending, error } = useQuery({
queryKey: ['projects'],
queryFn: () => fetch('/api/projects').then(res => res.json()),
});
if (isPending) return <p>Loading...</p>;
if (error) return <p>Something went wrong.</p>;
return (
<ul>
{data.map(project => (
<li key={project.id}>{project.name}</li>
))}
</ul>
);
}
Mutations and Cache Invalidation
After a successful mutation, invalidate the related query so the list refreshes with the new data.
tsx
const queryClient = useQueryClient();
const createProject = useMutation({
mutationFn: (name: string) =>
fetch('/api/projects', {
method: 'POST',
body: JSON.stringify({ name }),
}),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['projects'] });
},
});
Tips
- Use stable, descriptive query keys that include every parameter.
- Set staleTime to avoid unnecessary refetches.
- Prefetch on hover for instant navigation.
- Keep server data in the query cache instead of global state.