React Query - Beyond Basics

React Query - Beyond Basics

Jun 21, 2021

Various articles over the internet have already discussed the benefits of using react-query. The ease of use of useQuery/useMutation i.e. basically one-liner to get access to loading, fetching or error state and the response data, has already been iterated over and over again. But the more advanced and niche features has hardly been discussed. So here I am, delving deeper into some of the feature of react-query world.

I will give an example of a simple todo list app, where a list of todos is shown. And when user wants to create a new todo, a modal form is opened. After the todo is successfully created, the todo list will be refetched. In the todo list, if user clicks on a todo, it would go to a new page, where corresponding detail of the todo will be shown.

1. onSuccess & onError

Both useQuery and useMutation supports various configuration options. Two of them are onSuccess and onError parameters that accepts a function. The functions can be specially helpful if we would like to perform some non data rendering logic. In the todo list example if we would like to throw a success or error message chips that aren't necessarily a component. (In case rendering a component is needed, we are better of with isSuccess or isError). Basically this can act like the callback function as .then that we use in api fetching. Better use case would be to dispatch some redux state. It can be done here too, without fumbling around useEffect.

import { message } from "antd";

const { isLoading, isError, mutateAsync } = useMutation(
  "todo/create",
  () => {
    return axios.post("http://localhost:4000/todo/create", task);
  },
  {
    onSuccess: (res) => {
      dispatch(setTaskList(res.data.task_list));
      message.success("New todo created");
    },
    onError: () => {
      message.error("Some error occured");
    },
  }
);

2. Query Invalidation

In out task app example, we have discussed that we would like to refetch the all task list on successful creation of a task. So here comes the magic of query invalidation. We have to use the previously discussed, onSuccess function. In the function we can use query invalidation to invalidate i.e to ask react-query to refetch one or more query/queries.

In our todo app, when our create todo is successful, we would invalidate the query that is fetching our all todos list.

// 📁 AllTodos.jsx
// in todo/all query we are fetching the list of all todos
const { isLoading, isError, data } = useQuery("todo/all", () => {
  return axios.get(`http://localhost:4000/todo/all`);
});

// 📁 CreateTodo.jsx
// now when todo/create is successful we are invalidating todo/all
// So that todo list is being fetching in create new todo creation
const { isLoading, isError, mutateAsync } = useMutation(
  "todo/create",
  () => {
    return axios.post("http://localhost:4000/todo/create", todo);
  },
  {
    onSuccess: () => {
      queryClient.invalidateQueries("todo/all");
    },
  }
);

3. Query Retries

This can be small one. But can come handy when situation demands. React query comes with some pre-configured default values corresponding to each configuration option. For example cache time is 5mins and stale time is 0. So one of the many is retry option. It's default value is 3. That is if a query is not successful in fetching the query in first attempt, it will continue trying 3 times before declaring isError to be true. In some cases, you might not want that behavior. You can always change that to some other number denoting the number of retries that you would like to happen. Another way is, retry also accepts true and false as value too. What does that mean? If retry is true then react query will fetch the query until it is successful and if it is false, then no retry happens after any unsuccessful attempt.

const { isLoading, isError, data } = useQuery(
  "todo/all",
  () => {
    return axios.get(`http://localhost:4000/todo/all`);
  },
  {
    retry: 1, // or `true` or `false` or e.g. 6
  }
);

All of this options can be changed per query basis. But you might want to declare your own configuration options for all the queries (unless you specify otherwise in some particular query). Then you should consider doing that in the query client.

4. Enabling query conditionally

In some cases, you might want a query to run only if some condition is met. useQuery and all of the other goodies of react-query, being hooks we can't use them directly in some if else statement as that would break the basic rule of react hooks. For these type of scenarios, react-query comes with an option called enabled. We can always hard code them to be true or false, but where it really shines is when a variable is passed. Now according to that variable value change the query would be enabled or disabled. How cool is that!

For example, in our todo app, when user goes to individual todo, the todo_id is passed as param in the url (using react-router or other routing library). And according to the todo_id the details are fetched. Now we would like to only fetch the query if param is not null. We can do it this way then -

const { id } = useParams(); // from react-router

const { isLoading, isError, data } = useQuery(
  ["todo/detail", id],
  () => {
    return axios.get(`http://localhost:4000/todo/detail/:${id}`);
  },
  {
    enabled: !!id,
  }
);

5. Custom hooks for Query

It is more of a personal opinion rather than being a react-query specific feature. So if you need to customize the behavior beyond the pre-configured options or need to access the onSuccess or onError options, very soon you might end up something like this. Some might prefer that you can easily see what is happening in the query right away. But if you ever need to access the same query across multiple components, you might want to make your own custom hook wrapped around the whole react-query logic. And I assure you it is not some high end jujutsu. If we consider previous example it would go some thing like this:

const useGetTodoById = (id) => {
  const { isLoading, isError, data } = useQuery(
    ["todo/detail", id],
    () => {
      return axios.get(`http://localhost:4000/todo/detail/:${id}`);
    },
    {
      enabled: !!id,
      retry: 1,
      onSuccess: () => {
        // invalidate some query
      },
      onError: () => {
        // do something else
      },
    }
  );
  export { isLoading, isError, data };
};
// in the component use it just like this
const { isLoading, isError, data } = useGetTodoById(id);

Some pro tips

  1. If you consider writing custom hooks, you might also consider declaring a variable where you simply stores that data or if you need status code for some reason then you can abstract it away too here and pass as single value and making the data that we need to map upon or take other actions. A well defined variable makes more sense than generic data.

    const { isLoading, isError, data } = useQuery(
        ["todo/detail", id],
        () => {
          return axios.get(`http://localhost:4000/todo/detail/:${id}`);
        },
        {
          enabled: !!id,
          retry: 1,
          onSuccess: () => {
            // invalidate some query
          },
          onError: () => {
            // do something else
          },
        }
      );
      const fetchedTodo = data.data.todo
      const fetchTodoStatus = data.status
      export { isLoading, isError, fetchedTodo, fetchTodoStatus }
    }
    
  2. In case of renaming data as something else, you can do it directly in react-query too. And not only data, you can rename isLoading or isError to something else too. It is especially needed if you need to access two or more queries in one component.

    const {
      isLoading: isAllTodoLoading,
      isError: isAllTodoError,
      data: allTodo,
    } = useQuery("todo/all", () => {
      return axios.post("http://localhost:4000/todo/all", todo);
    });
    
  3. You can use api routes as query names. It will make a lot of sense if you abstract away your query function elsewhere. It might also help if you find that you need to access that particular query that you think you have used in some component already. And now you would to like to use it in some other component. Naming in that way, you will easily get away from finding what was the name of that particular query. After all the query name is crucial to utilize the benefit of react-query fruitfully. I have followed this throughout the article

  4. If using custom hooks you can keep them in separate files according to their main route. And keeping them all under services folder itself, that you might be already doing with axios.

    src
       - components
       - pages
       - services
           - todo.js
           - user.js
    

It is not meant to be something exhaustive. Just a few, that I am using daily.

Some of the last part has been purely personal hacks, that you might not agree upon or I might be actually doing wrong. Please feel free to tell me so.