How pattern matching can help elegantly handle different states (loading, error, success, empty) of TanStack React Query, making your components cleaner and more type-safe.
It’s interesting how simple solutions can often have a significant impact. Back
at React Paris, I met a React developer who told me, “I remember one of your
posts on LinkedIn; it was a
snippet about React Query, and now all my coworkers are using it.” It took me
some time to find it, but here is
the post.
It seemed like a good time to write a blog post about it.
Anyway, let’s get back to the main topic: Handling asynchronous data fetching is
a core part of many web applications. TanStack React Query is a fantastic
library for managing server state. However, rendering different UI based on the
query’s status (loading, error, success, potentially empty) can sometimes lead
to verbose and tedious conditional logic within components.
if (!postsQuery.data || postsQuery.data.length ===0) {
return<EmptyMessagemessage="No posts found."/>;
}
// Success state
return (
<ul>
{postsQuery.data.map((post)=> (
<likey={post.id}>{post.title}</li>
))}
</ul>
);
}
While this works, the nested if statements or multiple ternaries can become
cumbersome, especially as the component grows or if you need to handle these
states repeatedly across your application.
Handling multiple queries within the same component makes writing these if
statements even less enjoyable.
As mentioned in Dominik’s post, this can become tedious when you have a layout
component wrapping this logic. For instance, let’s say the boundary of your
component is represented by the red area below:
You will need to repeat the layout component for each conditional branch.
Ultimately, you might create a custom layout component like PostsListLayout
and wrap the return statement of each condition with it.
Here’s how that might look, repeating the PostsListLayout component for each
state:
if (!postsQuery.data || postsQuery.data.length ===0) {
return (
<PostsListLayout>
<EmptyMessagemessage="No posts found."/>
</PostsListLayout>
);
}
return (
<PostsListLayout>
<ul>
{postsQuery.data.map((post)=> (
<likey={post.id}>{post.title}</li>
))}
</ul>
</PostsListLayout>
);
}
This repetition makes the component harder to read and maintain.
Can we do better?
A Cleaner Approach: Pattern Matching Utility
To streamline this process, we can introduce a pattern matching utility
function. I will describe the function later, but first, let’s look at the
result.
Why? Because showing 15 lines of TypeScript upfront might scare some readers
away. So, let’s focus on the benefits first before diving into the
implementation details.
Pretty nice, isn’t it? All the complex conditional logic is gone!
Each query state is mapped to a corresponding property (Errored, Loading,
Empty, Success) in the options object, and each property holds the component
or render function for that state.
This is strictly typed. For example, if you provide the Empty property,
TypeScript ensures that the data passed to the Success function is
non-nullable. Conversely, if you omit Empty, data will be typed as
potentially null or undefined within Success.
Calls a defined callback function on each element of an array, and returns an array that contains the results.
@param ― callbackfn A function that accepts up to three arguments. The map method calls the callbackfn function one time for each element in the array.
@param ― thisArg An object to which the this keyword can refer in the callbackfn function. If thisArg is omitted, undefined is used as the this value.
This section delves into how I approach this kind of problem, offering a more
theoretical perspective on reducing complexity and simplifying codebases. Feel
free to skip to the next section if you are primarily interested in the
implementation details.
A common issue is repeating complex logic that essentially maps the query state
to the UI. This process can often be broken down into two distinct steps.
We can transform the single problem of “Mapping query to UI components” into two
simpler, distinct problems:
Mapping the query result to a defined State: A generic, reusable step.
Mapping that State to specific UI Components: A simpler, component-specific
step.
Why an hourglass?
I like to visualize this as an hourglass. We funnel the complex query result
object down into a smaller, well-defined set of states (the narrow part of the
hourglass). And then, we expand back out from these simple states to the desired
UI components (the bottom part of the hourglass).
The Implementation
This utility function takes the query result object and an options object
mapping query states to corresponding JSX elements or render functions.
Here’s the implementation of the matchQueryStatus utility:
This function checks the query status (isLoading, isError) and data presence
(isEmpty) and returns the corresponding component provided in the options
object.
💡
Info
Notice the TypeScript overloads. If you provide an Empty component in the options, the Success handler receives the query object where data is guaranteed to be non-nullable (NonNullable<T>). This improves type safety and reduces the need for non-null assertions (!) in your success component.
Using a pattern matching utility like matchQueryStatus for TanStack React
Query results offers several benefits:
Readability: Consolidates state handling logic into a single,
declarative structure.
Maintainability: Easier to modify or add handling for specific states.
Reusability: The utility function can be reused across different
components.
Type Safety: Leverages TypeScript for better guarantees, especially
around non-nullable data in the success state when handling empty states
explicitly.
While simple if/else checks are fine for basic cases, adopting a pattern
matching approach can significantly improve the clarity and robustness of your
React components when dealing with the various states of asynchronous operations
managed by TanStack React Query.
Feel free to use this pattern and adapt the utility function for your own needs.
Note: This article intentionally omits discussion of Suspense and newer APIs
like the use hook. While feedback on those is welcome in the comments, they
represent a topic for a separate article.
Any thoughts about this? Or questions? Feel free to comment below :)