The Problem with Manual Data Fetching
If you've ever fetched data in React using useEffect, you know it can get complicated fast. You need to manage multiple states yourself:
- Is the data currently loading? (isLoading)
- Did an error occur? (error)
- Where is the actual data? (data)
- Is the data "stale" and needs to be refetched?
- How do I re-fetch when the user re-focuses the window?
This leads to a lot of repetitive and error-prone boilerplate code in your components.
TanStack Query: A Server-State Manager
TanStack Query is not a global state manager like Redux or Zustand. It doesn't care about your client state (like whether a modal is open). It excels at managing server state: data that lives on your server and is fetched asynchronously.
It handles all the complexity for you, giving you a simple hook-based API to declare your data needs.
The useQuery Hook: Fetching Data
The useQuery hook is the workhorse for fetching (GET) data. It only needs two main things:
- A unique queryKey: An array that uniquely identifies this piece of data.
- A queryFn: An async function that fetches and returns the data.
JavaScript
import { useQuery, QueryClient, QueryClientProvider } from '@tanstack/react-query';
import axios from 'axios';
// 1. Create a client
const queryClient = new QueryClient();
// 2. Provide the client to your app
function App() {
return (
<QueryClientProvider client={queryClient}>
<Todos />
</QueryClientProvider>
);
}
// 3. Fetch data in a component
function Todos() {
// `useQuery` handles all the state management for us
const { data, error, isLoading } = useQuery({
queryKey: ['todos'], // A unique key for this query
queryFn: async () => { // The function to fetch data
const { data } = await axios.get('https://api.example.com/todos');
return data;
},
});
if (isLoading) return 'Loading...';
if (error) return 'An error has occurred: ' + error.message;
return (
<ul>
{data.map((todo) => (
<li key={todo.id}>{todo.title}</li>
))}
</ul>
);
}
Caching: Behind the scenes, TanStack Query automatically caches the data from this query using ['todos'] as the key. If another component uses useQuery with the same key, it will instantly receive the cached data while a fresh fetch happens in the background (the "stale-while-revalidate" strategy).
The useMutation Hook: Changing Data
For any action that changes data on the server (POST, PUT, DELETE), you use the useMutation hook.
After a mutation succeeds, the data we have on our client is now out of date. For example, if we add a new todo, our ['todos'] query is now stale. We need to tell TanStack Query to refetch it. This is called invalidation.
Code Example:
JavaScript
import { useMutation, useQueryClient } from '@tanstack/react-query';
function AddTodoForm() {
// Get the query client instance
const queryClient = useQueryClient();
// Create the mutation
const mutation = useMutation({
mutationFn: (newTodo) => {
// The function that performs the API call
return axios.post('/api/todos', newTodo);
},
onSuccess: () => {
// When the mutation is successful, invalidate the 'todos' query.
// This will cause any component using that query to automatically refetch.
queryClient.invalidateQueries({ queryKey: ['todos'] });
console.log("Todo added and list invalidated!");
},
});
const handleSubmit = (e) => {
e.preventDefault();
const title = e.target.elements.title.value;
mutation.mutate({ title }); // Execute the mutation
};
return (
<form onSubmit={handleSubmit}>
<input name="title" />
<button type="submit" disabled={mutation.isLoading}>
{mutation.isLoading ? 'Adding...' : 'Add Todo'}
</button>
</form>
);
}