- Published on
Boost React Performance: React Query Data Fetching & Mutations
- Authors
-
-
- Name
- Jitendra M
- @_JitendraM
-
Table of contents:
- π Introduction to React Query
-
Installation
- π GET Requests with useQuery
- βοΈ POST/PUT/DELETE with useMutation
- π Authentication Integration
- π― Advanced Patterns
- β Best Practices
- β οΈ Common Pitfalls
- β‘ Performance Optimization
- π― Summary
-
Explore More Topics

This comprehensive guide covers everything you need to know about using React Query (TanStack Query) for data fetching, mutations, and integrating with our custom authentication system.
π Introduction to React Query
React Query is a powerful data-fetching library that provides:
-
Caching: Automatic background updates and intelligent caching
-
Synchronization: Keeps your UI in sync with server state
-
Background Updates: Automatically refetches stale data
-
Optimistic Updates: Instant UI updates with rollback on failure
-
Error Handling: Built-in error states and retry logic
Core Concepts
- Queries: For fetching data (GET requests)
- Mutations: For modifying data (POST, PUT, DELETE requests)
- Query Keys: Unique identifiers for cached data
- Query Functions: Functions that return a Promise with your data
Installation
npm install @tanstack/react-query
Set up the QueryClient
in your root component:
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
const queryClient = new QueryClient();
function App() {
return (
<QueryClientProvider client={queryClient}>
<YourApp />
</QueryClientProvider>
);
}
π GET Requests with useQuery
Basic useQuery Structure
import { useQuery } from '@tanstack/react-query';
import axios from 'axios';
const useExample = () => {
return useQuery({
queryKey: ['unique-key'],
queryFn: async () => {
const response = await axios.get('/api/endpoint');
return response.data;
},
// Optional configurations
staleTime: 5 * 60 * 1000, // 5 minutes
cacheTime: 10 * 60 * 1000, // 10 minutes
refetchOnWindowFocus: false,
enabled: true, // Controls when query runs
});
};
Query States and Properties
const MyComponent = () => {
const {
data, // The data returned from queryFn
isLoading, // True if query is loading for the first time
isFetching, // True whenever query is fetching (including background)
isError, // True if query encountered an error
error, // The error object (if any)
isSuccess, // True if query completed successfully
refetch, // Function to manually refetch
isStale, // True if data is considered stale
} = useQuery({
queryKey: ['users'],
queryFn: fetchUsers,
});
if (isLoading) return <div>Loading...</div>;
if (isError) return <div>Error: {error.message}</div>;
if (isSuccess) return <div>Data: {JSON.stringify(data)}</div>;
};
Dynamic Query Keys
// β
Good: Dynamic query keys for different data
const useUserProfile = (userId: string) => {
return useQuery({
queryKey: ['user', 'profile', userId],
queryFn: async () => {
const response = await axios.get(`/api/users/${userId}`);
return response.data;
},
enabled: !!userId, // Only fetch when userId exists
});
};
// β
Good: Complex query keys
const usePostsWithFilters = (filters: { category?: string; status?: string }) => {
return useQuery({
queryKey: ['posts', filters],
queryFn: async () => {
const params = new URLSearchParams(filters);
const response = await axios.get(`/api/posts?${params}`);
return response.data;
},
});
};
Conditional Queries
// Only fetch when certain conditions are met
const useConditionalData = (shouldFetch: boolean, dataId?: string) => {
return useQuery({
queryKey: ['conditional-data', dataId],
queryFn: async () => {
const response = await axios.get(`/api/data/${dataId}`);
return response.data;
},
enabled: shouldFetch && !!dataId,
});
};
Dependent Queries
// Query that depends on another query's result
const useUserPosts = (userId: string) => {
// First query: Get user
const userQuery = useQuery({
queryKey: ['user', userId],
queryFn: () => fetchUser(userId),
enabled: !!userId,
});
// Second query: Get user's posts (depends on user data)
const postsQuery = useQuery({
queryKey: ['posts', userQuery.data?.id],
queryFn: () => fetchUserPosts(userQuery.data.id),
enabled: !!userQuery.data?.id, // Only run when user data is available
});
return {
user: userQuery.data,
posts: postsQuery.data,
isLoading: userQuery.isLoading || postsQuery.isLoading,
error: userQuery.error || postsQuery.error,
};
}
βοΈ POST/PUT/DELETE with useMutation
Basic useMutation Structure
import { useMutation, useQueryClient } from '@tanstack/react-query';
const useCreateUser = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (userData: CreateUserData) => {
const response = await axios.post('/api/users', userData);
return response.data;
},
onSuccess: (data, variables, context) => {
// Invalidate and refetch related queries
queryClient.invalidateQueries({ queryKey: ['users'] });
// Or update cache directly
queryClient.setQueryData(['user', data.id], data);
// Show success message
toast.success('User created successfully!');
},
onError: (error, variables, context) => {
// Handle error
toast.error('Failed to create user');
console.error('Mutation error:', error);
},
onSettled: (data, error, variables, context) => {
// Runs regardless of success/error
console.log('Mutation completed');
},
});
}
Mutation States and Usage
const CreateUserForm = () => {
const createUserMutation = useCreateUser();
const handleSubmit = (formData) => {
createUserMutation.mutate(formData);
};
return (
<form onSubmit={handleSubmit}>
<input type="text" name="name" />
<input type="email" name="email" />
<button
type="submit"
disabled={createUserMutation.isPending}
>
{createUserMutation.isPending ? 'Creating...' : 'Create User'}
</button>
{createUserMutation.isError && (
<div>Error: {createUserMutation.error.message}</div>
)}
{createUserMutation.isSuccess && (
<div>User created: {createUserMutation.data.name}</div>
)}
</form>
);
};
Different Mutation Types
// POST: Create new resource
const useCreatePost = () => {
return useMutation({
mutationFn: async (postData) => {
const response = await axios.post('/api/posts', postData);
return response.data;
},
});
};
// PUT: Update entire resource
const useUpdatePost = () => {
return useMutation({
mutationFn: async ({ id, data }) => {
const response = await axios.put(`/api/posts/${id}`, data);
return response.data;
},
});
};
// PATCH: Partial update
const useUpdatePostPartial = () => {
return useMutation({
mutationFn: async ({ id, updates }) => {
const response = await axios.patch(`/api/posts/${id}`, updates);
return response.data;
},
});
};
// DELETE: Remove resource
const useDeletePost = () => {
return useMutation({
mutationFn: async (postId) => {
await axios.delete(`/api/posts/${postId}`);
return postId;
},
});
};
Optimistic Updates
const useUpdatePost = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({ id, updates }) => {
const response = await axios.patch(`/api/posts/${id}`, updates);
return response.data;
},
onMutate: async ({ id, updates }) => {
// Cancel any outgoing refetches
await queryClient.cancelQueries({ queryKey: ['post', id] });
// Snapshot the previous value
const previousPost = queryClient.getQueryData(['post', id]);
// Optimistically update to the new value
queryClient.setQueryData(['post', id], (old) => ({
...old,
...updates,
}));
// Return a context object with the snapshotted value
return { previousPost };
},
onError: (err, { id }, context) => {
// If the mutation fails, use the context returned from onMutate to roll back
queryClient.setQueryData(['post', id], context.previousPost);
},
onSettled: (data, error, { id }) => {
// Always refetch after error or success
queryClient.invalidateQueries({ queryKey: ['post', id] });
},
});
};
π Authentication Integration
Using Our Custom useAuthenticatedQuery
import { useAuthenticatedQuery } from '@/lib/useAuthenticatedQuery';
// β
Simple authenticated query
const useUserProfile = () => {
return useAuthenticatedQuery({
queryKey: ['user', 'profile'],
queryFn: async () => {
const response = await axios.get('/api/user/profile');
return response.data;
},
staleTime: 5 * 60 * 1000,
});
// Automatically includes: enabled: !!localStorage.getItem('token')
};
// β
Authenticated query with additional conditions
const useChannelsList = (appId?: string) => {
return useAuthenticatedQuery({
queryKey: ['channels', appId],
queryFn: async () => {
const response = await axios.get(`/api/channels?appId=${appId}`);
return response.data;
},
}, !!appId); // Additional condition: only fetch when appId exists
// Final enabled: !!localStorage.getItem('token') && !!appId
};
Overriding to Regular useQuery
import { useQuery } from '@tanstack/react-query';
// β
Public data that doesn't require authentication
const usePublicPosts = () => {
return useQuery({
queryKey: ['public', 'posts'],
queryFn: async () => {
const response = await axios.get('/api/public/posts');
return response.data;
},
staleTime: 10 * 60 * 1000,
});
};
// β
Override authentication for specific cases
const usePostDetails = (postId: string, forcePublic = false) => {
// Use regular useQuery for public access
if (forcePublic) {
return useQuery({
queryKey: ['post', postId, 'public'],
queryFn: async () => {
const response = await axios.get(`/api/public/posts/${postId}`);
return response.data;
},
enabled: !!postId,
});
}
// Use authenticated query for private access
return useAuthenticatedQuery({
queryKey: ['post', postId, 'private'],
queryFn: async () => {
const response = await axios.get(`/api/posts/${postId}`);
return response.data;
},
}, !!postId);
}
Authentication in Mutations
// Mutations don't need authentication wrapper (they're triggered by user actions)
const useCreatePost = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (postData) => {
// Auth token is automatically included in axios config
const response = await axios.post('/api/posts', postData);
return response.data;
},
onSuccess: () => {
// Invalidate authenticated queries
queryClient.invalidateQueries({ queryKey: ['posts'] });
},
onError: (error) => {
if (error.response?.status === 401) {
// Handle authentication error
// Redirect to login or refresh token
}
},
});
};
π― Advanced Patterns
Query Factories
// Centralized query key management
export const postKeys = {
all: ['posts'] as const,
lists: () => [...postKeys.all, 'list'] as const,
list: (filters: string) => [...postKeys.lists(), { filters }] as const,
details: () => [...postKeys.all, 'detail'] as const,
detail: (id: string) => [...postKeys.details(), id] as const,
};
// Query factory functions
export const postQueries = {
list: (filters: PostFilters) => ({
queryKey: postKeys.list(JSON.stringify(filters)),
queryFn: () => fetchPosts(filters),
}),
detail: (id: string) => ({
queryKey: postKeys.detail(id),
queryFn: () => fetchPost(id),
}),
};
// Usage
const usePostList = (filters: PostFilters) => {
return useAuthenticatedQuery(postQueries.list(filters));
};
Infinite Queries
import { useInfiniteQuery } from '@tanstack/react-query';
const useInfinitePosts = (filters = {}) => {
return useInfiniteQuery({
queryKey: ['posts', 'infinite', filters],
queryFn: async ({ pageParam = 1 }) => {
const response = await axios.get('/api/posts', {
params: { ...filters, page: pageParam, limit: 10 }
});
return response.data;
},
getNextPageParam: (lastPage, pages) => {
return lastPage.hasMore ? pages.length + 1 : undefined;
},
initialPageParam: 1,
});
};
// Usage in component
const PostList = () => {
const {
data,
fetchNextPage,
hasNextPage,
isFetchingNextPage,
} = useInfinitePosts();
return (
<div>
{data?.pages.map((group, i) => (
<div key={i}>
{group.posts.map(post => (
<div key={post.id}>{post.title}</div>
))}
</div>
))}
<button
onClick={() => fetchNextPage()}
disabled={!hasNextPage || isFetchingNextPage}
>
{isFetchingNextPage ? 'Loading more...' : 'Load More'}
</button>
</div>
);
}
Parallel Queries
// Multiple queries running in parallel
const useDashboardData = (userId: string) => {
const userQuery = useAuthenticatedQuery({
queryKey: ['user', userId],
queryFn: () => fetchUser(userId),
}, !!userId);
const postsQuery = useAuthenticatedQuery({
queryKey: ['posts', 'user', userId],
queryFn: () => fetchUserPosts(userId),
}, !!userId);
const statsQuery = useAuthenticatedQuery({
queryKey: ['stats', 'user', userId],
queryFn: () => fetchUserStats(userId),
}, !!userId);
return {
user: userQuery.data,
posts: postsQuery.data,
stats: statsQuery.data,
isLoading: userQuery.isLoading || postsQuery.isLoading || statsQuery.isLoading,
error: userQuery.error || postsQuery.error || statsQuery.error,
};
};
β Best Practices
1. Query Key Conventions
// β
Good: Hierarchical and descriptive
['users', 'profile', userId]
['posts', 'list', { status: 'published', category: 'tech' }]
['channels', appId, 'settings']
// β Bad: Flat and unclear
['userProfile123']
['allPosts']
['data']
2. Error Handling
// β
Good: Comprehensive error handling
const usePostDetails = (postId: string) => {
return useAuthenticatedQuery({
queryKey: ['post', postId],
queryFn: async () => {
try {
const response = await axios.get(`/api/posts/${postId}`);
return response.data;
} catch (error) {
if (error.response?.status === 404) {
throw new Error('Post not found');
}
if (error.response?.status === 403) {
throw new Error('Access denied');
}
throw new Error('Failed to load post');
}
},
retry: (failureCount, error) => {
// Don't retry on 404 or 403
if (error.message.includes('not found') || error.message.includes('Access denied')) {
return false;
}
return failureCount < 3;
},
}, !!postId);
};
3. Cache Management
// β
Good: Strategic cache invalidation
const useUpdatePost = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: updatePost,
onSuccess: (data, variables) => {
// Update specific post
queryClient.setQueryData(['post', variables.id], data);
// Invalidate related lists
queryClient.invalidateQueries({
queryKey: ['posts', 'list'],
exact: false // Invalidates all post lists regardless of filters
});
// Invalidate user's posts if it's their post
if (data.authorId === getCurrentUserId()) {
queryClient.invalidateQueries({
queryKey: ['posts', 'user', data.authorId]
});
}
},
});
};
4. Loading States
// β
Good: Differentiate between loading states
const PostDetails = ({ postId }: { postId: string }) => {
const { data: post, isLoading, isFetching, error } = usePostDetails(postId);
if (error) {
return <ErrorMessage error={error} />;
}
if (isLoading) {
return <FullPageSpinner />; // Initial load
}
return (
<div>
{isFetching && <TopBarProgress />} {/* Background refetch */}
<h1>{post?.title}</h1>
<p>{post?.content}</p>
</div>
);
};
β οΈ Common Pitfalls
1. Query Key Dependencies
// β Bad: Missing dependencies in query key
const useUserPosts = (userId: string, filters: PostFilters) => {
return useQuery({
queryKey: ['posts', userId], // Missing filters!
queryFn: () => fetchUserPosts(userId, filters),
});
};
// β
Good: All dependencies included
const useUserPosts = (userId: string, filters: PostFilters) => {
return useQuery({
queryKey: ['posts', userId, filters],
queryFn: () => fetchUserPosts(userId, filters),
});
};
2. Stale Closures
// β Bad: Stale closure in mutation
const MyComponent = () => {
const [userId, setUserId] = useState('');
const mutation = useMutation({
mutationFn: updateUser,
onSuccess: () => {
// userId might be stale!
queryClient.invalidateQueries(['user', userId]);
},
});
// ...
};
// β
Good: Use mutation variables
const MyComponent = () => {
const [userId, setUserId] = useState('');
const mutation = useMutation({
mutationFn: updateUser,
onSuccess: (data, variables) => {
// Use variables.userId instead
queryClient.invalidateQueries(['user', variables.userId]);
},
});
// ...
};
3. Over-fetching
// β Bad: Fetching too frequently
const useUserProfile = () => {
return useQuery({
queryKey: ['user', 'profile'],
queryFn: fetchUserProfile,
staleTime: 0, // Always refetch
refetchInterval: 1000, // Every second
});
};
// β
Good: Reasonable cache and refetch settings
const useUserProfile = () => {
return useAuthenticatedQuery({
queryKey: ['user', 'profile'],
queryFn: fetchUserProfile,
staleTime: 5 * 60 * 1000, // 5 minutes
cacheTime: 10 * 60 * 1000, // 10 minutes
refetchOnWindowFocus: false,
});
};
β‘ Performance Optimization
1. Query Optimization
// β
Select only needed data
const useUserName = (userId: string) => {
return useAuthenticatedQuery({
queryKey: ['user', userId],
queryFn: () => fetchUser(userId),
select: (data) => data.name, // Only return name
staleTime: 10 * 60 * 1000,
}, !!userId);
};
// β
Structural sharing prevention
const useUserPosts = (userId: string) => {
return useAuthenticatedQuery({
queryKey: ['posts', 'user', userId],
queryFn: () => fetchUserPosts(userId),
select: useCallback(
(data) => data.filter(post => post.status === 'published'),
[]
),
}, !!userId);
};
2. Prefetching
// β
Prefetch related data
const usePostWithPrefetch = (postId: string) => {
const queryClient = useQueryClient();
const postQuery = useAuthenticatedQuery({
queryKey: ['post', postId],
queryFn: () => fetchPost(postId),
onSuccess: (data) => {
// Prefetch author data
queryClient.prefetchQuery({
queryKey: ['user', data.authorId],
queryFn: () => fetchUser(data.authorId),
staleTime: 5 * 60 * 1000,
});
},
}, !!postId);
return postQuery;
};
3. Background Updates
// β
Smart background updates
const useCriticalData = () => {
return useAuthenticatedQuery({
queryKey: ['critical', 'data'],
queryFn: fetchCriticalData,
staleTime: 30 * 1000, // 30 seconds
refetchInterval: 60 * 1000, // Refetch every minute
refetchIntervalInBackground: true, // Continue when tab is inactive
refetchOnWindowFocus: true, // Refetch when user returns
});
};
π― Summary
When to Use What
Scenario | Hook | Example |
---|---|---|
Fetch user data | useAuthenticatedQuery |
User profile, settings |
Fetch public data | useQuery |
Public posts, static content |
Create/Update data | useMutation |
Form submissions, file uploads |
Infinite scrolling | useInfiniteQuery |
Post feeds, search results |
Real-time data | useAuthenticatedQuery + refetchInterval |
Dashboard stats |
Quick Decision Tree
Need to fetch data?
βββ Is it authenticated?
β βββ Yes β useAuthenticatedQuery
β βββ No β useQuery
βββ Need to modify data?
βββ useMutation
Key Takeaways
-
Always use
useAuthenticatedQuery
for protected endpoints - Include all dependencies in query keys
- Handle loading and error states appropriately
- Use mutations for data modifications
- Optimize with caching strategies based on data criticality
- Leverage React Query DevTools for debugging