Managing asynchronous data in React applications can be a daunting task—handling API calls, loading states, errors, and caching often leads to complex and hard-to-maintain code. Fortunately, TanStack Query (formerly known as React Query) offers a clean, efficient solution to these challenges. In this blog post, we’ll explore what TanStack Query is, why it’s a must-have tool for React developers, and how to use it effectively with practical examples and references for further learning.
This guide is designed to be clear, concise, and beginner-friendly while providing actionable insights you can apply in your projects right away. Let’s dive in!
What is TanStack Query?
TanStack Query is a powerful library for managing, caching, and synchronizing asynchronous data in React applications. It provides hooks like useQuery and useMutation to streamline data fetching and updates, eliminating much of the boilerplate code you’d typically write. Whether you’re fetching a list of items, submitting a form, or keeping your UI in sync with a server, TanStack Query simplifies the process with a declarative and intuitive API.
Why Use TanStack Query?
In traditional React applications, you might use useEffect and local state to fetch data, like this:
function MyComponent() {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
useEffect(() => {
setLoading(true);
fetch('/api/data')
.then(res => res.json())
.then(data => setData(data))
.catch(err => setError(err))
.finally(() => setLoading(false));
}, []);
// Render logic here
}
This approach works but quickly becomes messy when you add caching, retries, or multiple API calls. TanStack Query solves these issues by offering:
- Simplified data fetching: Fetch data with a single hook and get built-in state management.
- Automatic caching: Reuse data across components without redundant requests.
- Effortless loading and error handling: Access isLoading and error states out of the box.
- Powerful invalidation: Keep your data fresh with minimal effort.
Setting Up TanStack Query in Your React Project
Let’s get started with TanStack Query in a React app. The setup is quick and straightforward.
Installation
Install TanStack Query using npm or yarn:
npm install @tanstack/react-query
Basic Configuration
To use TanStack Query, wrap your application with QueryClientProvider and provide it with a QueryClient instance. This sets up the caching and configuration system.
Here’s how to do it:
import React from 'react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
const queryClient = new QueryClient();
function App() {
return (
<QueryClientProvider client={queryClient}>
{/* Your app components go here */}
</QueryClientProvider>
);
}
export default App;
That’s it! Now you’re ready to use TanStack Query hooks in your components.
Core Concepts of TanStack Query
TanStack Query revolves around two key concepts: queries for fetching data and mutations for updating data. Let’s break them down.
Queries: Fetching Data
The useQuery hook is your go-to tool for fetching data. It takes a unique query key (used for caching) and a function that returns a promise.
Example: Fetching a Todo List
Imagine you have an API endpoint at /api/todos that returns a list of todos. Here’s how to fetch and display them:
import { useQuery } from '@tanstack/react-query';
function TodoList() {
const { data, isLoading, error } = useQuery(['todos'], () =>
fetch('/api/todos').then(res => res.json())
);
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>
);
}
What’s happening here?
- [‘todos’] is the query key, which uniquely identifies this data in the cache.
- The fetch function returns a promise that resolves to the todo list.
- isLoading and error provide the loading and error states, making UI updates a breeze.
Mutations: Updating Data
The useMutation hook handles data modifications (e.g., creating, updating, or deleting resources). Mutations are typically triggered by user actions, like submitting a form.
Example: Adding a New Todo
Let’s create a form to add a new todo and update the list:
import { useMutation, useQueryClient } from '@tanstack/react-query';
function AddTodo() {
const queryClient = useQueryClient();
const mutation = useMutation(
newTodo =>
fetch('/api/todos', {
method: 'POST',
body: JSON.stringify(newTodo),
headers: { 'Content-Type': 'application/json' },
}).then(res => res.json()),
{
onSuccess: () => {
queryClient.invalidateQueries(['todos']);
},
}
);
const handleSubmit = e => {
e.preventDefault();
const newTodo = { title: e.target.title.value };
mutation.mutate(newTodo);
};
return (
<form onSubmit={handleSubmit}>
<input type="text" name="title" placeholder="Enter todo title" />
<button type="submit">Add Todo</button>
</form>
);
}
What’s happening here?
- useMutation defines the API call to create a new todo.
- mutation.mutate(newTodo) triggers the mutation with the form data.
- onSuccess invalidates the ‘todos’ query, prompting a refetch to update the list.
Caching and Invalidation
TanStack Query caches data automatically based on the query key. When you request the same data again, it serves the cached version instantly and refetches in the background (a strategy called stale-while-revalidate).
To ensure data stays fresh, use queryClient.invalidateQueries. In the mutation example, we invalidated the ‘todos’ query after adding a new todo, triggering a refetch the next time the TodoList component renders.
Practical Examples
Let’s put it all together with two real-world examples.
Example 1: Fetching and Displaying Users
Suppose you have an API at /api/users that returns a list of users:
import { useQuery } from '@tanstack/react-query';
function UserList() {
const { data: users, isLoading, error } = useQuery(['users'], () =>
fetch('/api/users').then(res => res.json())
);
if (isLoading) return <div>Loading users...</div>;
if (error) return <div>Error fetching users: {error.message}</div>;
return (
<ul>
{users.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
This component fetches the user list and handles loading and error states cleanly.
Example 2: Adding a New User
Now, let’s add a form to create a new user and update the list:
import { useMutation, useQueryClient } from '@tanstack/react-query';
function AddUser() {
const queryClient = useQueryClient();
const mutation = useMutation(
newUser =>
fetch('/api/users', {
method: 'POST',
body: JSON.stringify(newUser),
headers: { 'Content-Type': 'application/json' },
}).then(res => res.json()),
{
onSuccess: () => {
queryClient.invalidateQueries(['users']);
},
}
);
const handleSubmit = e => {
e.preventDefault();
const newUser = { name: e.target.name.value };
mutation.mutate(newUser);
};
return (
<form onSubmit={handleSubmit}>
<input type="text" name="name" placeholder="Enter user name" />
<button type="submit">Add User</button>
</form>
);
}
Combine this with the UserList component, and you’ve got a fully functional CRUD interface!
References and Further Reading
TanStack Query offers much more than what we’ve covered here. To explore advanced features like optimistic updates or infinite queries, check out these resources:
- Official TanStack Query Documentation
- TanStack Query GitHub Repository
- React Query Tutorial by TkDodo
Conclusion
TanStack Query is a game-changer for managing asynchronous data in React. With its clean API, robust caching, and built-in state management, it takes the hassle out of data fetching and updates. By following the examples in this post, you can start integrating TanStack Query into your projects today.
Happy coding, and enjoy building faster, more reliable React applications!