React Query (TanStack Query)
Learn React Query for efficient server state management, caching, and data synchronization. This is a foundational concept in component-based UI development that professional developers rely on daily. The explanations below are written to be beginner-friendly while covering the depth and nuance that comes from real-world React experience. Take your time with each section and practice the examples
What is React Query?
React Query is a powerful library for managing server state in React applications. It provides caching, background updates, error handling, and optimistic updates out of the box.. This is an essential concept that every React developer must understand thoroughly. In professional development environments, getting this right can mean the difference between code that works reliably and code that breaks in production. The following sections break this down into clear, digestible pieces with practical examples you can try immediately
Basic React Query Setup
// Install React Query
npm install @tanstack/react-query
// Setup QueryClient
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 5 * 60 * 1000, // 5 minutes
cacheTime: 10 * 60 * 1000, // 10 minutes
retry: 3,
refetchOnWindowFocus: false,
},
},
});
function App() {
return (
<QueryClientProvider client={queryClient}>
<div className="app">
<UserList />
<ReactQueryDevtools initialIsOpen={false} />
</div>
</QueryClientProvider>
);
}
// Basic query usage
import { useQuery } from '@tanstack/react-query';
function UserList() {
const { data: users, isLoading, error, refetch } = useQuery({
queryKey: ['users'],
queryFn: async () => {
const response = await fetch('/api/users');
if (!response.ok) {
throw new Error('Failed to fetch users');
}
return response.json();
},
});
if (isLoading) return <div>Loading users...</div>;
if (error) return <div>Error: {error.message}</div>;
return (
<div>
<h2>Users</h2>
<button onClick={() => refetch()}>Refresh</button>
<ul>
{users.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
</div>
);
}
// Query with parameters
function UserProfile({ userId }) {
const { data: user, isLoading, error } = useQuery({
queryKey: ['user', userId],
queryFn: async () => {
const response = await fetch('/api/users/' + userId);
if (!response.ok) {
throw new Error('Failed to fetch user');
}
return response.json();
},
enabled: !!userId, // Only run query if userId exists
});
if (isLoading) return <div>Loading user...</div>;
if (error) return <div>Error: {error.message}</div>;
if (!user) return <div>User not found</div>;
return (
<div>
<h2>{user.name}</h2>
<p>Email: {user.email}</p>
<p>Role: {user.role}</p>
</div>
);
}Mutations and Optimistic Updates
// Mutations with React Query
import { useMutation, useQueryClient } from '@tanstack/react-query';
function CreateUser() {
const queryClient = useQueryClient();
const createUserMutation = useMutation({
mutationFn: async (userData) => {
const response = await fetch('/api/users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(userData),
});
if (!response.ok) {
throw new Error('Failed to create user');
}
return response.json();
},
onSuccess: (newUser) => {
// Invalidate and refetch users query
queryClient.invalidateQueries({ queryKey: ['users'] });
// Or update cache directly
queryClient.setQueryData(['users'], (oldData) => {
return oldData ? [...oldData, newUser] : [newUser];
});
},
onError: (error) => {
console.error('Failed to create user:', error);
},
});
const handleSubmit = (e) => {
e.preventDefault();
const formData = new FormData(e.target);
const userData = {
name: formData.get('name'),
email: formData.get('email'),
};
createUserMutation.mutate(userData);
};
return (
<form onSubmit={handleSubmit}>
<input name="name" placeholder="Name" required />
<input name="email" type="email" placeholder="Email" required />
<button
type="submit"
disabled={createUserMutation.isPending}
>
{createUserMutation.isPending ? 'Creating...' : 'Create User'}
</button>
</form>
);
}
// Optimistic updates
function UpdateUser({ user }) {
const queryClient = useQueryClient();
const updateUserMutation = useMutation({
mutationFn: async ({ userId, updates }) => {
const response = await fetch('/api/users/' + userId, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(updates),
});
if (!response.ok) {
throw new Error('Failed to update user');
}
return response.json();
},
onMutate: async ({ userId, updates }) => {
// Cancel any outgoing refetches
await queryClient.cancelQueries({ queryKey: ['users'] });
// Snapshot the previous value
const previousUsers = queryClient.getQueryData(['users']);
// Optimistically update to the new value
queryClient.setQueryData(['users'], (old) => {
return old.map(user =>
user.id === userId ? { ...user, ...updates } : user
);
});
// Return a context object with the snapshotted value
return { previousUsers };
},
onError: (err, variables, context) => {
// If the mutation fails, use the context returned from onMutate to roll back
if (context.previousUsers) {
queryClient.setQueryData(['users'], context.previousUsers);
}
},
onSettled: () => {
// Always refetch after error or success
queryClient.invalidateQueries({ queryKey: ['users'] });
},
});
const handleUpdate = (updates) => {
updateUserMutation.mutate({ userId: user.id, updates });
};
return (
<div>
<h3>{user.name}</h3>
<button
onClick={() => handleUpdate({ role: 'Admin' })}
disabled={updateUserMutation.isPending}
>
Make Admin
</button>
</div>
);
}