
switch-query is a tiny npm package that simplifies rendering the state of @tanstack/react-query
switch-query is a tiny npm package that simplifies displaying the state from @tanstack/react-query. It exports a single component, SwitchQuery, along with its types. Despite its simplicity, the package streamlines code structure and enables more thoughtful design UI/UX.
In general, to provide users with the most accurate information retrieved from the server, the following scenarios must be considered:
@tanstack/react-query implements types narrowing. This allows you to explicitly separate request states using strict typing.
Let’s take an example from the official website:
import { useQuery } from '@tanstack/react-query';
function Todos() {
const { data, isPending, error } = useQuery({
queryKey: ['todos'],
queryFn: () => fetch('/api/todos').then((r) => r.json()),
});
if (isPending) {
return <span>Loading...</span>;
}
if (error) {
return <span>Oops!</span>;
}
return (
<ul>
{data.map((t) => (
<li key={t.id}>{t.title}</li>
))}
</ul>
);
}
export default Todos;
In my opinion, this is one of the library’s key advantages: it encourages developers to create more thoughtful user interfaces.
However, when using if and return statements, it becomes inconvenient to add elements common to each state. For example, let’s add a container and a create button.
{% details Example of modified code %}
import { useQuery } from '@tanstack/react-query';
function Todos() {
const { data, isPending, error } = useQuery({
queryKey: ['todos'],
queryFn: () => fetch('/api/todos').then((r) => r.json()),
});
if (isPending) {
return (
<div className="container">
<span>Loading...</span>
<button>add</button>
</div>
);
}
if (error) {
return (
<div className="container">
<span>Oops!</span>
<button>add</button>
</div>
);
}
return (
<div className="container">
<ul>
{data.map((t) => (
<li key={t.id}>{t.title}</li>
))}
</ul>
<button>add</button>
</div>
);
}
export default Todos;
An alternative using conditions within JSX solves the code duplication issue but is less explicit and leaves room for errors, such as displaying multiple states simultaneously:
import { useQuery } from '@tanstack/react-query';
function Todos() {
const { data, isPending, error, isSuccess } = useQuery({
queryKey: ['todos'],
queryFn: () => fetch('/api/todos').then((r) => r.json()),
});
return (
<div className="container">
{isPending && <span>Loading...</span>}
{error && <span>Oops!</span>}
{isSuccess && (
<ul>
{data.map((t) => (
<li key={t.id}>{t.title}</li>
))}
</ul>
)}
<button>add</button>
</div>
);
}
export default Todos;
{% enddetails %}
With switch-query, this task is easily solved by encapsulating checks within a nested JSX element:
import { useQuery } from '@tanstack/react-query';
import { SwitchQuery } from 'switch-query';
function Todos() {
const query = useQuery({
queryKey: ['todos'],
queryFn: () => fetch('/api/todos').then((r) => r.json()),
});
return (
<div className="container">
<SwitchQuery
query={query}
pending={<span>Loading...</span>}
error={<span>Oops!</span>}
success={({ data }) => (
<ul>
{data.map((t) => (
<li key={t.id}>{t.title}</li>
))}
</ul>
)}
/>
<button>add</button>
</div>
);
}
export default Todos;
This approach is more convenient in the JSX context. There’s no need to excessively decompose components or use nested ternary operators.
Rendering via JSX allows working with multiple queries in a single component.
Suppose each item references a tag dictionary via tagId, and we need to display the tag title.
In this case, you can nest <SwitchQuery /> elements within each other. The order in which responses are received doesn’t matter, as all states are accounted for at every level.
import { useQuery } from '@tanstack/react-query';
import { SwitchQuery } from 'switch-query';
function Todos() {
const todos = useQuery({
queryKey: ['todos'],
queryFn: () => fetch('/api/todos').then((r) => r.json()),
});
const tags = useQuery({
queryKey: ['tags'],
queryFn: () => fetch('/api/tags').then((r) => r.json()),
});
return (
<div className="container">
<SwitchQuery
query={todos}
pending={<span>Loading...</span>}
error={<span>Oops!</span>}
success={({ data }) => (
<ul>
{data.map((t) => (
<li key={t.id}>
{t.title}
<SwitchQuery
query={tags}
success={({ data }) => data.title}
pending="loading..."
error={`#${t.tagId}`}
/>
</li>
))}
</ul>
)}
/>
<button>add</button>
<div>
<h2>Tags:</h2>
<SwitchQuery
query={tags}
success={({ data }) => (
<ul>
{Object.keys(data).map((id) => (
<li key={id}>{data.title}</li>
))}
</ul>
)}
pending="loading..."
error={({ error, refetch }) => (
<>
<div>{error.message}</div>
<button onClick={() => refetch()}>retry</button>
</>
)}
/>
</div>
</div>
);
}
export default Todos;
Implementing such a view without switch-query would require creating additional components.
For good design, it’s also important to explicitly inform the user when there is no data.
In switch-query empty state case is moved to additional props:
<SwitchQuery
query={query}
success={({ data }) => (
<ul>
{data.map((t) => (
<li key={t.id}>{t.title}</li>
))}
</ul>
)}
checkIsEmpty={(data) => !data.length}
empty={<div>Nothing to do</div>}
/>
Handling all possible states is brought to a more declarative style.
One might argue that all the examples above are flawed and it would be better to move everything into separate components. This is a perfectly valid stance and you could even use useSuspenseQuery.
However, in this case, implementation of a single interface block would be spread across multiple components and files, which would also need to be named.
I believe that @tanstack/react-query blurs established patterns by making the request an integral part of the views, which aligns with the final result. The library’s declarative API is perfectly complemented by the declarative nature of JSX.
gemmaI ported the whole Gemma-4 family — E2B, E4B, 12B, 31B, and the 26B-A4B MoE — to run on...
communityHey DEV, I'm Tobore. Let's actually connect. I've been on here for a while now, mostly writing and...
ai(yep, kinda clickbait, just for the funsies 😊) At the beginning of the year, I relaunched my...
aiMy laptop was sitting idle with the fan at full tilt. Nothing was running that I knew of. The culprit...
githubactionsI Built a Thing! TL;DR — Google Gemini-based Pull Request reviews and Issue Triaging for...
aiI've been hearing the word "harness" thrown around a lot lately. I assumed it just meant "the IDE" or...