Skip to content

Commit 23ae3db

Browse files
fix
1 parent 24a12ee commit 23ae3db

File tree

18 files changed

+201
-252
lines changed

18 files changed

+201
-252
lines changed

docs/router/framework/react/guide/external-data-loading.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,7 @@ Tools that are able can integrate with TanStack Router's convenient Dehydration/
141141

142142
**For critical data needed for the first render/paint**, TanStack Router supports **`dehydrate` and `hydrate`** options when configuring the `Router`. These callbacks are functions that are automatically called on the server and client when the router dehydrates and hydrates normally and allow you to augment the dehydrated data with your own data.
143143

144-
The `dehydrate` function can return any serializable JSON data which will get merged and injected into the dehydrated payload that is sent to the client. This payload is delivered via the `DehydrateRouter` component which, when rendered, provides the data back to you in the `hydrate` function on the client.
144+
The `dehydrate` function can return any serializable JSON data which will get merged and injected into the dehydrated payload that is sent to the client.
145145

146146
For example, let's dehydrate and hydrate a TanStack Query `QueryClient` so that our data we fetched on the server will be available for hydration on the client.
147147

packages/react-router-with-query/src/index.tsx

Lines changed: 105 additions & 96 deletions
Original file line numberDiff line numberDiff line change
@@ -39,100 +39,93 @@ export function routerWithQueryClient<TRouter extends AnyRouter>(
3939
queryClient: QueryClient,
4040
additionalOpts?: AdditionalOptions,
4141
): TRouter {
42+
const ogOptions = router.options
43+
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
52+
Wrap: ({ children }) => {
53+
const OuterWrapper = additionalOpts?.WrapProvider || Fragment
54+
const OGWrap = ogOptions.Wrap || Fragment
55+
return (
56+
<OuterWrapper>
57+
<QueryClientProvider client={queryClient}>
58+
<OGWrap>{children}</OGWrap>
59+
</QueryClientProvider>
60+
</OuterWrapper>
61+
)
62+
},
63+
}
64+
4265
let queryStream: PushableStream
4366

4467
if (router.isServer) {
45-
queryStream = createPushableStream()
46-
}
68+
router.options.dehydrate =
69+
async (): Promise<DehydratedRouterQueryState> => {
70+
const ogDehydrated = await ogOptions.dehydrate?.()
71+
const dehydratedQueryClient = queryDehydrate(queryClient)
4772

48-
const ogClientOptions = queryClient.getDefaultOptions()
49-
queryClient.setDefaultOptions({
50-
...ogClientOptions,
51-
dehydrate: {
52-
shouldDehydrateQuery: () => true,
53-
...ogClientOptions.dehydrate,
54-
},
55-
})
73+
router.serverSsr!.onRenderFinished(() => queryStream.close())
5674

57-
if (additionalOpts?.handleRedirects ?? true) {
58-
const ogMutationCacheConfig = queryClient.getMutationCache().config
59-
queryClient.getMutationCache().config = {
60-
...ogMutationCacheConfig,
61-
onError: (error, _variables, _context, _mutation) => {
62-
if (isRedirect(error)) {
63-
error.options._fromLocation = router.state.location
64-
return router.navigate(router.resolveRedirect(error).options)
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,
6581
}
6682

67-
return ogMutationCacheConfig.onError?.(
68-
error,
69-
_variables,
70-
_context,
71-
_mutation,
72-
)
73-
},
74-
}
83+
return dehydratedRouter
84+
}
7585

76-
const ogQueryCacheConfig = queryClient.getQueryCache().config
77-
queryClient.getQueryCache().config = {
78-
...ogQueryCacheConfig,
79-
onError: (error, _query) => {
80-
if (isRedirect(error)) {
81-
error.options._fromLocation = router.state.location
82-
return router.navigate(router.resolveRedirect(error).options)
83-
}
86+
queryStream = createPushableStream()
8487

85-
return ogQueryCacheConfig.onError?.(error, _query)
88+
const ogClientOptions = queryClient.getDefaultOptions()
89+
queryClient.setDefaultOptions({
90+
...ogClientOptions,
91+
dehydrate: {
92+
shouldDehydrateQuery: () => true,
93+
...ogClientOptions.dehydrate,
8694
},
87-
}
88-
if (router.isServer) {
89-
queryClient.getQueryCache().subscribe((event) => {
90-
if (event.type === 'added') {
91-
if (!router.serverSsr!.isDehydrated()) {
92-
return
93-
}
94-
if (queryStream!.isClosed) {
95-
console.warn(
96-
`tried to stream query ${event.query.queryHash} after stream was already closed`,
97-
)
98-
}
99-
queryStream!.enqueue(
100-
queryDehydrate(queryClient, {
101-
shouldDehydrateQuery: (query) => {
102-
if (query.queryHash === event.query.queryHash) {
103-
return (
104-
ogClientOptions.dehydrate?.shouldDehydrateQuery?.(query) ??
105-
true
106-
)
107-
}
108-
return false
109-
},
110-
}),
95+
})
96+
97+
queryClient.getQueryCache().subscribe((event) => {
98+
if (event.type === 'added') {
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`,
111105
)
112106
}
113-
})
114-
}
115-
}
116-
117-
const ogOptions = router.options
118-
router.options = {
119-
...router.options,
120-
dehydrate: async (): Promise<DehydratedRouterQueryState> => {
121-
router.serverSsr!.onRenderFinished(() => queryStream.close())
122-
const ogDehydrated = await ogOptions.dehydrate?.()
123-
return {
124-
...ogDehydrated,
125-
// When critical data is dehydrated, we also dehydrate the query client
126-
dehydratedQueryClient: queryDehydrate(queryClient),
127-
// prepare the stream for queries coming up during rendering
128-
queryStream: queryStream.stream,
107+
queryStream!.enqueue(
108+
queryDehydrate(queryClient, {
109+
shouldDehydrateQuery: (query) => {
110+
if (query.queryHash === event.query.queryHash) {
111+
return (
112+
ogClientOptions.dehydrate?.shouldDehydrateQuery?.(query) ??
113+
true
114+
)
115+
}
116+
return false
117+
},
118+
}),
119+
)
129120
}
130-
},
131-
hydrate: async (dehydrated: DehydratedRouterQueryState) => {
132-
console.log('dehydrated', dehydrated)
121+
})
122+
// on the client
123+
} else {
124+
router.options.hydrate = async (dehydrated: DehydratedRouterQueryState) => {
133125
await ogOptions.hydrate?.(dehydrated)
134126
// On the client, hydrate the query client with the dehydrated data
135127
queryHydrate(queryClient, dehydrated.dehydratedQueryClient)
128+
136129
const reader = dehydrated.queryStream.getReader()
137130
reader.read().then(function handle({ done, value }): Promise<void> {
138131
queryHydrate(queryClient, value)
@@ -141,24 +134,39 @@ export function routerWithQueryClient<TRouter extends AnyRouter>(
141134
}
142135
return reader.read().then(handle)
143136
})
144-
},
145-
context: {
146-
...ogOptions.context,
147-
// Pass the query client to the context, so we can access it in loaders
148-
queryClient,
149-
},
150-
// Wrap the app in a QueryClientProvider
151-
Wrap: ({ children }) => {
152-
const OuterWrapper = additionalOpts?.WrapProvider || Fragment
153-
const OGWrap = ogOptions.Wrap || Fragment
154-
return (
155-
<OuterWrapper>
156-
<QueryClientProvider client={queryClient}>
157-
<OGWrap>{children}</OGWrap>
158-
</QueryClientProvider>
159-
</OuterWrapper>
160-
)
161-
},
137+
}
138+
if (additionalOpts?.handleRedirects ?? true) {
139+
const ogMutationCacheConfig = queryClient.getMutationCache().config
140+
queryClient.getMutationCache().config = {
141+
...ogMutationCacheConfig,
142+
onError: (error, _variables, _context, _mutation) => {
143+
if (isRedirect(error)) {
144+
error.options._fromLocation = router.state.location
145+
return router.navigate(router.resolveRedirect(error).options)
146+
}
147+
148+
return ogMutationCacheConfig.onError?.(
149+
error,
150+
_variables,
151+
_context,
152+
_mutation,
153+
)
154+
},
155+
}
156+
157+
const ogQueryCacheConfig = queryClient.getQueryCache().config
158+
queryClient.getQueryCache().config = {
159+
...ogQueryCacheConfig,
160+
onError: (error, _query) => {
161+
if (isRedirect(error)) {
162+
error.options._fromLocation = router.state.location
163+
return router.navigate(router.resolveRedirect(error).options)
164+
}
165+
166+
return ogQueryCacheConfig.onError?.(error, _query)
167+
},
168+
}
169+
}
162170
}
163171

164172
return router
@@ -171,6 +179,7 @@ type PushableStream = {
171179
isClosed: boolean
172180
error: (err: unknown) => void
173181
}
182+
174183
function createPushableStream(): PushableStream {
175184
let controllerRef: ReadableStreamDefaultController | undefined
176185
const stream = new ReadableStream({

packages/react-router/src/Matches.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ export function Matches() {
4848

4949
// Do not render a root Suspense during SSR or hydrating from SSR
5050
const ResolvedSuspense =
51-
router.isServer || (typeof document !== 'undefined' && router.clientSsr)
51+
router.isServer || (typeof document !== 'undefined' && router.ssr)
5252
? SafeFragment
5353
: React.Suspense
5454

packages/react-router/src/ScriptOnce.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ export function ScriptOnce({
1414

1515
return (
1616
<script
17-
className="tsr-once"
17+
className="$tsr"
1818
dangerouslySetInnerHTML={{
1919
__html: [
2020
children,

packages/react-router/src/Transitioner.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,8 @@ export function Transitioner() {
6969
// Try to load the initial location
7070
useLayoutEffect(() => {
7171
if (
72-
(typeof window !== 'undefined' && router.clientSsr) ||
72+
// if we are hydrating from SSR, loading is triggered in ssr-client
73+
(typeof window !== 'undefined' && router.ssr) ||
7374
(mountLoadForRouter.current.router === router &&
7475
mountLoadForRouter.current.mounted)
7576
) {

packages/router-core/src/router.ts

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -653,10 +653,9 @@ export interface ServerSsr {
653653
getScript: () => string | Promise<string>,
654654
opts?: { logScript?: boolean },
655655
) => Promise<void>
656-
streamValue: (key: string, value: any) => void
657-
streamedKeys: Set<string>
658656
isDehydrated: () => boolean
659657
onRenderFinished: (listener: () => void) => void
658+
dehydrate: () => Promise<void>
660659
}
661660

662661
export type AnyRouterWithContext<TContext> = RouterCore<
@@ -3010,10 +3009,6 @@ export class RouterCore<
30103009

30113010
serverSsr?: ServerSsr
30123011

3013-
clientSsr?: {
3014-
getStreamedValue: <T>(key: string) => T | undefined
3015-
}
3016-
30173012
_handleNotFound = (
30183013
matches: Array<AnyRouteMatch>,
30193014
err: NotFoundError,

packages/router-core/src/ssr/createRequestHandler.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { createMemoryHistory } from '@tanstack/history'
22
import { mergeHeaders } from './headers'
3-
import { attachRouterServerSsrUtils, dehydrateRouter } from './ssr-server'
3+
import { attachRouterServerSsrUtils } from './ssr-server'
44
import type { HandlerCallback } from './handlerCallback'
55
import type { AnyRouter } from '../router'
66
import type { Manifest } from '../manifest'
@@ -39,7 +39,7 @@ export function createRequestHandler<TRouter extends AnyRouter>({
3939

4040
await router.load()
4141

42-
await dehydrateRouter(router)
42+
await router.serverSsr?.dehydrate()
4343

4444
const responseHeaders = getRequestHeaders({
4545
router,

packages/router-core/src/ssr/serializeString.ts

Lines changed: 0 additions & 52 deletions
This file was deleted.

0 commit comments

Comments
 (0)