Skip to main content

How to use Tanstack Query's useQuery with strong Typescript typing in Remix

· 3 min read

TLDR; You can simply use: useQuery<SerializeFrom<typeof loader>>

The blog post was meant to pile on the bandwagon of laughing at how ridiculous TypeScript types can get.

However, after playing with Shopify Oxygen, I recently learned about the (undocumented) Remix type SerializeFrom.

Instead of littering your code with the ridiculous thing I derive in the blog post: useQuery<Awaited<ReturnType<Awaited<ReturnType<typeof loader>>['json']>>>

Is it bad that this common use case of wanting to re-use the data type returned from your loaders in your client is undocumented and hard to find (as of May 20 2024)? Yes of course. But at least it's there!

This also goes to show the utility of TypeScript's type system. Usually when you see a ridiculous looking type, there's a way to clean it up for downstream consumers.

Leaving the original blog post below to document my madness for posterity.

How to use Tanstack Query's useQuery with Remix and Typescript

Using TanStack Query to fetch a resource route in your Remix app?

Want a strongly typed response?

lmaooo

JK here you go:

useQuery<Awaited<ReturnType<Awaited<ReturnType<typeof loader>>['json']>>>

Yay TypeScript!

Want a full example? Surely you're intrigued by now...

import { loader } from '~/routes/your_resource_route'

const {
data,
isLoading,
error,
} = useQuery<Awaited<ReturnType<Awaited<ReturnType<typeof loader>>['json']>>>(
['your_loader_name', queryString],
() =>
fetch(`${window.location.origin}/your_resource_route`).then(
(res) => res.json(),
),
{
enabled: typeof window === 'object', // fetch only on client
},
)

Why does that type work?

To fetch and get the response in JSON format in one line, you do:

await(await fetch('')).json()

And loaders are pretty much just fetch under the hood.

So, to infer the loader's response, we use Awaited<ReturnType<typeof loader>

To infer the .json() part, we use ReturnType<...>['json']>

And finally, because for some reason the json method is async, we wrap everything in a final Awaited<...>

<Awaited<ReturnType<Awaited<ReturnType<typeof loader>>['json']>>>(

Yay!

Running only on the client

The enabled: typeof window === 'object' argument sidesteps all the complex initialData, hydration, and dehydration stuff mentioned in the TanStack Query docs.

Note that it will limit this fetch to the client, so this pattern should only be used for components with complex fetching logic that don't need to render right away.

What about defer?

Why not use the the amazing defer API?

My use case is a complex chart with lots of data to paginate through. Previously I was fetching it in my loader and the page took 4 seconds to paint. Now the page paints in 0.5 seconds.

Also, the pagination makes re-using the page loader a bad idea - if I request a new chunk of data for the chart from the loader (ex. ?chart_page=2), the entire loader would run again, re-rendering the entire route & its children. All I want is the chart to re-render with a new chunk of data, full-stack component style.