|
| 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.) |
0 commit comments