Skip to content

Commit ff45cf9

Browse files
authored
feat(react-query): add getQueryClient (#1843)
1 parent 09e14da commit ff45cf9

File tree

19 files changed

+1005
-33
lines changed

19 files changed

+1005
-33
lines changed

.changeset/strong-laws-deny.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
"@suspensive/react-query-4": minor
3+
"@suspensive/react-query-5": minor
4+
"@suspensive/react-query": minor
5+
---
6+
7+
feat(react-query): add getQueryClient

docs/suspensive.org/src/content/en/docs/react-query/_meta.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ export default {
1111
type: 'separator',
1212
title: 'API Reference',
1313
},
14+
getQueryClient: { title: 'getQueryClient' },
1415
mutationOptions: { title: 'mutationOptions' },
1516
usePrefetchQuery: { title: 'usePrefetchQuery' },
1617
usePrefetchInfiniteQuery: { title: 'usePrefetchInfiniteQuery' },
Lines changed: 254 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,254 @@
1+
import { Callout } from '@/components'
2+
3+
# getQueryClient
4+
5+
<Callout type='experimental'>
6+
7+
`getQueryClient` is an experimental feature, so this interface may change.
8+
9+
</Callout>
10+
11+
A utility function to manage QueryClient instances in a server-safe manner.
12+
13+
- **On the server**: Always creates a new QueryClient instance for each request
14+
- **In the browser**: Returns a singleton QueryClient instance, creating one if it doesn't exist
15+
16+
This pattern is essential for proper React Query usage with SSR frameworks like Next.js, as it prevents sharing QueryClient state between requests on the server while maintaining a single instance in the browser to preserve cache across re-renders.
17+
18+
<Callout type='important'>
19+
20+
Sharing a QueryClient between requests on the server can lead to **serious security vulnerabilities**. `getQueryClient` creates a new QueryClient instance for each request to prevent data leakage between users and exposure of sensitive information. See the [Server-Side Behavior](#server-side-behavior) section below for details.
21+
22+
</Callout>
23+
24+
## Basic Usage
25+
26+
```tsx /getQueryClient/
27+
import { getQueryClient } from '@suspensive/react-query'
28+
import { QueryClientProvider } from '@tanstack/react-query'
29+
30+
function Providers({ children }: { children: React.ReactNode }) {
31+
const queryClient = getQueryClient()
32+
return (
33+
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
34+
)
35+
}
36+
```
37+
38+
## Configuration
39+
40+
You can pass a `QueryClientConfig` to customize the QueryClient instance:
41+
42+
```tsx /getQueryClient/
43+
import { getQueryClient } from '@suspensive/react-query'
44+
import { QueryClientProvider } from '@tanstack/react-query'
45+
46+
function Providers({ children }: { children: React.ReactNode }) {
47+
const queryClient = getQueryClient({
48+
defaultOptions: {
49+
queries: {
50+
staleTime: 5000,
51+
retry: 3,
52+
},
53+
},
54+
})
55+
return (
56+
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
57+
)
58+
}
59+
```
60+
61+
<Callout type='warning'>
62+
63+
**Browser Environment Behavior**
64+
65+
In the browser environment, the config parameter is only used when creating the initial QueryClient instance. Subsequent calls with different configs will return the existing instance and the new config will be ignored. This is intentional to maintain a stable singleton across re-renders.
66+
67+
</Callout>
68+
69+
## Server-Side Behavior
70+
71+
### Preventing Security Issues (Highest Priority)
72+
73+
Sharing a QueryClient between requests on the server can lead to serious security vulnerabilities:
74+
75+
- **Data leakage between users**: One user's data may be exposed to another user
76+
- **Sensitive information exposure**: Authentication tokens, personal information, etc. may be included in another user's request
77+
- **Incorrect cache state**: Cached data from a previous request may be incorrectly returned to the next request
78+
79+
`getQueryClient` creates a new QueryClient instance for each request to completely prevent these security issues.
80+
81+
> 💡 **Related documentation**: For more information about why QueryClient should not be shared between requests on the server, see the [TanStack Query SSR Guide - Initial Setup](https://tanstack.com/query/latest/docs/framework/react/guides/ssr#initial-setup) section.
82+
83+
### Setting Cache Removal Time to Prevent OOM
84+
85+
On the server, `getQueryClient` sets the cache removal time to `Infinity` to prevent memory leaks and OOM (Out of Memory) errors. This is an additional safety measure to address memory issues that can occur when servers handle many concurrent requests.
86+
87+
`gcTime` (v5) or `cacheTime` (v4) represents the time until TanStack Query removes query data from the QueryClient's cache. By setting this to `Infinity` on the server:
88+
89+
- Data is not automatically removed from the cache, keeping it available until the request completes
90+
- The [`isValidTimeout`](https://github.com/TanStack/query/blob/main/packages/query-core/src/utils.ts) check in TanStack Query prevents `setTimeout` scheduling in [`scheduleGc`](https://github.com/TanStack/query/blob/main/packages/query-core/src/removable.ts#L13-L21), completely eliminating the performance cost that would occur with many concurrent requests
91+
92+
This behavior is **automatic and cannot be overridden** - even if you provide a different cache removal time value in your config, it will be set to `Infinity` on the server. (In v4, the `cacheTime` option is used; in v5, the `gcTime` option is used. See the Version Differences section below for details.)
93+
94+
> 💡 **Related documentation**: For more information about high memory consumption on server and why `gcTime` defaults to `Infinity` on the server, see the [TanStack Query SSR Guide - High memory consumption on server](https://tanstack.com/query/latest/docs/framework/react/guides/ssr#high-memory-consumption-on-server) section.
95+
96+
```tsx /getQueryClient/
97+
import { getQueryClient } from '@suspensive/react-query'
98+
99+
// Server environment
100+
const queryClient = getQueryClient({
101+
defaultOptions: {
102+
queries: {
103+
// On server, cache removal time is set to Infinity
104+
// to keep cache alive until the request completes and prevent setTimeout scheduling
105+
gcTime: 5000, // v5: This will be overridden to Infinity on server
106+
// In v4, cacheTime is used and also overridden to Infinity
107+
},
108+
},
109+
})
110+
111+
// On server: Cache removal time is always Infinity (OOM prevention)
112+
// On browser: Uses the provided value (or default)
113+
```
114+
115+
### New Instance Per Request
116+
117+
On the server, `getQueryClient` creates a new QueryClient instance for each call. This ensures that:
118+
119+
- Each request has isolated cache state
120+
- No data leaks between different user requests
121+
- Proper cleanup when the request completes
122+
123+
```tsx /getQueryClient/
124+
// Server environment
125+
const queryClient1 = getQueryClient()
126+
const queryClient2 = getQueryClient()
127+
128+
// queryClient1 !== queryClient2 (different instances)
129+
```
130+
131+
## Browser-Side Behavior
132+
133+
### Singleton Pattern
134+
135+
In the browser, `getQueryClient` returns the same QueryClient instance across multiple calls. This ensures:
136+
137+
- Cache is preserved across re-renders
138+
- Consistent state management
139+
- Optimal performance
140+
141+
```tsx /getQueryClient/
142+
// Browser environment
143+
const queryClient1 = getQueryClient()
144+
const queryClient2 = getQueryClient()
145+
146+
// queryClient1 === queryClient2 (same instance)
147+
```
148+
149+
## Use Cases
150+
151+
### Next.js App Router
152+
153+
```tsx /getQueryClient/
154+
// app/providers.tsx
155+
'use client'
156+
157+
import { getQueryClient } from '@suspensive/react-query'
158+
import { QueryClientProvider } from '@tanstack/react-query'
159+
import { ReactNode } from 'react'
160+
161+
export function Providers({ children }: { children: ReactNode }) {
162+
const queryClient = getQueryClient()
163+
return (
164+
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
165+
)
166+
}
167+
```
168+
169+
```tsx
170+
// app/layout.tsx
171+
import { Providers } from './providers'
172+
173+
export default function RootLayout({
174+
children,
175+
}: {
176+
children: React.ReactNode
177+
}) {
178+
return (
179+
<html lang="en">
180+
<body>
181+
<Providers>{children}</Providers>
182+
</body>
183+
</html>
184+
)
185+
}
186+
```
187+
188+
### With Custom Configuration
189+
190+
```tsx /getQueryClient/
191+
import { getQueryClient } from '@suspensive/react-query'
192+
import { QueryClientProvider } from '@tanstack/react-query'
193+
194+
function Providers({ children }: { children: React.ReactNode }) {
195+
const queryClient = getQueryClient({
196+
defaultOptions: {
197+
queries: {
198+
staleTime: 1000 * 60 * 5, // 5 minutes
199+
gcTime: 1000 * 60 * 10, // 10 minutes (v5)
200+
// cacheTime: 1000 * 60 * 10, // 10 minutes (v4)
201+
retry: 2,
202+
refetchOnWindowFocus: false,
203+
},
204+
mutations: {
205+
retry: 1,
206+
},
207+
},
208+
})
209+
210+
return (
211+
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
212+
)
213+
}
214+
```
215+
216+
## Version Differences
217+
218+
### @tanstack/react-query v4
219+
220+
In v4, the cache time option is called `cacheTime`:
221+
222+
```tsx
223+
// Server: cacheTime is automatically set to Infinity
224+
const queryClient = getQueryClient({
225+
defaultOptions: {
226+
queries: {
227+
cacheTime: 5000, // Overridden to Infinity on server
228+
},
229+
},
230+
})
231+
```
232+
233+
### @tanstack/react-query v5
234+
235+
In v5, the cache time option is called `gcTime` (garbage collection time):
236+
237+
```tsx
238+
// Server: gcTime is automatically set to Infinity
239+
const queryClient = getQueryClient({
240+
defaultOptions: {
241+
queries: {
242+
gcTime: 5000, // Overridden to Infinity on server
243+
},
244+
},
245+
})
246+
```
247+
248+
## Important Notes
249+
250+
- **Server safety**: Always use `getQueryClient` instead of creating `new QueryClient()` directly in SSR environments to ensure proper request isolation and OOM prevention.
251+
252+
- **Browser singleton**: The browser instance is a singleton, so config changes after the first call are ignored. Configure your QueryClient on the first call.
253+
254+
- **Automatic OOM prevention**: The cache removal time override to `Infinity` on the server is automatic and cannot be disabled. This is a safety feature to prevent memory issues. (In v4, the `cacheTime` option is used; in v5, the `gcTime` option is used.)

docs/suspensive.org/src/content/ko/docs/react-query/_meta.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ export default {
1212
type: 'separator',
1313
title: 'API 문서',
1414
},
15+
getQueryClient: { title: 'getQueryClient' },
1516
mutationOptions: { title: 'mutationOptions' },
1617
usePrefetchQuery: { title: 'usePrefetchQuery' },
1718
usePrefetchInfiniteQuery: { title: 'usePrefetchInfiniteQuery' },

0 commit comments

Comments
 (0)