React Query and Server State Management
60 minReact Query (TanStack Query) is a powerful library for managing server state in React applications.
It provides automatic background refetching, caching, synchronization, and error handling for API calls.
React Query separates server state from client state, making it easier to manage data fetching and caching.
React Query automatically caches data, deduplicates requests, and provides loading and error states out of the box. This eliminates the need to manually manage these concerns.
The library supports optimistic updates, allowing you to update the UI immediately while the server request is in progress, then roll back if it fails.
React Query's query invalidation system ensures data stays fresh by automatically refetching when related data changes, keeping your UI in sync with server state.
Key Concepts
- React Query manages server state separately from client state.
- Automatic caching, deduplication, and background refetching.
- Built-in loading and error states for queries and mutations.
- Optimistic updates for better user experience.
- Query invalidation keeps data fresh automatically.
Learning Objectives
Master
- Setting up React Query with QueryClient and QueryClientProvider
- Using useQuery for data fetching with automatic caching
- Implementing mutations with useMutation
- Handling optimistic updates and error recovery
Develop
- Understanding server state vs client state
- Designing data fetching strategies
- Building responsive UIs with React Query
Tips
- Configure QueryClient with appropriate staleTime based on your data freshness requirements.
- Use consistent query key patterns for easier cache management.
- Implement error boundaries to handle query errors gracefully.
- Use React Query Devtools in development to inspect queries and cache.
Common Pitfalls
- Not configuring QueryClient defaults, leading to excessive refetching.
- Using inconsistent query keys, breaking cache invalidation.
- Not handling error states properly in mutations.
- Over-fetching data when more specific queries would suffice.
Summary
- React Query simplifies server state management with automatic caching.
- It provides loading and error states out of the box.
- Optimistic updates improve user experience.
- Query invalidation keeps data fresh automatically.
Exercise
Implement a comprehensive data fetching system using React Query with caching, mutations, and optimistic updates.
import { QueryClient, QueryClientProvider, useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { useState } from 'react';
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 5 * 60 * 1000, // 5 minutes
cacheTime: 10 * 60 * 1000, // 10 minutes
retry: 3,
refetchOnWindowFocus: false
}
}
});
// API functions
const api = {
getUsers: async () => {
const response = await fetch('https://jsonplaceholder.typicode.com/users');
if (!response.ok) throw new Error('Failed to fetch users');
return response.json();
},
getUser: async (id) => {
const response = await fetch(`https://jsonplaceholder.typicode.com/users/${id}`);
if (!response.ok) throw new Error('Failed to fetch user');
return response.json();
},
createUser: async (userData) => {
const response = await fetch('https://jsonplaceholder.typicode.com/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();
},
updateUser: async ({ id, ...userData }) => {
const response = await fetch(`https://jsonplaceholder.typicode.com/users/${id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(userData)
});
if (!response.ok) throw new Error('Failed to fetch user');
return response.json();
},
deleteUser: async (id) => {
const response = await fetch(`https://jsonplaceholder.typicode.com/users/${id}`, {
method: 'DELETE'
});
if (!response.ok) throw new Error('Failed to delete user');
return { id };
}
};
// Custom hooks
function useUsers() {
return useQuery({
queryKey: ['users'],
queryFn: api.getUsers,
select: (data) => data.map(user => ({
...user,
fullName: `${user.name} (${user.username})`
}))
});
}
function useUser(id) {
return useQuery({
queryKey: ['user', id],
queryFn: () => api.getUser(id),
enabled: !!id
});
}
function useCreateUser() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: api.createUser,
onSuccess: (newUser) => {
// Optimistically update the cache
queryClient.setQueryData(['users'], (oldData) => {
return oldData ? [...oldData, newUser] : [newUser];
});
// Invalidate and refetch users list
queryClient.invalidateQueries({ queryKey: ['users'] });
},
onError: (error) => {
console.error('Failed to create user:', error);
}
});
}
function useUpdateUser() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: api.updateUser,
onMutate: async (updatedUser) => {
// Cancel any outgoing refetches
await queryClient.cancelQueries({ queryKey: ['users'] });
await queryClient.cancelQueries({ queryKey: ['user', updatedUser.id] });
// Snapshot the previous value
const previousUsers = queryClient.getQueryData(['users']);
const previousUser = queryClient.getQueryData(['user', updatedUser.id]);
// Optimistically update to the new value
queryClient.setQueryData(['users'], (oldData) => {
return oldData ? oldData.map(user =>
user.id === updatedUser.id ? { ...user, ...updatedUser } : user
) : oldData;
});
queryClient.setQueryData(['user', updatedUser.id], updatedUser);
// Return a context object with the snapshotted value
return { previousUsers, previousUser };
},
onError: (err, updatedUser, context) => {
// If the mutation fails, use the context returned from onMutate to roll back
if (context?.previousUsers) {
queryClient.setQueryData(['users'], context.previousUsers);
}
if (context?.previousUser) {
queryClient.setQueryData(['user', updatedUser.id], context.previousUser);
}
},
onSettled: (data, error, variables) => {
// Always refetch after error or success
queryClient.invalidateQueries({ queryKey: ['users'] });
queryClient.invalidateQueries({ queryKey: ['user', variables.id] });
}
});
}
function useDeleteUser() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: api.deleteUser,
onSuccess: (deletedUser) => {
// Remove from cache
queryClient.setQueryData(['users'], (oldData) => {
return oldData ? oldData.filter(user => user.id !== deletedUser.id) : oldData;
});
// Remove individual user cache
queryClient.removeQueries({ queryKey: ['user', deletedUser.id] });
}
});
}
// Components
function UserList() {
const { data: users, isLoading, error } = useUsers();
const createUserMutation = useCreateUser();
const deleteUserMutation = useDeleteUser();
const handleCreateUser = () => {
const newUser = {
name: 'New User',
username: 'newuser',
email: 'newuser@example.com'
};
createUserMutation.mutate(newUser);
};
if (isLoading) return <div>Loading users...</div>;
if (error) return <div>Error: {error.message}</div>;
return (
<div>
<h2>Users</h2>
<button onClick={handleCreateUser} disabled={createUserMutation.isPending}>
{createUserMutation.isPending ? 'Creating...' : 'Add User'}
</button>
<ul>
{users?.map(user => (
<li key={user.id}>
{user.fullName} - {user.email}
<button
onClick={() => deleteUserMutation.mutate(user.id)}
disabled={deleteUserMutation.isPending}
>
Delete
</button>
</li>
))}
</ul>
</div>
);
}
function UserForm({ userId, onSave }) {
const { data: user, isLoading } = useUser(userId);
const updateUserMutation = useUpdateUser();
const [formData, setFormData] = useState({});
const handleSubmit = (e) => {
e.preventDefault();
updateUserMutation.mutate({ id: userId, ...formData });
onSave?.();
};
if (isLoading) return <div>Loading user...</div>;
return (
<form onSubmit={handleSubmit}>
<input
type="text"
placeholder="Name"
defaultValue={user?.name}
onChange={(e) => setFormData(prev => ({ ...prev, name: e.target.value }))}
/>
<input
type="email"
placeholder="Email"
defaultValue={user?.email}
onChange={(e) => setFormData(prev => ({ ...prev, email: e.target.value }))}
/>
<button type="submit" disabled={updateUserMutation.isPending}>
{updateUserMutation.isPending ? 'Updating...' : 'Update User'}
</button>
</form>
);
}
// Main App
function App() {
return (
<QueryClientProvider client={queryClient}>
<div className="app">
<h1>React Query Demo</h1>
<UserList />
</div>
</QueryClientProvider>
);
}