In this article, we'll explore the core concepts of React Query, demonstrate how to integrate it into our projects, and highlight its powerful features that can significantly improve your development workflow. Get ready to transform the way you fetch, cache, and synchronise data in your React applications!
Why should we stop fetching data using React Effects
While using React's useEffect
for data fetching is perfectly valid, there are several reasons to consider moving away from it in favor of a dedicated data fetching library like React Query (even the React documentation suggests using React Query for data fetching).
Some downsides of using Effects for data fetching:
- Performance Issues: “network waterfalls” and unnecessary re-fetching are some of the common issues when using React
useEffect
and can lead to a very bad user experience. - Lack of Built-in Caching:
useEffect
does not provide caching out of the box, which causes repeated network requests and complex solutions to manage it. - Complexity in State Management: managing loading, error, and data states manually with
useEffect
can lead to cumbersome and error-prone code, especially as the application scales. - Effects don’t run on server: data will may not be available when the component initially renders.
How React Query works
React Query is a powerful library designed to simplify data fetching and state management in React applications. Here’s a breakdown of how React Query works:
1. Querying Data
useQuery
hook: The core of React Query is theuseQuery
hook. This hook allows you to fetch data from a server and automatically manage its state (loading, error, data…).- Query key: Each query is identified by a unique key (one or more strings in an array). This key helps React Query cache and manage the data efficiently.
2. Caching
- Automatic caching: When data is fetched, React Query caches it. If the same query is made again, it retrieves data from the cache instead of making a new network request.
- Stale data management: You can define how long data should be considered fresh (not stale). After this period, React Query will refetch the data in the background.
3. Background Refetching
React Query automatically refetches data in several scenarios to keep the data fresh and in sync. Here are the main situations when this happens:
- Mounting of components: When a component mounts, if the data is considered stale.
- Window focus: Whenever the window regains focus, such as when a user switches between tabs or windows and returns to the one containing your application.
- Network reconnection: If the network connection is lost and later restored.
4. Data Mutations
- useMutation Hook: it refers to the process of creating, updating, or deleting data on the server. Unlike querying, which retrieves data,
useMutation
is used to modify data and manage side effects associated with it. - Optimistic updates: when a user performs an action that will mutate data, the UI is updated right away to reflect the anticipated outcome of that action, enhancing user experience.
5. Query Invalidation
- React Query allows you to mark a cached query as "stale" so that it will be refetched the next time it's accessed. This is essential for ensuring that the UI reflects the most up-to-date data from the server after certain actions, such as mutations or user interactions.
6. Automatic Garbage Collection
- React Query automatically removes queries from its cache when they are no longer being used and have been inactive for a certain period. This process helps prevent memory leaks and ensures that only relevant data stays in the cache.
7. DevTools
- React Query DevTools is an optional, user-friendly tool for React Query that helps us, developers, debug and monitor queries, mutations, and cache state. It provides a visual interface to inspect the details of our queries and see how React Query manages their lifecycle.
8. Server-Side Rendering (SSR)
- React Query supports Server-Side Rendering (SSR), which helps in pre-fetching data on the server before sending it to the client. This makes initial page loads faster and can improve SEO by serving a fully rendered page to search engines.
How to implement React Query
Here’s a step-by-step guide on how to use React Query to manage server data fetching, caching, updating, and synchronization in a React app.
Step 1: Install React Query
First, add React Query to your project:
npm install @tanstack/react-query
Step 2: Setup QueryClientProvider
To configure React Query, wrap your app in a QueryClientProvider
. This provider uses a QueryClient
instance, which manages caching, background fetching, and updates.
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
const queryClient = new QueryClient();
function App() {
return (
<QueryClientProvider client={queryClient}>
<YourComponent />
</QueryClientProvider>
);
}
Step 3: Fetch Data with useQuery
The useQuery
hook fetches data from an API, automatically caching it and handling states like loading and errors.
import { useQuery } from '@tanstack/react-query';
function YourComponent() {
const { data, error, isLoading } = useQuery(['todos'], fetchTodos);
if (isLoading) return <p>Loading...</p>;
if (error) return <p>Error: {error.message}</p>;
return (
<div>
{data.map((todo) => (
<p key={todo.id}>{todo.title}</p>
))}
</div>
);
}
// Sample fetch function
const fetchTodos = async () => {
const response = await fetch('/api/todos');
return response.json();
};
- Key
(['todos'])
: EachuseQuery
requires a unique key to identify and cache data. - Query Function (
fetchTodos
): This async function fetches the data you need from an API.
Step 4: Handle Data Mutations with useMutation
The useMutation
hook is used to create, update, or delete data. Once a mutation is successful, you can use query invalidation to refetch relevant data and keep your app’s state up to date.
import { useMutation, useQueryClient } from '@tanstack/react-query';
function TodoAdder() {
const queryClient = useQueryClient();
const addTodoMutation = useMutation(addTodo, {
onSuccess: () => {
queryClient.invalidateQueries(['todos']);
},
});
return (
<button onClick={() => addTodoMutation.mutate({ title: 'New Todo' })}>
Add Todo
</button>
);
}
const addTodo = async (newTodo) => {
const response = await fetch('/api/todos', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(newTodo),
});
return response.json();
};
- Mutation Function (
addTodo
): This async function modifies the server state. onSuccess
: After a mutation, this callback invalidates the['todos']
query, refetching and updating the data to show the newly added todo.
Step 5: Optional DevTools for Debugging
React Query DevTools can help you monitor queries, cache status, and more during development:
npm install @tanstack/react-query-devtools
Then, add <ReactQueryDevtools />
to your app:
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
function App() {
return (
<QueryClientProvider client={queryClient}>
<YourComponent />
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
);
}
Conclusion
Replacing useEffect
with React Query for data fetching and side effects is a great recommendation for building modern React applications.
React Query transforms the way you handle server-side data in React apps, providing a more declarative approach that simplifies complex state management. By leveraging its powerful features like caching, background synchronization, and query invalidation, you can create highly responsive and performant applications. And last, but not least, integrating React Query DevTools makes it easy to monitor and debug, ensuring that your app’s data flow is smooth and transparent.
Whether you’re building a simple single-page app or a complex data-heavy application, React Query empowers you to build faster, smarter, and more user-friendly apps with less effort.