Denys Isaichenko

Software Engineer

Difference between isPending and isLoading in TanStack Query

It all started with this tweet:

Since I had also been using isLoading for all my loading states, it was quite a surprise for me to read this. I had suspected something was wrong with this approach when I needed to check data in addition to the isLoading check due to TypeScript errors.

Let’s dive deeper and explore the difference.

Setup

The basic code will contain a single useQuery call with some mock data:

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

async function sleep(ms: number): Promise<void> {
  return new Promise((resolve) => setTimeout(resolve, ms));
}

function App() {
  const { ... } = useQuery({
    queryKey: ["pokemon"],
    queryFn: async () => {
      await sleep(1_000);

      return { pokemon: "Pikachu" };
    },
  });

  return null;
}

export default App;

Let’s console.log some of the flags combinations to find out the difference.

isLoading & isError

In the first example, we’ll try to use only isLoadingand isError:

function App() {
  const { isLoading, isError, data } = useQuery({...});

  console.log({ isLoading, isError, data });

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

  if (isError) {
    return <>Error</>;
  }

  return <>{data?.pokemon}</>;
}

The console will give us:

{isLoading: true, isError: false, data: undefined}
{isLoading: false, isError: false, data: {…}}

Additionally, there are no extra console.log calls when switching browser tabs (assuming refetchOnWindowFocus is true by default).

Notice that we’re using data?.pokemon because otherwise, TypeScript will throw an error 'data' is possibly 'undefined'. That happens because isLoading = false and isError = false don’t guarantee that data exists. Which is a bit strange when you think about it.

isPending & isError

Now let’s switch isLoading with isPending:

function App() {
  const { isPending, isError, data } = useQuery({...});

  console.log({ isPending, isError, data });

  if (isPending) {
    return <>Loading</>;
  }

  if (isError) {
    return <>Error</>;
  }

  return <>{data.pokemon}</>;
}

Console will give us:

{isPending: true, isError: false, data: undefined}
{isPending: false, isError: false, data: {…}}

So the output basically the same, except now we don’t need to use data?.pokemon because data is always defined if isPending and isError are both false. Interesting!

What’s the difference?

To clarify the difference, let’s modify our example by introducing the enabled flag and setting it to false:

  const { isLoading, isPending, isError, data } = useQuery({
    enabled: false,
    ...
  });

  console.log({ isLoading, isPending, isError, data });

The console will give us:

{isLoading: false, isPending: true, isError: false, data: undefined}

There’s only one log entry and isLoading is false while isPending is true. This explains why checking only isLoading doesn’t guarantee that data is available.

What Do These Flags Mean?

Most of the time, you’ll want to use isPending, which makes sense when you think about it.