|
1 | 1 | 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' |
9 | 3 | 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' |
25 | 8 |
|
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 |
33 | 12 | }
|
34 |
| - ? TRouter |
35 |
| - : never |
36 | 13 |
|
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) |
43 | 18 |
|
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, |
52 | 23 | Wrap: ({ children }) => {
|
53 |
| - const OuterWrapper = additionalOpts?.WrapProvider || Fragment |
| 24 | + const OuterWrapper = opts?.WrapProvider || Fragment |
54 | 25 | const OGWrap = ogOptions.Wrap || Fragment
|
55 | 26 | return (
|
56 | 27 | <OuterWrapper>
|
57 |
| - <QueryClientProvider client={queryClient}> |
| 28 | + <QueryClientProvider client={opts.queryClient}> |
58 | 29 | <OGWrap>{children}</OGWrap>
|
59 | 30 | </QueryClientProvider>
|
60 | 31 | </OuterWrapper>
|
61 | 32 | )
|
62 | 33 | },
|
63 | 34 | }
|
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 |
| - } |
209 | 35 | }
|
0 commit comments