Difference between isPending and isLoading in TanStack Query
It all started with this tweet:
Omo, I've been handling loading and error states with @tan_stack query wrong for a very long time pic.twitter.com/EbOHE79MTN
— Paul (@jadge_dev) December 24, 2024
It was surprising to read this since I had also used isLoading
for all my loading states. I suspected something was wrong with this approach when I needed to check the data
and the isLoading
checks 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";
function App() {
const { ... } = useQuery({
queryKey: ["pokemon"],
queryFn: async () => {
return { pokemon: "Pikachu" };
},
});
return null;
}
export default App;
Let’s console.log
some flag combinations to find the difference.
isLoading & isError
In the first example, we’ll try to use only isLoading
and 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, switching browser tabs does not result in extra console.log
calls (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 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}</>;
}
The console will give us the following:
{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 is 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?
isPending
indicates that we’re waiting for data to load for the first time. The flag will always betrue
if the query is not enabled.isLoading
: Indicates active data loading for the first time. If the query is not enabled, it will befalse
.isFetching
: Indicates active data fetching. This flag istrue
when queryFn is executed for the first time or during background re-fetching.