DEV Community

Cover image for From useEffect to React Query: Modernizing your data management in react
Diona Rodrigues
Diona Rodrigues

Posted on • Originally published at dionarodrigues.dev

From useEffect to React Query: Modernizing your data management in react

In the modern web development landscape, building fast, responsive applications that efficiently manage server-side data is more crucial than ever. Traditional methods of fetching data in React, especially using useEffect, can often lead to complex state management, repetitive code, and performance issues. With React Query, developers can take advantage of features like automatic caching, background fetching, and built-in mutation support, all while minimising boilerplate code.

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 the useQuery 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
Enter fullscreen mode Exit fullscreen mode

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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();
};
Enter fullscreen mode Exit fullscreen mode
  • Key (['todos']): Each useQuery 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();
};
Enter fullscreen mode Exit fullscreen mode
  • 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
Enter fullscreen mode Exit fullscreen mode

Then, add <ReactQueryDevtools /> to your app:

import { ReactQueryDevtools } from '@tanstack/react-query-devtools';

function App() {
  return (
    <QueryClientProvider client={queryClient}>
      <YourComponent />
      <ReactQueryDevtools initialIsOpen={false} />
    </QueryClientProvider>
  );
}
Enter fullscreen mode Exit fullscreen mode

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.

Top comments (1)

Collapse
 
console_x profile image
F. Güngör

and it is also very simple paginating data with useQuery

const fetchPosts = async (page = 1) => {
const response = await axios.get(/api/posts?page=${page});
return response.data;
};

const { data, isLoading, isError } = useQuery(
["posts", page],
() => fetchPosts(page),
{
keepPreviousData: true,
},
);