Skip to content

Commit 194815d

Browse files
move query integration into a separate package
also renamed init function
1 parent daa2137 commit 194815d

File tree

11 files changed

+378
-220
lines changed

11 files changed

+378
-220
lines changed

examples/react/start-basic-react-query/src/router.tsx

Lines changed: 13 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,26 @@
11
import { QueryClient } from '@tanstack/react-query'
22
import { createRouter as createTanStackRouter } from '@tanstack/react-router'
3-
import { routerWithQueryClient } from '@tanstack/react-router-ssr-query'
3+
import { setupRouterSsrQueryIntegration } from '@tanstack/react-router-ssr-query'
44
import { routeTree } from './routeTree.gen'
55
import { DefaultCatchBoundary } from './components/DefaultCatchBoundary'
66
import { NotFound } from './components/NotFound'
77

8-
// NOTE: Most of the integration code found here is experimental and will
9-
// definitely end up in a more streamlined API in the future. This is just
10-
// to show what's possible with the current APIs.
11-
128
export function createRouter() {
139
const queryClient = new QueryClient()
1410

15-
return routerWithQueryClient(
16-
createTanStackRouter({
17-
routeTree,
18-
context: { queryClient },
19-
defaultPreload: 'intent',
20-
defaultErrorComponent: DefaultCatchBoundary,
21-
defaultNotFoundComponent: () => <NotFound />,
22-
}),
11+
const router = createTanStackRouter({
12+
routeTree,
13+
context: { queryClient },
14+
defaultPreload: 'intent',
15+
defaultErrorComponent: DefaultCatchBoundary,
16+
defaultNotFoundComponent: () => <NotFound />,
17+
})
18+
setupRouterSsrQueryIntegration({
19+
router,
2320
queryClient,
24-
)
21+
})
22+
23+
return router
2524
}
2625

2726
declare module '@tanstack/react-router' {

package.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
"@playwright/test": "^1.52.0",
4141
"@tanstack/config": "^0.16.1",
4242
"@tanstack/react-query": "5.66.0",
43+
"@tanstack/query-core": "5.66.0",
4344
"@types/node": "^22.10.2",
4445
"@types/react": "^19.0.8",
4546
"@types/react-dom": "^19.0.3",
@@ -80,7 +81,8 @@
8081
"eslint": "$eslint",
8182
"vite": "$vite",
8283
"@playwright/test": "$@playwright/test",
83-
"@tanstack/react-query": "5.66.0",
84+
"@tanstack/react-query": "$@tanstack/react-query",
85+
"@tanstack/query-core": "$@tanstack/query-core",
8486
"use-sync-external-store": "1.2.2",
8587
"@tanstack/history": "workspace:*",
8688
"@tanstack/router-core": "workspace:*",
@@ -94,6 +96,7 @@
9496
"@tanstack/virtual-file-routes": "workspace:*",
9597
"@tanstack/router-plugin": "workspace:*",
9698
"@tanstack/router-vite-plugin": "workspace:*",
99+
"@tanstack/router-ssr-query-core": "workspace:*",
97100
"@tanstack/react-router-ssr-query": "workspace:*",
98101
"@tanstack/zod-adapter": "workspace:*",
99102
"@tanstack/valibot-adapter": "workspace:*",

packages/react-router-ssr-query/package.json

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -63,19 +63,21 @@
6363
"engines": {
6464
"node": ">=12"
6565
},
66+
"dependencies": {
67+
"@tanstack/router-ssr-query-core": "workspace:^"
68+
},
6669
"devDependencies": {
6770
"@vitejs/plugin-react": "^4.3.4",
6871
"react": ">=19",
6972
"react-dom": ">=19",
70-
"@tanstack/router-core": "workspace:^",
7173
"@tanstack/react-router": "workspace:^",
72-
"@tanstack/react-query": ">=5.66.0"
74+
"@tanstack/react-query": ">=5.49.0"
7375
},
7476
"peerDependencies": {
7577
"react": ">=18.0.0 || >=19.0.0",
7678
"react-dom": ">=18.0.0 || >=19.0.0",
77-
"@tanstack/router-core": ">=1.114.7",
78-
"@tanstack/react-router": ">=1.43.2",
79+
"@tanstack/query-core": ">=5.49.0",
80+
"@tanstack/react-router": ">=1.127.0",
7981
"@tanstack/react-query": ">=5.49.2"
8082
}
8183
}
Lines changed: 18 additions & 192 deletions
Original file line numberDiff line numberDiff line change
@@ -1,209 +1,35 @@
11
import { Fragment } from 'react'
2-
import {
3-
QueryClientProvider,
4-
dehydrate as queryDehydrate,
5-
hydrate as queryHydrate,
6-
} from '@tanstack/react-query'
7-
import { isRedirect } from '@tanstack/router-core'
8-
import '@tanstack/router-core/ssr/client'
2+
import { QueryClientProvider } from '@tanstack/react-query'
93
import type { AnyRouter } from '@tanstack/react-router'
10-
import type {
11-
QueryClient,
12-
DehydratedState as QueryDehydratedState,
13-
} from '@tanstack/react-query'
14-
15-
type AdditionalOptions = {
16-
WrapProvider?: (props: { children: any }) => React.JSX.Element
17-
/**
18-
* If `true`, the QueryClient will handle errors thrown by `redirect()` inside of mutations and queries.
19-
*
20-
* @default true
21-
* @link [Guide](https://tanstack.com/router/latest/docs/framework/react/api/router/redirectFunction)
22-
*/
23-
handleRedirects?: boolean
24-
}
4+
import {
5+
RouterSsrQueryOptions,
6+
setupCoreRouterSsrQueryIntegration,
7+
} from '@tanstack/router-ssr-query-core'
258

26-
type DehydratedRouterQueryState = {
27-
dehydratedQueryClient: QueryDehydratedState
28-
queryStream: ReadableStream<QueryDehydratedState>
29-
}
30-
export type ValidateRouter<TRouter extends AnyRouter> =
31-
NonNullable<TRouter['options']['context']> extends {
32-
queryClient: QueryClient
9+
export type Options<TRouter extends AnyRouter> =
10+
RouterSsrQueryOptions<TRouter> & {
11+
WrapProvider?: (props: { children: any }) => React.JSX.Element
3312
}
34-
? TRouter
35-
: never
3613

37-
export function routerWithQueryClient<TRouter extends AnyRouter>(
38-
router: ValidateRouter<TRouter>,
39-
queryClient: QueryClient,
40-
additionalOpts?: AdditionalOptions,
41-
): TRouter {
42-
const ogOptions = router.options
14+
export function setupRouterSsrQueryIntegration<TRouter extends AnyRouter>(
15+
opts: Options<TRouter>,
16+
) {
17+
setupCoreRouterSsrQueryIntegration(opts)
4318

44-
router.options = {
45-
...router.options,
46-
context: {
47-
...ogOptions.context,
48-
// Pass the query client to the context, so we can access it in loaders
49-
queryClient,
50-
},
51-
// Wrap the app in a QueryClientProvider
19+
const ogOptions = opts.router.options
20+
21+
opts.router.options = {
22+
...ogOptions,
5223
Wrap: ({ children }) => {
53-
const OuterWrapper = additionalOpts?.WrapProvider || Fragment
24+
const OuterWrapper = opts?.WrapProvider || Fragment
5425
const OGWrap = ogOptions.Wrap || Fragment
5526
return (
5627
<OuterWrapper>
57-
<QueryClientProvider client={queryClient}>
28+
<QueryClientProvider client={opts.queryClient}>
5829
<OGWrap>{children}</OGWrap>
5930
</QueryClientProvider>
6031
</OuterWrapper>
6132
)
6233
},
6334
}
64-
65-
if (router.isServer) {
66-
const queryStream = createPushableStream()
67-
68-
router.options.dehydrate =
69-
async (): Promise<DehydratedRouterQueryState> => {
70-
const ogDehydrated = await ogOptions.dehydrate?.()
71-
const dehydratedQueryClient = queryDehydrate(queryClient)
72-
73-
router.serverSsr!.onRenderFinished(() => queryStream.close())
74-
75-
const dehydratedRouter = {
76-
...ogDehydrated,
77-
// When critical data is dehydrated, we also dehydrate the query client
78-
dehydratedQueryClient,
79-
// prepare the stream for queries coming up during rendering
80-
queryStream: queryStream.stream,
81-
}
82-
83-
return dehydratedRouter
84-
}
85-
86-
const ogClientOptions = queryClient.getDefaultOptions()
87-
queryClient.setDefaultOptions({
88-
...ogClientOptions,
89-
dehydrate: {
90-
shouldDehydrateQuery: () => true,
91-
...ogClientOptions.dehydrate,
92-
},
93-
})
94-
95-
queryClient.getQueryCache().subscribe((event) => {
96-
if (event.type === 'added') {
97-
// before rendering starts, we do not stream individual queries
98-
// instead we dehydrate the entire query client in router's dehydrate()
99-
if (!router.serverSsr!.isDehydrated()) {
100-
return
101-
}
102-
if (queryStream.isClosed()) {
103-
console.warn(
104-
`tried to stream query ${event.query.queryHash} after stream was already closed`,
105-
)
106-
return
107-
}
108-
queryStream.enqueue(
109-
queryDehydrate(queryClient, {
110-
shouldDehydrateQuery: (query) => {
111-
if (query.queryHash === event.query.queryHash) {
112-
return (
113-
ogClientOptions.dehydrate?.shouldDehydrateQuery?.(query) ??
114-
true
115-
)
116-
}
117-
return false
118-
},
119-
}),
120-
)
121-
}
122-
})
123-
// on the client
124-
} else {
125-
router.options.hydrate = async (dehydrated: DehydratedRouterQueryState) => {
126-
await ogOptions.hydrate?.(dehydrated)
127-
// On the client, hydrate the query client with the dehydrated data
128-
queryHydrate(queryClient, dehydrated.dehydratedQueryClient)
129-
130-
const reader = dehydrated.queryStream.getReader()
131-
reader
132-
.read()
133-
.then(async function handle({ done, value }) {
134-
queryHydrate(queryClient, value)
135-
if (done) {
136-
return
137-
}
138-
const result = await reader.read()
139-
return handle(result)
140-
})
141-
.catch((err) => {
142-
console.error('Error reading query stream:', err)
143-
})
144-
}
145-
if (additionalOpts?.handleRedirects ?? true) {
146-
const ogMutationCacheConfig = queryClient.getMutationCache().config
147-
queryClient.getMutationCache().config = {
148-
...ogMutationCacheConfig,
149-
onError: (error, _variables, _context, _mutation) => {
150-
if (isRedirect(error)) {
151-
error.options._fromLocation = router.state.location
152-
return router.navigate(router.resolveRedirect(error).options)
153-
}
154-
155-
return ogMutationCacheConfig.onError?.(
156-
error,
157-
_variables,
158-
_context,
159-
_mutation,
160-
)
161-
},
162-
}
163-
164-
const ogQueryCacheConfig = queryClient.getQueryCache().config
165-
queryClient.getQueryCache().config = {
166-
...ogQueryCacheConfig,
167-
onError: (error, _query) => {
168-
if (isRedirect(error)) {
169-
error.options._fromLocation = router.state.location
170-
return router.navigate(router.resolveRedirect(error).options)
171-
}
172-
173-
return ogQueryCacheConfig.onError?.(error, _query)
174-
},
175-
}
176-
}
177-
}
178-
179-
return router
180-
}
181-
182-
type PushableStream = {
183-
stream: ReadableStream
184-
enqueue: (chunk: unknown) => void
185-
close: () => void
186-
isClosed: () => boolean
187-
error: (err: unknown) => void
188-
}
189-
190-
function createPushableStream(): PushableStream {
191-
let controllerRef: ReadableStreamDefaultController
192-
const stream = new ReadableStream({
193-
start(controller) {
194-
controllerRef = controller
195-
},
196-
})
197-
let _isClosed = false
198-
199-
return {
200-
stream,
201-
enqueue: (chunk) => controllerRef.enqueue(chunk),
202-
close: () => {
203-
controllerRef.close()
204-
_isClosed = true
205-
},
206-
isClosed: () => _isClosed,
207-
error: (err: unknown) => controllerRef.error(err),
208-
}
20935
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<img src="https://static.scarf.sh/a.png?x-pxid=d988eb79-b0fc-4a2b-8514-6a1ab932d188" />
2+
3+
# TanStack Router Core
4+
5+
See [https://tanstack.com/router](https://tanstack.com/router) for documentation.
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
// @ts-check
2+
3+
import rootConfig from '../../eslint.config.js'
4+
5+
export default [
6+
...rootConfig,
7+
{
8+
files: ['src/**/*.{ts,tsx}', 'tests/**/*.{ts,tsx}'],
9+
rules: {
10+
'unused-imports/no-unused-vars': 'off',
11+
'@typescript-eslint/no-unnecessary-condition': 'off',
12+
},
13+
},
14+
]

0 commit comments

Comments
 (0)