Skip to main content

React Query

Installation​

pnpm add @ts-rest/react-query @tanstack/react-query

This is a client using @ts-rest/react-query, using @tanstack/react-query under the hood.

Initializing QueryClientProvider​

After installation, ensure that your React application is wrapped with react-query's QueryClientProvider. This provider is essential for managing the state and lifecycle of your queries and mutations.

import { QueryClient, QueryClientProvider } from '@tanstack/react-query';

const queryClient = new QueryClient();

function App() {
return <QueryClientProvider client={queryClient}>...</QueryClientProvider>;
}

For more detailed information, refer to the official react-query documentation.

initQueryClient​

The below snippet is how you'd create a query client, this is pretty much the same structure as the @ts-rest/core client.

import { initQueryClient } from '@ts-rest/react-query';

export const client = initQueryClient(router, {
baseUrl: 'http://localhost:3333',
baseHeaders: {},
api?: () => ... // <- Optional Custom API Fetcher (see below)
});

To customise the API, follow the same docs as the core client here

tip

By default, ts-rest encodes query parameters as regular URL encoded strings (with support for nested objects, arrays etc) with full decode compatibility from qs

To encode query parameters as JSON, you can use the jsonQuery option, please note you'll need to configure your backend to support decoding JSON query parameters.

export const client = initQueryClient(router, {
...,
jsonQuery: true
});

useQuery and useMutation​

Once you've created a client using initQueryClient, you may traverse the object (in the exact same structure as your contract layout) to find the query or mutation you want to use.

const queryResult = client.posts.get.useQuery(
['posts'], // <- queryKey
{ params: { id: '1' } }, // <- Query params, Params, Body etc (all typed)
{ staleTime: 1000 }, // <- react-query options (optional)
);
tip

The design philosophy of @ts-rest/react-query is to make it as easy as possible to use react-query with @ts-rest. This means that the useQuery and useMutation hooks are as close to the original react-query hooks as possible, as such we don't abstract away from the query keys or the options. Only the query function itself!

const App = () => {
// Effectively a useQuery hook
const { data, isLoading, error } = client.posts.get.useQuery(['posts']);

// Effectively a useMutation hook
const { mutate, isLoading } = client.posts.create.useMutation();

if (isLoading) {
return <div>Loading...</div>;
}

if (data?.status !== 200) {
return <div>Error</div>;
}

return (
<div>
{data.body.map((post) => (
<p key={post.id}>post.title</p>
))}
</div>
);
};
info

When destructing the response from useQuery or useMutation, remember that ts-rest returns a status and body property, so you'll need to destructure those as well.

The reason for this is error handling! Please see the Relevant Docs

Regular Query and Mutations​

@ts-rest/react-query allows for a regular fetch or mutation if you want, without having to initialise two different clients, one with @ts-rest/core and one with @ts-rest/react-query.

// Normal fetch
const { body, status } = await client.posts.get.query();

// useQuery hook
const { data, isLoading } = client.posts.get.useQuery();

useInfiniteQuery​

One fantastic feature of react-query is the ability to create infinite queries. This is a great way to handle pagination.

Prisma's Docs explain the concepts of cursor and offset pagination fantastically, especially if you use Prisma client with @ts-rest

Cursor Pagination​

This is a simple cursor based pagination example,

const { isLoading, data, hasNextPage, fetchNextPage } = useInfiniteQuery(
queryKey,
({ pageParam = 1 }) => pageParam,
{
getNextPageParam: (lastPage, allPages) => lastPage.nextCursor,
getPreviousPageParam: (firstPage, allPages) => firstPage.prevCursor,
},
);

Offset Pagination​

This example specifically uses an API with skip and take query parameters, so this is requires slightly more configuration than a regular query (hence the complicated looking getNextPageParam)

const PAGE_SIZE = 5;

export function Index() {
const { isLoading, data, hasNextPage, fetchNextPage } =
client.getPosts.useInfiniteQuery(
['posts'],
({ pageParam = { skip: 0, take: PAGE_SIZE } }) => ({
query: { skip: pageParam.skip, take: pageParam.take },
}),
{
getNextPageParam: (lastPage, allPages) =>
lastPage.status === 200
? lastPage.body.count > allPages.length * PAGE_SIZE
? { take: PAGE_SIZE, skip: allPages.length * PAGE_SIZE }
: undefined
: undefined,
},
);

if (isLoading) {
return <div>Loading...</div>;
}

if (!data) {
return <div>No posts found</div>;
}

const posts = data.pages.flatMap((page) =>
page.status === 200 ? page.body.posts : [],
);

//...
}

useQueries​

useQueries (useQueries docs) is a great way to fetch multiple queries at once, or to dynamically fetch queries based on some condition. This is great because normally the number of useQuery hooks per component is fixed.

const queries = client.posts.get.useQueries({
queries: [
// <- This queries array can be changed at runtime!
{
queryKey: ['posts', '1'],
params: {
id: '1',
},
},
{
queryKey: ['posts', '2'],
params: {
id: '2',
},
},
],
});

if (queries.some((query) => query.isLoading)) {
return <div>Loading...</div>;
}

return (
<div>
{queries.map((query) => (
<p key={query.data?.body.id}>{query.data?.body.title}</p>
))}
</div>
);

QueryClient Helpers​

In addition to the hooks provided, @ts-rest/react-query also provides a custom hook useTsRestQueryClient with wrapper functions around some QueryClient functions to help invoke the API as well as provide some typing.

import { initQueryClient, useTsRestQueryClient } from '@ts-rest/react-query';

export const client = initQueryClient(router);

const App = () => {
const apiQueryClient = useTsRestQueryClient(client);

// You can either use apiQueryClient or client to call useQuery, useMutation, etc.
const { data, isLoading, error } = apiQueryClient.posts.get.useQuery([
'posts',
]);
const { mutate, isLoading } = client.posts.create.useMutation();

const createPost = async () => {
return mutate(
{ body: { title: 'Hello World' } },
{
onSuccess: async (data) => {
// this is typed ^
apiQueryClient.posts.get.setQueryData(['posts'], (oldPosts) => {
// this is also typed ^
return {
...oldPosts,
body: [...oldPosts.body, data.body],
};
});
},
},
);
};

if (isLoading) {
return <div>Loading...</div>;
}

if (data?.status !== 200) {
return <div>Error</div>;
}

return (
<div>
<button onClick={createPost}>Create Post</button>
{data.body.map((post) => (
<p key={post.id}>post.title</p>
))}
</div>
);
};

Functions​

Following the same design philosophy as the hooks, these helpers follow the original QueryClient function APIs as closely as possible. Essentially, the API is exactly the same, but instead of passing a queryFn, you pass the request parameters directly.

  • async fetchQuery(queryKey, args, options) => Promise<SuccessResponse>
  • async fetchInfiniteQuery(queryKey, argsMapper, options) => Promise<InfiniteData<SuccessResponse>>
  • async prefetchQuery(queryKey, args, options) => Promise<void>
  • async prefetchInfiniteQuery(queryKey, argsMapper, options) => Promise<void>
  • getQueryData(queryKey, filters) => SuccessResponse | undefined
  • async ensureQueryData(queryKey, args, options) => Promise<SuccessResponse>
  • getQueriesData(filters) => [QueryKey, SuccessResponse | undefined][]
  • setQueryData(queryKey, updater) => SuccessResponse | undefined

If you would like to use any of these functions outside a component or a hook, these functions are also available through the client directly, but you have to pass in the QueryClient instance as the first argument.

Functions such as invalidateQueries that neither exchange typed data nor invoke the API have no benefit of being wrapped. Therefore, call these functions directly through your QueryClient instance.

info

Functions that may throw on failure such as fetchQuery will throw an error if the request fails or returns a non-2xx response. Be sure to handle these errors appropriately.