Back
Amine Ben Yedder
Amine Ben Yedder
Mastering React Query: A Complete Guide from Setup to Advanced Usage

Mastering React Query: A Complete Guide from Setup to Advanced Usage

Why React Query?

Managing server state in React applications can be tricky. Redux and Context API work well for client-side state, but handling async data (fetching, caching, and synchronization) often leads to boilerplate code and complexity.

Why


Enter React Query: a powerful library that simplifies data fetching, caching, and state synchronization with minimal setup. It eliminates the need for manual state management for server data, making your code cleaner and more efficient.
In this guide, we’ll cover:
Initial Setup: Getting started with React Query
Core Concepts: Fetching data with useQuery
Advanced Techniques: Mutations, pagination, optimistic updates
Best Practices: Optimizing performance and avoiding pitfalls
Let’s get started!

1. Getting started with React Query

a. Installation

First initialize your react project:

terminal
1npm create vite@latest react_query_app

This will give you suggestions:

  • Select a framework ... select: React
  • Select a variant ... select: Typescript

Now, add React Query to your project:

terminal
1npm install @tanstack/react-query

b. Configure the Query Client

Wrap your app with QueryClientProvider to enable React Query:

main.tsx
1import { StrictMode } from "react"; 2import { createRoot } from "react-dom/client"; 3import App from "./App.tsx"; 4import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; 5 6const queryClient = new QueryClient(); 7 8createRoot(document.getElementById("root")!).render( 9 <StrictMode> 10 <QueryClientProvider client={queryClient}> 11 <App /> 12 </QueryClientProvider> 13 </StrictMode> 14); 15

That’s it! You’re now ready to fetch and manage server data effortlessly.

2. Fetching Data with useQuery

a. Basic Data Fetching Example

The useQuery hook handles data fetching, caching, and loading states automatically.

App.tsx
1import { useQuery } from "@tanstack/react-query"; 2 3type Post = { 4 userId: number; 5 id: number; 6 title: string; 7 body: string; 8}; 9 10const fetchPosts = async () => { 11 const res = await fetch("https://jsonplaceholder.typicode.com/posts"); 12 return res.json(); 13}; 14 15function App() { 16 const { data, isLoading, error } = useQuery<Post[]>({ 17 queryKey: ["posts"], // Unique key for caching 18 queryFn: fetchPosts, // Function to fetch data 19 }); 20 21 if (isLoading) return <div>Loading...</div>; 22 if (error) return <div>Error: {error.message}</div>; 23 24 return ( 25 <ul> 26 {data?.map((post) => ( 27 <li key={post.id}>{post.title}</li> 28 ))} 29 </ul> 30 ); 31} 32 33export default App;

b. Key Benefits:

  • Automatic Caching: Data is stored and reused efficiently.
  • Background Updates: Stale data refreshes automatically.
  • Built-in Loading & Error States: No need for manual state management.

3. Advanced Data Management

a. Pagination with useQuery

Implement smooth pagination using keepPreviousData:

App.tsx
1import { keepPreviousData, useQuery } from "@tanstack/react-query"; 2import { useState } from "react"; 3 4type Post = { 5 userId: number; 6 id: number; 7 title: string; 8 body: string; 9}; 10 11const fetchPosts = async ({ limit, page }: { limit: string; page: number }) => { 12 const res = await fetch( 13 `https://jsonplaceholder.typicode.com/posts?_limit=${limit}&_page=${page}` 14 ); 15 return res.json(); 16}; 17 18function App() { 19 const [limit, setLimit] = useState("10"); 20 const [page, setPage] = useState(1); 21 const { data, isLoading, error } = useQuery<Post[]>({ 22 queryKey: ["posts", limit, page], // Unique key for caching 23 queryFn: () => fetchPosts({ limit, page }), // Function to fetch data 24 }); 25 26 if (isLoading) return <div>Loading...</div>; 27 if (error) return <div>Error: {error.message}</div>; 28 29 return ( 30 <> 31 <ul> 32 {data?.map((post) => ( 33 <li key={post.id}>{post.title}</li> 34 ))} 35 </ul> 36 <select onChange={(e) => setLimit(e.target.value)} value={limit}> 37 <option value="5">5</option> 38 <option value="10">10</option> 39 <option value="25">25</option> 40 <option value="50">50</option> 41 </select> 42 <button onClick={() => setPage((p) => Math.max(p - 1, 1))}> 43 Previous 44 </button> 45 <button onClick={() => setPage((p) => p + 1)}>Next</button> 46 </> 47 ); 48} 49 50export default App; 51

b. Modifying Data with useMutation

Create, update, or delete data with mutations:

App.tsx
1import { useMutation } from "@tanstack/react-query"; 2 3type User = { 4 name: string; 5 email: string; 6}; 7 8const addUser = async (newUser: User) => { 9 const res = await fetch("https://jsonplaceholder.typicode.com/users", { 10 method: "POST", 11 body: JSON.stringify(newUser), 12 }); 13 return res.json(); 14}; 15 16function App() { 17 const mutation = useMutation({ 18 mutationFn: addUser, 19 }); 20 21 const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => { 22 e.preventDefault(); 23 mutation.mutate({ name: "John Doe", email: "john@example.com" }); 24 }; 25 26 return ( 27 <form onSubmit={handleSubmit}> 28 <button type="submit" disabled={mutation.isPending}> 29 {mutation.isPending ? "Saving..." : "Add User"} 30 </button> 31 </form> 32 ); 33} 34 35export default App; 36

c. useQuery vs useMutation

Think of these hooks as two different tools for different jobs when working with server data:

  • useQuery is your data fetcher:
    • It executes on page load
    • It caches the result
    • Refreshes automatically with configuration
  • useMutation is your data manager:
    • It does not execute on page load
    • It does not cache the result
    • Can only be called manually

d. Understanding invalidateQueries in React Query Mutations

When working with useMutation in React Query, one of the most important concepts is cache invalidation and invalidateQueries is your primary tool for keeping your UI in sync with server data after mutations.

e. What Does invalidateQueries Do?

InvalidateQueries marks cached data as stale and triggers automatic refetches for matching queries. This ensures your UI always displays fresh data after mutations

App.tsx
1import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; 2 3type User = { 4 name: string; 5 email: string; 6}; 7 8type UserWithId = User & { id: number }; 9 10const fetchUsers = async () => { 11 const res = await fetch(`https://jsonplaceholder.typicode.com/users`); 12 return res.json(); 13}; 14 15const addUser = async (newUser: User) => { 16 const res = await fetch("https://jsonplaceholder.typicode.com/users", { 17 method: "POST", 18 body: JSON.stringify(newUser), 19 }); 20 return res.json(); 21}; 22 23function App() { 24 const { data, isLoading } = useQuery<UserWithId[]>({ 25 queryKey: ["users"], 26 queryFn: fetchUsers, 27 }); 28 const queryClient = useQueryClient(); 29 const mutation = useMutation({ 30 mutationFn: addUser, 31 onSuccess: () => { 32 queryClient.invalidateQueries({ queryKey: ["users"] }); // Refresh user list 33 }, 34 }); 35 36 const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => { 37 e.preventDefault(); 38 mutation.mutate({ name: "John Doe", email: "john@example.com" }); 39 }; 40 41 if (isLoading) return <p>Loading users...</p>; 42 43 return ( 44 <form onSubmit={handleSubmit}> 45 {data?.map((user) => ( 46 <article key={user.id}> 47 <h3>{user.name}</h3> 48 <span>{user.email}</span> 49 </article> 50 ))} 51 <button type="submit" disabled={mutation.isPending}> 52 {mutation.isPending ? "Saving..." : "Add User"} 53 </button> 54 </form> 55 ); 56} 57 58export default App;

In this example, After the mutation succeeds, the users query will be invalidated triggering the fetchUsers query to display fresh data.

f. Optimistic Updates for a Smoother UX

Update the UI instantly while waiting for server confirmation:

App.tsx
1import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; 2 3type User = { 4 name: string; 5 email: string; 6}; 7 8type UserWithId = User & { id: number }; 9 10const fetchUsers = async () => { 11 const res = await fetch(`https://jsonplaceholder.typicode.com/users`); 12 return res.json(); 13}; 14 15const addUser = async (newUser: User) => { 16 const res = await fetch("https://jsonplaceholder.typicode.com/users", { 17 method: "POST", 18 body: JSON.stringify(newUser), 19 }); 20 return res.json(); 21}; 22 23function App() { 24 const { data, isLoading } = useQuery<UserWithId[]>({ 25 queryKey: ["users"], 26 queryFn: fetchUsers, 27 }); 28 const queryClient = useQueryClient(); 29 const mutation = useMutation({ 30 mutationFn: addUser, 31 onError: () => { 32 console.log( 33 "When this mutation succeeds, this message will be displayed" 34 ); 35 }, 36 onSuccess: () => { 37 console.log("When this mutation fails, this message will be displayed"); 38 queryClient.invalidateQueries({ queryKey: ["users"] }); 39 }, 40 onSettled: () => { 41 console.log( 42 "Wether it fails or succeeds, When this mutation ends, this message will be displayed" 43 ); 44 }, 45 }); 46 47 const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => { 48 e.preventDefault(); 49 mutation.mutate({ name: "John Doe", email: "john@example.com" }); 50 }; 51 52 if (isLoading) return <p>Loading users...</p>; 53 54 return ( 55 <form onSubmit={handleSubmit}> 56 {data?.map((user) => ( 57 <article key={user.id}> 58 <h3>{user.name}</h3> 59 <span>{user.email}</span> 60 </article> 61 ))} 62 <button type="submit" disabled={mutation.isPending}> 63 {mutation.isPending ? "Saving..." : "Add User"} 64 </button> 65 </form> 66 ); 67} 68 69export default App;

4. Best Practices for Optimal Performance

🚀 Use Descriptive Query Keys: Example: ['posts', { limit: '10', page: 1 }] for better cache management.
🚀 Adjust staleTime & cacheTime: Control how long data remains fresh or cached.
🚀 Prefetch Data for Better UX: Load data before the user needs it.
🚀 Use select for Derived Data: Transform API responses without altering cache.
🚀 Disable Unnecessary Refetches: Set refetchOnWindowFocus: false if not needed.

5. Conclusion

React Query revolutionizes how we handle server state in React apps. By leveraging its powerful caching, background updates, and mutation handling, you can build faster, more responsive applications with less code.

Explore features like Infinite Queries for seamless endless scrolling, Server-Side Rendering (SSR) Support for improved SEO and performance, and Custom Hooks to keep your logic reusable and your codebase clean. With React Query, managing async data becomes intuitive, reducing boilerplate and optimizing performance effortlessly.

Happy coding! 🚀