Status Checks in React Query
— react, React Query, JavaScript, TypeScript — 3 min read
Last Update: 2023-10-21
- #1: Practical React Query
- #2: React Query Data Transformations
- #3: React Query Render Optimizations
- #4: Status Checks in React Query
- #5: Testing React Query
- #6: React Query and TypeScript
- #7: Using WebSockets with React Query
- #8: Effective React Query Keys
- #8a: Leveraging the Query Function Context
- #9: Placeholder and Initial Data in React Query
- #10: React Query as a State Manager
- #11: React Query Error Handling
- #12: Mastering Mutations in React Query
- #13: Offline React Query
- #14: React Query and Forms
- #15: React Query FAQs
- #16: React Query meets React Router
- #17: Seeding the Query Cache
- #18: Inside React Query
- #19: Type-safe React Query
- #20: You Might Not Need React Query
- #21: Thinking in React Query
- #22: React Query and React Context
- #23: Why You Want React Query
- #24: The Query Options API
- #25: Automatic Query Invalidation after Mutations
- #26: How Infinite Queries work
- #27: React Query API Design - Lessons Learned
One advantage of React Query is the easy access to status fields of the query. You instantly know if your query is loading or if it's erroneous. For this, the library exposes a bunch of boolean flags, which are mostly derived from the internal state machine. Looking at the types, your query can be in one of the following states:
success
: Your query was successful, and you havedata
for iterror
: Your query did not work, and anerror
is setpending
: Your query has no data
Note that the isFetching
flag is not part of the internal state machine - it is an additional flag that will be true whenever a request is in-flight. You can be fetching and success, you can be fetching and error - but you cannot be loading and success at the same time. The state machine makes sure of that.
The standard example
Most examples look something like this:
1const todos = useTodos()2
3if (todos.isPending) {4 return 'Loading...'5}6if (todos.error) {7 return 'An error has occurred: ' + todos.error.message8}9
10return <div>{todos.data.map(renderTodo)}</div>
Here, we check for pending and error first, and then display our data. This is probably fine for some use-cases, but not for others. Many data fetching solutions, especially hand-crafted ones, have no refetch mechanism, or only refetch on explicit user interactions.
But React Query does.
It refetches quite aggressively per default, and does so without the user actively requesting a refetch. The concepts of refetchOnMount
, refetchOnWindowFocus
and refetchOnReconnect
are great for keeping your data accurate, but they might cause a confusing ux if such an automatic background refetch fails.
Background errors
In many situations, if a background refetch fails, it could be silently ignored. But the code above does not do that. Let's look at two examples:
- The user opens a page, and the initial query loads successfully. They are working on the page for some time, then switch browser tabs to check emails. They come back some minutes later, and React Query will do a background refetch. Now that fetch fails.
- Our user is on page with a list view, and they click on one item to drill down to the detail view. This works fine, so they go back to the list view. Once they go to the detail view again, they will see data from the cache. This is great - except if the background refetch fails.
In both situations, our query will be in the following state:
1{2 "status": "error",3 "error": { "message": "Something went wrong" },4 "data": [{ ... }]5}
As you can see, we will have both an error and the stale data available. This is what makes React Query great - it embraces the stale-while-revalidate caching mechanism, which means it will always give you data if it exists, even if it's stale.
Now it's up to us to decide what we display. Is it important to show the error? Is it enough to show the stale data only, if we have any? Should we show both, maybe with a little background error indicator?
There is no clear answer to this question - it depends on your exact use-case. However, given the two above examples, I think it would be a somewhat confusing user experience if data would be replaced with an error screen.
This is even more relevant when we take into account that React Query will retry failed queries three times per default with exponential backoff, so it might take a couple of seconds until the stale data is replaced with the error screen. If you also have no background fetching indicator, this can be really perplexing.
This is why I usually check for data-availability first:
1const todos = useTodos()2
3if (todos.data) {4 return <div>{todos.data.map(renderTodo)}</div>5}6if (todos.error) {7 return 'An error has occurred: ' + todos.error.message8}9
10return 'Loading...'
Again, there is no clear principle of what is right, as it is highly dependent on the use-case. Everyone should be aware of the consequences that aggressive refetching has, and we have to structure our code accordingly rather than strictly following the simple todo-examples 😉.
Special thanks go to Niek Bosch who first highlighted to me why this pattern of status checking can be harmful in some situations.
Feel free to reach out to me on bluesky if you have any questions, or just leave a comment below ⬇️