Using React Query to solve state management in your React app
Tags: javascript, typescript, react, redux
Published: 2021-10-15 16:16:31

I recently got the opportunity (and the privilege!) of starting a greenfield project at my current company, involving an frontend application for an internal tool. The devs involved had the chance to choose the tech stack that we considered convenient, and we collaboratively wrote an RFC (Request for Comment) and presented it to the rest of the company to open up our choices to discussion.

One of the main points that came up -after settling for React, the lingua franca framework at our company- is how we would handle state management. Our main application uses Redux, but many other alternatives were brought up: MobX, using native hooks (a combination of useReducer + useContext), using Redux plus Redux Toolkit. I even got to know and proposed Recoil, a super exciting project -and definitely a library with one of the best presentation videos I've seen so far.

But our Staff Engineer Zac came up with a different idea. Enter React-Query.

### React Query's novel approach to state management

"I haven't used it yet, but I love the different approach it takes to handle state within an application. It basically splits server side state out from client side state and automates a lot of stuff like re-fetching and caching", explained Zac.

The idea clicked with me instantly: most of the state that React apps keep in their store is just a reflection of data persisted remotely somewhere (a User, a list of Posts, Comments, or To-Dos, for e.g.). Only a minor part of it is client-side only, and it almost always corresponds to UI/UX information, like if whether a modal is open, a sidebar expanded, etc.

So the idea behind React Query is taking that majority of server-side state and handling it completely: fetching, re-fetching, storing, cacheing, updating and memoizing it in an all-in-one solution. This separations helps to reduce a lot of the boilerplate that inevitably arises with other combined client- and server-side state management tools (such as Redux).

The library also offers some advanced features like "optimistic updates", in which the library assumes an update to the data will be successful before actually receiving a response from the back-end, and allows to easily roll it back if it fails, making the app seem responsive as a breeze to the user.

Promising enough. We decided that we'd go with it during the Proof of Concept phase of the app and started writing code.

### Writing the PoC with create-react-app

As we started working on the frontend way before our backend team had availability to build the services that would provide the data needed for the app, we decided to go forward with setting up our project with **create-react-app with its TypeScript template** and React Query using JSONPlaceholder as a fake API.

So, let's write some code!

First, we created a new app with create-react-app's CLI and installed react-query:

```bash
npx create-react-app react-query-demo --template=typescript
cd react-query-demo
yarn add react-query
```

The `App.tsx` component that comes by default looks so:

```tsx
import React from 'react';
import logo from './logo.svg';
import './App.css';

function App() {
  return (
    <div className="App">
      <header className="App-header">
        <img src={logo} className="App-logo" alt="logo" />
        <p>
          Edit <code>src/App.tsx</code> and save to reload.
        </p>
        <a
          className="App-link"
          href="https://reactjs.org"
          target="_blank"
          rel="noopener noreferrer"
        >
          Learn React
        </a>
      </header>
    </div>
  );
}

export default App;
```
Following React-Query's excellent documentation, we first modified that file by wrapping our app with the `QueryClientProvider` that comes included in the library and created a new component `UserList` where we will fetch our `Users` from our fake API.

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

import './App.css';
import { UserList } from "./UserList"

const queryClient = new QueryClient();

const App = () => {
  return (
    <QueryClientProvider client={queryClient}>
      <div className="App">
        <header className="App-header">
          <h1>React Query Demo</h1>
        </header>
        <UserList />
      </div>
    </QueryClientProvider>
  );
}

export default App;
```

Let's unpack our changes in that component. We first instantiated a new `queryClient` instance with the `QueryClient` constructor that React Query provides. We then passed that instance to the `QueryClientProvider` with which we wrapped our whole app. This provides a context for our cached data and allows all components wrapped in it to use the querying and updating hooks that the library provides.

We also cleaned up our component a bit, changed the title and added our newly created `UserList` component, where things start to get real interesting. Let's take a look at it:

```tsx
import React from "react";
import { useQuery } from "react-query";

interface User {
  id: number;
  name: string;
  username: string;
  email: string;
}

const USERS_KEY = "users";

export const UserList = () => {
  const {
    isLoading,
    data: users,
    isError,
    error
  } = useQuery<User[], Error>(
    USERS_KEY,
    () => fetch('https://jsonplaceholder.typicode.com/users')
  ).then(res => {
    if (!res.ok) {
      throw new Error('Network response failed')
    }
    return res.json()
  }));

  if (isLoading) {
    return <span>Loading...</span>;
  }

  if (isError) {
    return <span>Error: {error?.message}</span>;
  }

  return (
    <ul>
      {users?.map(({ name, username, email }: User) => (
        <div className="userRow">
          <h3>{name}</h3>
          <p>Username: {username}</p>
          <p>{email}</p>
        </div>
      ))}
    </ul>
  );
};
```

A lot more going on here, but this is where the juice of React Query really shows. Let's unpack all of it.

As we are using JSONPlaceholder's fake API to fetch a list of users, we first create the `User` interface, a simplified version [based on the schema provided](https://jsonplaceholder.typicode.com/users) by the site. In our case, we will fetch an array of Users and display it to the user.

Within our component, we make use of the main tool that React-Query provides: the `useQuery` hook. The hook takes two arguments:

- a **unique query key** which is used internally by React Query for "refetching, caching and sharing queries across the application". The library will store the data under this key, in a similar way as data for different reducers are kept under a key name in Redux. In our case, we set it to the `USERS_KEY` constant, which is simply a string of value  `"users"`.
- a **function that returns a promise** that resolves the data, or throws an error.

The second argument highlights one of library's great advantages: since React Query's fetching mechanisms are agnostically built on **Promises**, it can be used with literally any asynchronous data fetching client, such as Axios, the native **fetch** and even GraphQL! (we'll expand on how to do this in a subsequent post).

For now, we are using **fetch** to request a list of `User`s from the [`https://jsonplaceholder.typicode.com/users`](https://jsonplaceholder.typicode.com/users) endpoint. Notice that, when using fetch, we must also manually check if the request is successful, and throw an error is if it not, as the second parameter expects the fetcher function to throw when an error occurs, and fetch does not automatically do this. This wouldn't be necessary if we were using Axios, for example.

**Note for TypeScript users:** React Query allows you to provide, via Generics, the result and error types of its hooks. This is especially useful when creating your own custom hooks, for example:

```tsx
const useGetUsers = () => {
   return useQuery<User[], Error>('users', fetchUsers)
}
```

The `useQuery` hook returns an object, from where we have destructured three properties:

- **isLoading:** a boolean that indicates that the query has no data and is currently fetching.
- **data:** the property that contains the data that the Promise resolved to if the request was successful. In our case, it is an array of `User`s, and we aliased it to the variable name `users` just for clarity.
- **isError:** a boolean that indicated that the query encountered an error.
- **error:** a property that contains the error thrown if the query is in an **isError** state.

We can use these properties to decide what the component should render, depending on the state of the query. We first check if it is in an `isLoading` state, and render a message accordingly. We then check if it an error occured via the `isError` boolean, and display the error under `error.message` . Finally, we can safely assume that query is in `isSuccess` state and render our list of users.

**Updating our server-side state**

So far so good, but what about when we need to create, update or delete our remotely stored data? React Query solves this issue with the concept of **Mutations** and the `useMutation` hook.

Let's create another component `CreateUser` that renders a button which POSTs a new user to the API when clicking on it, and add it to our `App` .

```tsx
[...]

const App = () => {
  return (
    <QueryClientProvider client={queryClient}>
      <div className="App">
        <header className="App-header">
          <h1>React Query Demo</h1>
        </header>
        <UserList />
				<CreateUser />
      </div>
    </QueryClientProvider>
  );
}

export default App;
```

This time, we'll use Axios as our HTTP client to highlight React Query's versatility. Let's install it first:

```bash
yarn add axios
```

And let's write the code for our new component:

```tsx
import React from "react";
import axios from "axios";
import { useMutation, useQueryClient } from "react-query";

import { User, USERS_KEY } from "./UserList";

const exampleUser = {
  name: "John Doe",
  email: "johndoe@gmail.com",
  username: "johndoe1990"
} as User;

const postUser = (user: User) => axios
    .post<User>('https://jsonplaceholder.typicode.com/users', user);

export const CreateUser = () => {
  const queryClient = useQueryClient();
  const { isLoading, mutate } = useMutation(postUser, {
    onSuccess: () => {
      queryClient.invalidateQueries(USERS_KEY);
    }
  });
  const onButtonClick = () => mutate(exampleUser);

  if (isLoading) {
    return <p>Creating User...</p>;
  }

  return <button onClick={onButtonClick}>Click to post a new user</button>;
};
```

Let's go over what's going on here.

Firstly, we create a hard-coded `exampleUser` to POST into the fake API when the user clicks on the button. We also create our required **mutation function**, `postUser` **,** which returns a Promise of an Axios Response of a POST call to our `/users` endpoint, passing in as data the argument of our mutation function.

Inside our component, we'll first initialize an instance of `queryClient` with the `useQueryClient` hook, also provided by React Query. This is the same instance created in `App.tsx` and provided by our `QueryClientProvider`. We'll make use of it in a second.

And now we make use of the `useMutation` hook, also provided by React Query, which takes two arguments:

- a required **mutation function** that performs an asynchronous task and returns a Promise. In our case, we pass in the already defined `postUser` function.
- an object with multiple properties:
    - an optional **mutation key**, in a similar way as we defined a **query key**, to be used internally. We don't need to set one for this example.
    - an optional **onSuccess** callback, that fires when the mutation is successful and is passed the mutation result.
    - an optional **onError** callback that will fires if the mutation fails, and will get the error passed.
    - an optional **onMutate** callback, that fires before the mutation function is fired and is passed the same variables the mutation function would receive. This allows us to do optimistic updates: that is, we can early update a resource (and our UI) in the hope that the mutation succeeds and give our application a "synchronous feel". The value returned from this function will be passed to the **onError** and the **onSettled** callbacks, so that we can rollback our optimistic update in case the mutations fails.
    - more config properties can be found [in the docs.](https://react-query.tanstack.com/reference/useMutation)

In our example, we are only setting up an `onSuccess` callback whose job is to invalidate our `"users"` query, by calling the `invalidateQueries` utility provided by our `queryClient` and passing our `USERS_KEY` as argument to it. By invalidating this query key in our cache after the mutation is successful, we indicate React Query that the data under that key is outdated and it should refetch it. Thus, the library will automatically re-query our `/users` endpoint, and will bring back our updated `Users` list.

The `useMutation` hook returns an object from where we destructure two properties:

- **mutate:** a function that can be called passing variables to it as parameters and will trigger the mutation defined in the **mutation function** defined in the hook.
- **isLoading:** a boolean that indicates that the mutation is still pending.

Our `CreateUser` component will use **mutate** when clicking on a button, so we create an `onButtonClick` function that fires `mutate` passing it our hardcoded `exampleUser` as an argument. We then use our `isLoading` flag to show an appropriate message to the user when the mutation is pending, or display the button with a call-to-action otherwise.

And that's it! Try playing around with it in the app. One note though, if you check the Network tab on the DevTools, you'll see that, because we are using a fake API, the **POST** call to add a user will indeed succeed with status code `201` . However, when React Query refetches the data, (the subsequent **GET** call that gets fired after we invalidate our query key) the new user won't be in the returned data there as **JSONPlaceholder** will simply ignore any new data added to it. However, on a real API you would see the `User` you just posted.

**Wrapping it up**

We've seen how React Query can handle the fetching of the data, cacheing and updating (via refetching) and provides us with a user-friendly hook to handle the returned data and rendering. Through the use of its **query keys**, and its simple API, the library can replace a fully fledged state management solution, taking out of your hands the responsibility of writing hundred of lines of boilerplate code, and adding great functionality that you'd otherwise have to write from scratch.

Check out [the finished demo app](https://react-query-demo-zeta.vercel.app/) and [clone the repo](https://github.com/jpdjere/react-query-demo) to play around with the code. And don't forget to give [the official documentation](https://react-query.tanstack.com/overview) a read.

Thanks for reading!