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
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 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, 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?
isPending
: Indicates that we’re waiting for data to load for the first time. If the query is not enabled, it will always betrue
.isLoading
: Indicates active loading of data for the first time. If the query is not enabled, it will befalse
.isFetching
: Indicates active data fetching. This flag istrue
when queryFn is being executed, either for the first time or during background re-fetching.
Most of the time, you’ll want to use isPending, which makes sense when you think about it.