Usequery simplifies API Requests with React Query
Usequery simplifies API Requests with React Query

In this post you will learn what is React Query and why does it make a lot of sense to use this library inside your React project.

Official Website

As you can see I'm in the official website of React Query which is called TanStack. Here we are talking about library of version 4. Inside TanStack we have several libraries like Table, Router, Charts and others.

So the first question is what is React Query.

React Query is a library for data fetching, caching and synchronising data with the component state.

Typically inside our application we have a component

const Users = () => {
  const [inputValue, setInputValue] = useState("");
  const [users, setUsers] = useState([])

  const addUser = () => {
    createUser().then(user => {
      setUsers([...users, newUser])
    })
    setInputValue('')
  }

  useEffect(() => {
    getUsers().then(response => {
      setUsers(response.data)
    })
  }, []);

  return (
    <div>
      <div>Total: {users.length}</div>
      <div>
        <input
          type="text"
          value={inputValue}
          onChange={(e) => setInputValue(e.target.value)}
        />
        <button onClick={addUser}>Add user</button>
      </div>
      <div>
        {users.map((user, index) => (
          <div key={index}>{user.name}</div>
        ))}
      </div>
    </div>
  );
};

Here we have a state for our users and when we fetch data on initialize we must fulfill our state. After this later we can create new user and update users state.

This is totally fine and this is how we write code in React out of the box. What is the problem?

We use useState + useEffect everywhere just to fetch data and set a state.

React Query implements for us exactly this logic.

const Foo = () => {
  const {isLoading, error, data} = useQuery(['users'], getUsers)
}

This is an example how we can fetch data from API and get directly local properties by using useQuery hook.

React Query eliminates the whole bunch of logic that we must write every single time to synchronize API call with local state.

If you are still not sure what is React Query you might think about it like for example Apollo Client or RTK Query if you are familiar with them.

How can I use React Query?

This is why let's look on the real example and refactor it to React Query.

Project

As you can see this is a project that we want to refactor. We fetch a list of users, we can type a new user and click add, which will send an API request and create new user.

Which actually means we have 2 real requests that we want to refactor. This is exactly what you saw in the code above that I showed you.

Our first step is to install 2 additioanl packages.

npm install @tanstack/react-query
npm install @tanstack/react-query-devtools

We install a library itself and a tool which will help us to debug react-query later.

Now we must initialize React Query correctly.

import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";

const queryClient = new QueryClient();
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
  <React.StrictMode>
    <QueryClientProvider client={queryClient}>
      <App />
      <ReactQueryDevtools initialIsOpen />
    </QueryClientProvider>
  </React.StrictMode>
);

As you can see here we must wrap our App with QueryClientProvider. This is a component of React Query which will allow us to make requests. Additionally we must create queryClient and pass it inside.

Also we imported ReactQueryDevtools component and rendered it after our App. It is not mandatory but will help us tremendously in debugging.

useQuery

Now we can jump back and change our Users component.

import { useQuery } from "@tanstack/react-query";
const Users = () => {
  const { data: users = [] } = useQuery(["users"], getUsers);
}

This is a single liner that we need in order to fetch users from API. Inside useQuery we must provide a path that we want to cache. In our case it's [users] and a promise to get our data. We also removed useEffect from the component as well as useState for users because we don't need them anymore.

As you can see we get back a data property. As we have users inside our component we rename it and default to an empty array. Additionally we can get isLoading, isError, error is we need them.

React query devtools

When we open browser we directly see React Query Devtools. Here we also see all cached paths. In our case only [users]. We can click on it and check what data is cached. Additionally on the page we successfully fetched data from the API and our component successfully rendered it.

Which actually means that instead of writing useEffect and managing state on our own we leveraged this library to implement getting data, caching and updating our state.

useMutation

Now we must implement creating of our user. And for sure we want to use React Query for this. For this we must use useMutation. Mutations allow us to make changes to the API, for example create, update or delete.

import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";

const Users = () => {
  ...
  const createUserMutation = useMutation(createUser, {
    onSuccess: () => {
      console.log('onSuccess')
    },
  });
  const [inputValue, setInputValue] = useState("");
  const addUser = () => {
    createUserMutation.mutate(inputValue);
    setInputValue("");
  };
  ...
}

Here we imported useMutation and useQueryClient. After this we created a createUserMutation by calling useMutation. Inside we provide a promise and options. The only option that we set is onSuccess which is just a callback after correct mutation.

Now we can call createUserMutation.mutate anywhere which will call an API.

As you can see in browser we can add a user and our API request is send correctly in network. The only problem is that our list in not being update at all.

Cache invalidation

Mutations don't update our state.

We have our cached path [users] which was never updated. Our mutation doesn't change this cached data. What is the solution for this? We must invalidate our cache.

For this we can create queryClient property and call queryClient.invalidateQueries inside our success callback.

const Users = () => {
  const queryClient = useQueryClient();
  const { data: users = [] } = useQuery(["users"], getUsers);
  const createUserMutation = useMutation(createUser, {
    onSuccess: () => {
      queryClient.invalidateQueries(["users"]);
    },
  });
  const addUser = () => {
    createUserMutation.mutate(inputValue);
    setInputValue("");
  };

As you can see in browser our list is refetched every single time when we add a user. It happens because we invalidate the query [users] which triggers refetching of the data.

As you can see the main goal of React Query is to stop synchronising server data with client data plus we get caching and invalidation out of the box.

And actually if you are interested to learn how to create a full stack application with Socket IO, Typescript, Node, MongoDB and Angular make sure to check my Build Fullstack Trello clone: WebSocket, Socket IO course.

📚 Source code of what we've done