Skip to content

Commit 597cd92

Browse files
authored
feat(react-query): react-query v5 (#633)
1 parent 92641b9 commit 597cd92

103 files changed

Lines changed: 25579 additions & 16214 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.changeset/chilly-jars-bow.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
'@ts-rest/react-query': minor
3+
'@ts-rest/core': minor
4+
'@ts-rest/solid-query': minor
5+
'@ts-rest/vue-query': minor
6+
---
7+
8+
You can now pass functions as values for your `baseHeaders` in your client. This makes it much easier now to fetch and set access tokens from your authentication libraries.

.changeset/eighty-owls-report.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@ts-rest/react-query': minor
3+
---
4+
5+
New and vastly improved React Query integration with v5 support in `@ts-rest/react-query/v5`

.github/workflows/prerelease.yml

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ jobs:
1717
permissions:
1818
actions: read
1919
contents: write
20+
id-token: write
2021
packages: write
2122
pull-requests: write
2223
issues: read
@@ -46,8 +47,41 @@ jobs:
4647
- name: Set changesets to prerelease mode
4748
run: pnpm changeset pre enter ${{ inputs.tag }}
4849

49-
- name: Set versions in package.json
50-
run: node tools/scripts/prerelease-ci.mjs
50+
- name: Evaluate pre-release version
51+
run: pnpm version-packages
52+
53+
- name: Get pre-release version
54+
id: pre-release-version
55+
uses: martinbeentjes/npm-get-version-action@3cf273023a0dda27efcd3164bdfb51908dd46a5b
56+
with:
57+
path: libs/ts-rest/core
58+
59+
- name: Reset changes
60+
run: git checkout .
61+
62+
- name: Get latest published version and strip tag
63+
id: package-versions
64+
run: |
65+
export LATEST_PUBLISHED_VERSION=$(pnpm dlx latest-version-cli @ts-rest/core --range=${{ inputs.tag }})
66+
export CURRENT_PRERELEASE_VERSION=${{ steps.pre-release-version.outputs.current-version }}
67+
echo "latest_version=${LATEST_PUBLISHED_VERSION}" >> $GITHUB_OUTPUT
68+
echo "latest_version_stripped=${LATEST_PUBLISHED_VERSION%-*}" >> $GITHUB_OUTPUT
69+
echo "project_version_stripped=${CURRENT_PRERELEASE_VERSION%-*}" >> $GITHUB_OUTPUT
70+
71+
- name: Set to latest published version so we can bump to next version
72+
if: ${{ steps.package-versions.outputs.latest_version_stripped == steps.package-versions.outputs.project_version_stripped }}
73+
uses: jaywcjlove/github-action-package@f6a7afaf74f96a166243f05560d5af4bd4eaa570
74+
with:
75+
path: libs/ts-rest/core/package.json
76+
version: '${{ steps.package-versions.outputs.latest_version }}'
77+
78+
- name: Reset pre-release mode
79+
run: |
80+
rm .changeset/pre.json
81+
pnpm changeset pre enter ${{ inputs.tag }}
82+
83+
- name: Update versions in all packages
84+
run: pnpm version-packages
5185

5286
- name: Modify "workspaces" value in package.json
5387
run: sed -e "s;libs/ts-rest/\*;dist/libs/ts-rest/*;g" package.json > package-new.json && mv package-new.json package.json
@@ -72,3 +106,4 @@ jobs:
72106
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
73107
NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
74108
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
109+
NPM_CONFIG_PROVENANCE: true

.github/workflows/release.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ jobs:
1010
permissions:
1111
actions: read
1212
contents: write
13+
id-token: write
1314
packages: write
1415
pull-requests: write
1516
issues: read
@@ -72,3 +73,4 @@ jobs:
7273
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
7374
NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
7475
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
76+
NPM_CONFIG_PROVENANCE: true

apps/docs/docs/core/fetch.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,15 @@ All of the client libraries (`@ts-rest/core`, `@ts-rest/react-query`, and `@ts-r
44

55
```typescript
66
import { initClient } from '@ts-rest/core';
7+
import { getAccessToken } from '@some-auth-lib/sdk';
78
import { contract } from './contract';
89

910
export const client = initClient(contract, {
1011
baseUrl: 'http://localhost:3334',
11-
baseHeaders: {},
12+
baseHeaders: {
13+
'x-app-source': 'ts-rest',
14+
'x-access-token': () => getAccessToken(),
15+
},
1216
});
1317
```
1418

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
# Error Handling
2+
3+
If a request fails, the `error` property will be set to the response from the server, or the thrown error by `fetch`. This is the same as the `data` property for successful requests.
4+
5+
The type of the `error` property on the React Query hooks will be set as `{ status: ...; body: ...; headers: ... } | Error`, where status is a non-2xx status code, and `body`
6+
set to your response schema for status codes defined in your contract, or `unknown` for status codes not in your contract.
7+
8+
The `Error` type is included because requests can fail without returning a response. See [Fetch#Exceptions](https://developer.mozilla.org/en-US/docs/Web/API/Window/fetch#exceptions) for more information.
9+
10+
```tsx
11+
import { isFetchError } from '@ts-rest/react-query/v5';
12+
import { tsr } from './tsr';
13+
14+
const Post = ({ id }: { id: string }) => {
15+
const { data, error, isPending } = tsr.getPost.useQuery({
16+
queryKey: ['posts', id],
17+
queryData: {
18+
params: { id },
19+
},
20+
});
21+
22+
if (isPending) {
23+
return <div>Loading...</div>;
24+
}
25+
26+
if (error) {
27+
if (isFetchError(error)) {
28+
return <div>We could not retrieve this post. Please check your internet connection.</div>;
29+
}
30+
31+
if (error.status === 404) {
32+
return <div>Post not found</div>;
33+
}
34+
35+
return <div>Unexpected error occurred</div>;
36+
}
37+
38+
return (
39+
<div>
40+
<h1>{data.body.title}</h1>
41+
<p>{data.body.content}</p>
42+
</div>
43+
);
44+
};
45+
```
46+
47+
## Fully Type-Safe Error Handling
48+
49+
In order to ensure that your code is handling all possible error cases, there are type guard functions that have been provided to help the handling of both expected and unexpected errors.
50+
51+
- `isFetchError(error)` - Returns `true` if the error is an instance of `Error` thrown by `fetch`.
52+
- `isUnknownErrorResponse(error, contractEndpoint)` - Returns `true` if the error, if a response has been received but the status code is not defined in the contract.
53+
- `isNotKnownResponseError(error, contractEndpoint)` - Combines `isFetchError` and `isUnknownErrorResponse`, in case you want to be able to quickly type guard into defined error responses in one statement.
54+
- `exhaustiveGuard(error)` - Check if all possible error cases have been handled. Otherwise, you get a compile-time error.
55+
56+
We also return the `contractEndpoint` property from all hooks, so you can easily pass it to the types guards without having import the contract.
57+
58+
```tsx
59+
import { isFetchError, isUndefinedErrorResponse, exhaustiveGuard } from '@ts-rest/react-query/v5';
60+
import { tsr } from './tsr';
61+
62+
const Post = ({ id }: { id: string }) => {
63+
const { data, error, isPending, contractEndpoint } = tsr.getPost.useQuery({
64+
queryKey: ['posts', id],
65+
queryData: {
66+
params: { id },
67+
},
68+
});
69+
70+
if (isPending) {
71+
return <div>Loading...</div>;
72+
}
73+
74+
if (error) {
75+
if (isFetchError(error)) {
76+
return <div>We could not retrieve this post. Please check your internet connection.</div>;
77+
}
78+
79+
if (isUndefinedErrorResponse(error, contractEndpoint)) {
80+
return <div>Unexpected error occurred</div>;
81+
}
82+
83+
if (error.status === 404) {
84+
return <div>Post not found</div>;
85+
}
86+
87+
// this should be unreachable code if you handle all possible error cases
88+
// if not, you will get a compile-time error on the line below
89+
return exhaustiveGuard(error);
90+
}
91+
92+
return (
93+
<div>
94+
<h1>{data.body.title}</h1>
95+
<p>{data.body.content}</p>
96+
</div>
97+
);
98+
};
99+
```
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
# QueryClient
2+
3+
In addition to the hooks provided, `@ts-rest/react-query` also provides an extended version of `QueryClient` that is fully type-safe.
4+
5+
It follows the same structure as your contract, and can be used the same way as the original `QueryClient` with similar function
6+
signatures to the ts-rest hooks for functions such as `queryClient.fetchQuery` and it's respective `useQuery` hook.
7+
8+
```tsx
9+
import { tsr } from './tsr';
10+
11+
const Posts = () => {
12+
const POSTS_QUERY_KEY = ['posts'];
13+
14+
const tsrQueryClient = tsr.useQueryClient();
15+
const { data, isLoading } = tsr.posts.get.useQuery({ queryKey: POSTS_QUERY_KEY });
16+
const { mutate } = tsr.posts.create.useMutation();
17+
18+
const createPost = async () => {
19+
return mutate(
20+
{ body: { title: 'Hello World' } },
21+
{
22+
onSuccess: async (data) => {
23+
// this is typed ^
24+
tsrQueryClient.posts.get.setQueryData(POSTS_QUERY_KEY, (oldPosts) => {
25+
// this is also typed ^
26+
return {
27+
...oldPosts,
28+
body: [...oldPosts.body, data.body],
29+
};
30+
});
31+
},
32+
},
33+
);
34+
};
35+
36+
if (isLoading) {
37+
return <div>Loading...</div>;
38+
}
39+
40+
if (data?.status !== 200) {
41+
return <div>Error</div>;
42+
}
43+
44+
return (
45+
<div>
46+
<button onClick={createPost}>Create Post</button>
47+
{data.body.map((post) => (
48+
<p key={post.id}>post.title</p>
49+
))}
50+
</div>
51+
);
52+
};
53+
```
54+
55+
## Non-Wrapped Functions
56+
57+
For functions that do not consume or provide typed data such as `queryClient.invalidateQueries()`, it makes no sense to wrap these and access them through an endpoint path such as `tsrQueryClient.posts.get.invalidateQueries()`.
58+
As such, these functions are provided as-is at the root level of the `tsr.useQueryClient()` instance.
59+
60+
You can actually use the `QueryClient` returned from `tsr.useQueryClient()` anywhere you would normally use a `QueryClient` instance, as under the hood
61+
we use the `QueryClient` returned from `useQueryClient()`, and we simply extend it with the ts-rest functions.
62+

apps/docs/docs/react-query/ssr.md

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
# Server Rendering
2+
3+
The common strategy to efficiently and optimally do server side rendering, as well as prevent request waterfalls on the client, is to do prefetching on the server,
4+
then pass a dehydrated form of the query cache from the server to the client.
5+
6+
In these scenarios, the React Query code will not run inside a provider, so we need to initialize the `QueryClient` manually and pass it to ts-rest.
7+
8+
Therefore, instead of using `tsr.useQueryClient()` as you usually would in your components, use `tsr.initQueryClient(queryClient)` to pass your created `QueryClient` to ts-rest.
9+
10+
See the [`@tanstack/react-query` Server Rendering Guide](https://tanstack.com/query/v5/docs/framework/react/guides/ssr) for an in-depth guide on how to properly do server side rendering.
11+
12+
## Examples
13+
14+
### Next.js Pages Router
15+
16+
```tsx
17+
// pages/posts.tsx
18+
import { dehydrate, QueryClient } from '@tanstack/react-query';
19+
import { tsr } from './tsr';
20+
21+
export async function getServerSideProps() {
22+
const tsrQueryClient = tsr.initQueryClient(new QueryClient());
23+
24+
await tsrQueryClient.getPosts.prefetchQuery({ queryKey: ['POSTS'] });
25+
26+
return {
27+
props: {
28+
dehydratedState: dehydrate(queryClient),
29+
},
30+
}
31+
}
32+
```
33+
34+
### React Server Components
35+
36+
```tsx
37+
// app/posts/page.tsx
38+
import { dehydrate, HydrationBoundary, QueryClient} from '@tanstack/react-query';
39+
import { tsr } from './tsr';
40+
41+
export default async function PostsPage() {
42+
const tsrQueryClient = tsr.initQueryClient(new QueryClient()); // <-- or pass a QueryClient from anywhere depending on your needs
43+
44+
await tsrQueryClient.getPosts.prefetchQuery({ queryKey: ['POSTS'] });
45+
46+
return (
47+
<HydrationBoundary state={dehydrate(tsrQueryClient)}>
48+
<Posts />
49+
</HydrationBoundary>
50+
);
51+
}
52+
```
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# Troubleshooting
2+
3+
## `No QueryClient set, use QueryClientProvider to set one`
4+
5+
If you see this error despite having set a `QueryClient` using `QueryClientProvider`. Then you might have different versions of `@tanstack/react-query` installed in your project.
6+
7+
This can also happen in rare cases when ESM and CJS versions of the package are mixed by a bundler like Webpack.
8+
9+
If you have made sure that you are using the same version of `@tanstack/react-query` across your project, and are still having problems, you can work around this
10+
by importing `@tanstack/react-query` from `@ts-rest/react-query/tanstack` instead of `@tanstack/react-query`. This will ensure that you are using the same version as the one used
11+
by `@ts-rest/react-query`.
12+
13+
```tsx
14+
import { QueryClient, QueryClientProvider } from '@ts-rest/react-query/tanstack';
15+
16+
const queryClient = new QueryClient()
17+
18+
function App() {
19+
return <QueryClientProvider client={queryClient}>...</QueryClientProvider>
20+
}
21+
```
22+
23+
:::info
24+
25+
The import path is `@ts-rest/react-query/tanstack` and not `@ts-rest/react-query/v5/tanstack`. `@ts-rest/react-query/tanstack` simply re-exports whichever version of `@tanstack/react-query` you have installed.
26+
27+
:::

0 commit comments

Comments
 (0)