React Query (TanStack Query)
Learn React Query for efficient server state management, caching, and data synchronization
90 minā¢By Priygop Teamā¢Last updated: Feb 2026
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.
Basic React Query Setup
Example
// 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
Example
// 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>
);
}