skip to Main Content

I’m using react-query in my React app to fetch and mutate data. When I click a button to mutate the data, I need the button to be disabled and spinner to appear until the new mutated data is returned.

To achieve this, I’m using the isLoading and isMutating flags provided by useMutation and useQuery hooks.

  const queryClient = useQueryClient();
  const {
    isLoading,
    error,
    data: todos,
  } = useQuery<Todo[]>("todos", fetchTodos);
  const { mutate, isLoading: isMutating } = useMutation(addTodo, {
    onSuccess: () => {
      queryClient.invalidateQueries("todos");
    },
  });

  const handleAddTodo = () => {
    if (!todos) return;
    mutate({ id: todos.length + 1, title: "New Todo", completed: false });
  };

  if (isLoading) return <div>Loading...</div>;
  if (error) return <div>Error fetching todos.</div>;
  if (!todos) return <div>Unexpected missing data</div>;

  return (
    <>
      <button disabled={isMutating} onClick={handleAddTodo}>
        Add Todo
      </button>
      <ul>
        {todos.map((todo) => (
          <li key={todo.id}>
            {todo.completed ? "☑" : "☐"} - {todo.title}
          </li>
        ))}
      </ul>
    </>
  );

When I click on the button to save data, for a brief moment, "Loading…" appears, then "loading" disappears, but old data are still displayed. Then again "loading" appears, and new data appear after that.

  • clicked button
  • a mutation request is sent
  • isMutating is true, button is disabled
  • a mutation request is completed, cache is invalidated
  • isMutating is false, button is enabled
  • a query request is sent, isLoading is still false, old data is displayed
  • a query request finishes, new data appears

How do I disable the button until the new data comes in?

2

Answers


  1. Chosen as BEST ANSWER

    TL;DR isLoading of useQuery is useless for showing spinners, use isFetching if you don't need background loading and need proper spinners

    I should've read documentation on useQuery more carefully.

    The isLoading property of the useQuery hook only indicates that the query is loading if there is no cached data available. If there is cached data available, the hook will return that data immediately and isLoading will be set to false. This results in old data being displayed briefly before the new data is fetched.

    What I need is isFetching property. It indicates whether the query is currently fetching new data, regardless of whether there is cached data available. So using isFetching solves the issue.

    Probably, I should use an overlay based on isFetching and the data shown based on isLoading. The button should be disabled based on isMutating and isFetching.

    Also, watch a demo from react-query creator where he mentions that he doesn't like multiple spinners going everywhere and prefers optimistic background updates.

    react-query v4 (a new version published in organization) describes the need for two separate statuses, status and fetchStatus: https://tanstack.com/query/v4/docs/react/guides/queries#why-two-different-states

    The main reason is ability to do loading in the background while displaying the cached (stale) version.


  2. if you return the result from invalidateQueries (which is a Promise) from the onSuccess callback, your mutation will stay in loading state until that Promise has resolved as well:

    onSuccess: () => {
      return queryClient.invalidateQueries("todos");
    },
    

    with that, you only need to disable the button while the mutation is in-flight (in loading state)

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search