ReactJS

Simple Server State management with React Query

Simple Server State management with React Query

React Query is a library that provides hooks for fetching, caching, and updating data in React applications without touching any global state. It helps manage server state which is asynchronous and often not included in the typical state management libraries like Redux or Context API.

Setting Up React Query

To get started, you need to install React Query:

npm install react-query

Once installed, you'll wrap your application with the QueryClientProvider and create a QueryClient instance:

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

const queryClient = new QueryClient();

function App() {
  return (
    <QueryClientProvider client={queryClient}>
      {/* The rest of your application */}
    </QueryClientProvider>
  );
}

export default App;

Fetching Data with useQuery

Let's fetch some data using the useQuery hook. We will use a simple example where we fetch a list of todos from a JSON placeholder API:

import { useQuery } from '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, error, isLoading } = useQuery('todos', fetchTodos);

  if (isLoading) return 'Loading...';
  if (error) return 'An error has occurred: ' + error.message;

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

In this example, useQuery is called with two arguments: a unique key for the query ('todos') and a function (fetchTodos) that returns a promise which resolves to the data.

Mutations with useMutation

For creating or updating data, React Query provides the useMutation hook. Here's how you might use it to add a new todo:

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

const addTodo = async (newTodo) => {
  const response = await fetch('https://jsonplaceholder.typicode.com/todos', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
    },
    body: JSON.stringify(newTodo),
  });
  if (!response.ok) {
    throw new Error('Network response was not ok');
  }
  return response.json();
};

function AddTodo() {
  const queryClient = useQueryClient();
  const mutation = useMutation(addTodo, {
    onSuccess: () => {
      // Invalidate and refetch
      queryClient.invalidateQueries('todos');
    },
  });

  const handleSubmit = (event) => {
    event.preventDefault();
    const newTodo = { title: 'Do laundry', completed: false };
    mutation.mutate(newTodo);
  };

  return (
    <form onSubmit={handleSubmit}>
      <button type="submit">Add Todo</button>
    </form>
  );
}

When the addTodo mutation is successful, we call queryClient.invalidateQueries() to refetch the todos and update the UI with the new todo.

Optimistic Updates

React Query shines with optimistic updates, which allow the UI to update immediately before the mutation is confirmed by the server:

function ToggleTodo({ todo }) {
  const queryClient = useQueryClient();
  const mutation = useMutation(
    newTodo => fetch(`https://jsonplaceholder.typicode.com/todos/${newTodo.id}`, {
      method: 'PUT',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify(newTodo),
    }),
    {
      // When mutate is called:
      onMutate: async newTodo => {
        await queryClient.cancelQueries('todos');
        const previousTodos = queryClient.getQueryData('todos');
        queryClient.setQueryData('todos', old =>
          old.map(item => item.id === newTodo.id ? { ...item, ...newTodo } : item)
        );
        return { previousTodos };
      },
      // If the mutation fails, roll back to the previous value
      onError: (err, newTodo, context) => {
        queryClient.setQueryData('todos', context.previousTodos);
      },
      // Always refetch after error or success:
      onSettled: () => {
        queryClient.invalidateQueries('todos');
      },
    }
  );

  const toggle = () => {
    mutation.mutate({ ...todo, completed: !todo.completed });
  };

  return (
    <li onClick={toggle} style={{ textDecoration: todo.completed ? 'line-through' : 'none' }}>
      {todo.title}
    </li>
  );
}

In this code, onMutate performs the optimistic update by immediately updating the todos query data. If the mutation fails, onError rolls back to the previous state.

Conclusion

React Query offers a robust set of tools for managing server state in React applications. By leveraging its capabilities for data fetching, mutations, and optimistic updates, developers can greatly simplify state management and improve the user experience with less loading time and more immediate responses to user actions. As a result, React Query has become an essential part of the modern React developer's toolkit.

A blog for self-taught engineers

Сommunity is filled with like-minded individuals who are passionate about learning and growing as engineers.