Skip to main content

Setup

Fully typed RPC-like client for React Query v5? Oh, yes!

const {
data, // <- fully typed response data
error, // <- fully typed error data
} = tsr.getPost.useQuery({
queryKey: ['posts'],
queryData: { // <- fully typed request data
params: { id: '1' },
},
staleTime: 1000, // <- react-query options (optional)
});
info

This documentation is for React Query v5. If you are looking for the v4 docs, please click here.

In order to use the React Query v5 version of @ts-rest/react-query, make sure you import @ts-rest/react-query/v5 in your code and not @ts-rest/react-query.

Instructions

1. Installation

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

2. Setup React Query

If you are not familiar enough with React Query, check out the official @tanstack/react-query documentation to learn more about it and how to set it up.

Before proceeding, make sure you set up your QueryClientProvider at the root of your application or layout.

3. Initialize ts-rest React Query

Import your contract, and pass it with the client options to the initTsrReactQuery function. The client options are the same as the ones you would pass to the initClient function in @ts-rest/core.

import { initTsrReactQuery } from '@ts-rest/react-query/v5';
import { getAccessToken } from '@some-auth-lib/sdk';
import { contract } from './contract';

export const tsr = initTsrReactQuery(contract, {
baseUrl: 'http://localhost:3333',
baseHeaders: {
'x-app-source': 'ts-rest',
'x-access-token': () => getAccessToken(),
},
});
info

initTsrReactQuery uses our the fetch client under the hood, so you can configure it just as you would the fetch client, including passing a custom API fetcher. For more information, see the fetch client documentation.

4. Setup ts-rest Provider

Add the tsr.ReactQueryProvider to your root component, just below the QueryClientProvider. It is important that it lives as a child of the QueryClientProvider so that ts-rest can access the query client.

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

const queryClient = new QueryClient()

export function Providers({ children }: { children: React.ReactNode }) {
return (
<QueryClientProvider client={queryClient}>
<tsr.ReactQueryProvider>{children}</tsr.ReactQueryProvider>
</QueryClientProvider>
);
}

Now use the hooks from tsr in your components. The structure of the tsr container follows the same structure as your contract.

Complete Example

import { tsr } from './tsr';

const Posts = () => {
const tsrQueryClient = tsr.useQueryClient();

const { data, isPending } = tsr.posts.get.useQuery({ queryKey: ['posts'] });
const { mutate } = tsr.posts.create.useMutation({
onMutate: (newPost) => {
// get current posts, so we can reset back to it if the mutation fails
const lastGoodKnown = tsrQueryClient.posts.get.getQueryData(['posts']);

// optimistically update the cache with the new post
tsrQueryClient.posts.get.setQueryData(['posts'], (old) => ({
...old,
body: [
...old.body,
{
...newPost.body,
id: `placeholder-${Date.now()}`
}
]
}));

// return the old posts to be stored in mutation context
return { lastGoodKnown };
},
onError: (error, newPost, context) => {
tsrQueryClient.posts.get.setQueryData(['posts'], context.lastGoodKnown);
},
onSettled: () => {
// trigger a refetch regardless of it the mutation was successful or not
tsrQueryClient.invalidateQueries({ queryKey: ['posts'] });
// ^ QueryClient functions that do not consume or provide typed data are not wrapped by ts-rest
// and are provided at the root level only
},
});

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

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

return (
<div>
<ul>
{data.body.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
<button onClick={() => mutate({ body: { title: 'Hello World' } })}>Create Post</button>
</div>
);
};

All the hooks have the same function signatures and options exactly as the ones from @tanstack/react-query, with the only difference being that you need to pass queryData instead of queryFn.

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.

Directly Fetching

If you want to make fetch requests directly without going through React Query, we also expose query and mutate functions, so you do not have to initialize a separate fetch client.

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

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