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?
isPendingindicates that we’re waiting for data to load for the first time. The flag will always betrueif 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 istruewhen queryFn is executed for the first time or during background re-fetching.
What to use?
Official documentation says this:
For most queries, it's usually sufficient to check for the
isPendingstate, then theisErrorstate, then finally, assume that thedatais available and render the successful state