React Query data fetching
Introduction
In modern web development, efficient data fetching and state management are critical for building responsive and scalable applications. React Query has emerged as a powerful library that simplifies data fetching, caching, synchronization, and updates in React applications. Unlike traditional state management solutions like Redux, React Query is specifically designed to handle server-state, making it an excellent choice for applications that rely heavily on APIs.
This blog post explores React Query’s core features, demonstrates how to implement data fetching, and highlights best practices for optimizing performance. Whether you're fetching data from REST APIs, GraphQL, or other sources, React Query provides a streamlined developer experience with minimal boilerplate.
What is React Query?
React Query is a data-fetching and state management library for React applications. It abstracts away the complexities of handling asynchronous data, such as caching, background updates, and error handling, while providing a declarative API. Key features include:
- Automatic Caching: React Query caches fetched data by default, reducing redundant network requests.
- Background Refetching: Data is automatically refreshed in the background when stale.
- Optimistic Updates: Supports optimistic UI updates for mutations.
- Pagination & Infinite Queries: Built-in support for paginated and infinite-scroll data.
- Devtools: A dedicated DevTools panel for debugging queries and mutations.
Unlike client-state libraries (e.g., Redux), React Query focuses on server-state, meaning it handles data synchronization between your app and backend seamlessly.
Setting Up React Query
To get started with React Query, install the library using npm or yarn:
npm install @tanstack/react-query
Next, wrap your application with the QueryClientProvider
to provide the React Query context:
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; const queryClient = new QueryClient(); function App() { return ( <QueryClientProvider client={queryClient}> <YourAppComponent /> </QueryClientProvider> ); } export default App;
This setup enables React Query's features across your entire application.
Fetching Data with useQuery
The useQuery
hook is the primary way to fetch data in React Query. It requires a query key (a unique identifier for the query) and a query function (an asynchronous function that fetches data).
Basic Example: Fetching a Todo List
import { useQuery } from '@tanstack/react-query'; const fetchTodos = async () => { const response = await fetch('https://jsonplaceholder.typicode.com/todos'); if (!response.ok) { throw new Error('Network response was not ok'); } return response.json(); }; function Todos() { const { data, isLoading, error } = useQuery({ queryKey: ['todos'], queryFn: fetchTodos, }); if (isLoading) return <div>Loading...</div>; if (error) return <div>Error: {error.message}</div>; return ( <ul> {data?.map((todo) => ( <li key={todo.id}>{todo.title}</li> ))} </ul> ); }
Key Observations:
queryKey
: Acts as a unique identifier for the query (used for caching and refetching).queryFn
: The function that fetches the data.- Status Handling: React Query provides
isLoading
,error
, anddata
states out of the box.
Mutations with useMutation
While useQuery
handles data fetching, useMutation
is used for creating, updating, or deleting data (POST, PUT, DELETE requests).
Example: Adding a New Todo
import { useMutation, useQueryClient } from '@tanstack/react-query'; const addTodo = async (newTodo) => { const response = await fetch('https://jsonplaceholder.typicode.com/todos', { method: 'POST', body: JSON.stringify(newTodo), headers: { 'Content-Type': 'application/json' }, }); if (!response.ok) throw new Error('Failed to add todo'); return response.json(); }; function AddTodo() { const queryClient = useQueryClient(); const mutation = useMutation({ mutationFn: addTodo, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['todos'] }); // Refetch todos after mutation }, }); const handleSubmit = (e) => { e.preventDefault(); const formData = new FormData(e.target); const title = formData.get('title'); mutation.mutate({ title, completed: false }); }; return ( <form onSubmit={handleSubmit}> <input name="title" placeholder="Todo title" required /> <button type="submit" disabled={mutation.isPending}> {mutation.isPending ? 'Adding...' : 'Add Todo'} </button> </form> ); }
Key Features:
- Optimistic Updates: You can implement optimistic UI updates using
onMutate
. - Automatic Refetching:
invalidateQueries
ensures the data stays fresh after mutations. - Pending State:
isPending
helps manage loading states during mutations.
Advanced Features and Best Practices
1. Pagination and Infinite Queries
React Query supports paginated data with useInfiniteQuery
:
import { useInfiniteQuery } from '@tanstack/react-query'; const fetchPosts = async ({ pageParam = 1 }) => { const res = await fetch(`https://api.example.com/posts?page=${pageParam}`); return res.json(); }; function Posts() { const { data, fetchNextPage, hasNextPage } = useInfiniteQuery({ queryKey: ['posts'], queryFn: fetchPosts, getNextPageParam: (lastPage) => lastPage.nextPage, }); return ( <div> {data?.pages.map((page, i) => ( <div key={i}> {page.posts.map((post) => ( <div key={post.id}>{post.title}</div> ))} </div> ))} <button onClick={() => fetchNextPage()} disabled={!hasNextPage}> Load More </button> </div> ); }
2. Prefetching Data
Improve UX by prefetching data before it’s needed:
const queryClient = useQueryClient(); // Prefetch todos when hovering over a link const handleHover = () => { queryClient.prefetchQuery({ queryKey: ['todos'], queryFn: fetchTodos, }); };
3. Stale Time and Cache Control
Configure how long data remains fresh before refetching:
useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
staleTime: 5 * 60 * 1000, // Data stays fresh for 5 minutes
});
Conclusion
React Query revolutionizes data fetching in React applications by providing a robust, declarative API that handles caching, synchronization, and mutations effortlessly. By leveraging features like automatic background updates, pagination, and optimistic UI, developers can build performant apps with minimal boilerplate.
Key takeaways:
- Use
useQuery
for fetching and caching data. - Handle mutations with
useMutation
and invalidate queries to keep data fresh. - Optimize performance with pagination, prefetching, and stale time configurations.
Whether you're building a small project or a large-scale application, React Query simplifies server-state management, allowing you to focus on delivering a great user experience. Give it a try in your next project!