DEV Community

Cover image for React Query: Making Your Server-State Problems Disappear (Like Magic)
Sathish
Sathish

Posted on

React Query: Making Your Server-State Problems Disappear (Like Magic)

Managing server state in React is like trying to juggle flaming swords—possible, but unnecessarily difficult. You could wrestle with useEffect and useState to handle data fetching, caching, and synchronization, but that approach often leads to bloated components and messy code. Enter React Query, the superhero you didn’t know you needed! It’s like your personal assistant for managing server state—fetching, caching, and syncing all while sipping coffee.

In this blog post, we’ll explore what React Query is, how it works, and why it makes managing server-state easy (and fun!). We’ll also cover the magical powers of cache invalidation and provide some code examples to make it all click.


What is React Query?

React Query is a data-fetching and state management library for React that abstracts away all the headache-inducing aspects of working with asynchronous data. It comes with out-of-the-box features like caching, background updates, and synchronization between tabs. Basically, it’s like giving your React app superpowers.

It helps you focus on building features rather than obsessing over when or how your data will be fetched. It works great with REST APIs, GraphQL, or any other asynchronous data source.

Why Should You Care?

If you're tired of writing complex logic to handle server state, handling loading states, refetching data, or even canceling network requests, then React Query is your solution. It makes managing data as simple as calling a function and keeps the rest of your app reactive, fast, and performant.


Let’s Get Our Hands Dirty: Installing React Query

First things first, let’s add React Query to your project:

npm install react-query
Enter fullscreen mode Exit fullscreen mode

Next, wrap your app in a QueryClientProvider—this is the magical sauce that makes everything work.

import { QueryClient, QueryClientProvider } from 'react-query';

const queryClient = new QueryClient();

function App() {
  return (
    <QueryClientProvider client={queryClient}>
      <YourAwesomeApp />
    </QueryClientProvider>
  );
}

export default App;
Enter fullscreen mode Exit fullscreen mode

Now you’re ready to start querying data like a pro!


Basic Usage: Fetching Data the Easy Way

Let’s start with the simplest example: fetching data with React Query. Instead of painstakingly managing your state and side effects, you’ll just use the useQuery hook.

import { useQuery } from 'react-query';
import axios from 'axios';

const fetchTodos = async () => {
  const { data } = await axios.get('/api/todos');
  return data;
};

function Todos() {
  const { data, error, isLoading } = useQuery('todos', fetchTodos);

  if (isLoading) return <p>Loading...</p>;
  if (error) return <p>Oh no, something went wrong!</p>;

  return (
    <ul>
      {data.map(todo => (
        <li key={todo.id}>{todo.title}</li>
      ))}
    </ul>
  );
}
Enter fullscreen mode Exit fullscreen mode

Here’s what’s happening:

  • The useQuery hook takes two arguments: a unique key ('todos') and a function that fetches the data (fetchTodos).
  • React Query automatically handles loading states, caching, and error handling for you, so you can focus on what matters: building your app.

The Cache: React Query’s Hidden Superpower

The coolest feature of React Query is its built-in caching. When data is fetched with useQuery, it’s stored in the cache for future use. This means that subsequent requests for the same data will be served instantly from the cache (no need to hit the server again unless necessary).

Here’s how it works:

  • Stale-While-Revalidate: By default, React Query treats cached data as stale, but usable. It will immediately return the stale data while re-fetching in the background.
  • Automatic Garbage Collection: Data in the cache will stick around until it’s unused for a certain period (default is 5 minutes), after which it gets cleaned up to save memory.

For instance, if you switch between tabs that both use the same data, you won’t be hitting the server again and again—it just serves you the cached version until the background refetch finishes.


Cache Invalidation: Making Sure Your Data Is Fresh as a Daisy

Ah, cache invalidation—the classic computer science problem! Luckily, React Query makes it much easier to handle.

When you mutate data (for instance, by adding a new todo), you need to tell React Query to invalidate the cache so it knows to refetch the data. You can do this using the useMutation hook and queryClient.invalidateQueries function.

import { useMutation, useQueryClient } from 'react-query';
import axios from 'axios';

const addTodo = async (newTodo) => {
  const { data } = await axios.post('/api/todos', newTodo);
  return data;
};

function AddTodo() {
  const queryClient = useQueryClient();

  const mutation = useMutation(addTodo, {
    onSuccess: () => {
      // Invalidate the todos cache after adding a new one
      queryClient.invalidateQueries('todos');
    },
  });

  const handleAddTodo = () => {
    mutation.mutate({ title: 'Learn React Query' });
  };

  return (
    <button onClick={handleAddTodo}>
      {mutation.isLoading ? 'Adding...' : 'Add Todo'}
    </button>
  );
}
Enter fullscreen mode Exit fullscreen mode

Here’s what’s happening:

  • We’re using useMutation to handle the server call that adds a new todo.
  • After the mutation succeeds, we invalidate the todos cache using queryClient.invalidateQueries('todos').
  • This triggers a re-fetch of the todos, ensuring that your UI reflects the most up-to-date data.

Cache invalidation is crucial in ensuring that your app doesn't serve outdated data to your users, keeping the UI always in sync with the backend.


Optimistic Updates: Because Waiting Is Overrated

Nobody likes waiting. Optimistic updates let you instantly update the UI before the server responds, giving users a smoother experience. React Query makes this super easy to implement.

const mutation = useMutation(addTodo, {
  onMutate: async (newTodo) => {
    // Cancel any outgoing queries for 'todos'
    await queryClient.cancelQueries('todos');

    // Snapshot the previous todos
    const previousTodos = queryClient.getQueryData('todos');

    // Optimistically update to the new value
    queryClient.setQueryData('todos', (old) => [...old, newTodo]);

    // Return context so we can rollback if the mutation fails
    return { previousTodos };
  },
  onError: (err, newTodo, context) => {
    // Rollback the cache if the mutation fails
    queryClient.setQueryData('todos', context.previousTodos);
  },
  onSettled: () => {
    // Always refetch after mutation
    queryClient.invalidateQueries('todos');
  },
});
Enter fullscreen mode Exit fullscreen mode

Now your UI will update instantly when a new todo is added, and the mutation’s outcome will either commit the change or roll it back. Users will thank you for the snappy experience!


Wrapping Up

React Query isn’t just another data-fetching library—it’s a productivity booster, a headache eliminator, and a performance optimizer all in one. By handling server-state with features like caching, cache invalidation, and optimistic updates, React Query takes the burden of managing data out of your hands.

So, if you’re tired of fighting with useEffect, give React Query a try. It might just feel like magic.

Now go forth and fetch data, invalidate caches, and never worry about stale state again! Your future self will thank you.


Peace out! 👋🏻

Top comments (0)