diff --git a/docs/framework/react/api/router/RouterOptionsType.md b/docs/framework/react/api/router/RouterOptionsType.md
index d3c6f4ad96..5de8c3bdaa 100644
--- a/docs/framework/react/api/router/RouterOptionsType.md
+++ b/docs/framework/react/api/router/RouterOptionsType.md
@@ -276,23 +276,6 @@ const router = createRouter({
- Type: `(err: TSerializedError) => unknown`
- This method is called to define how errors are deserialized from the router's dehydrated state.
-### `transformer` property
-
-- Type: `RouterTransformer`
-- Optional
-- The transformer that will be used when sending data between the server and the client during SSR.
-- Defaults to a very lightweight transformer that supports a few basic types. See the [SSR guide](../../guide/ssr.md) for more information.
-
-#### `transformer.stringify` method
-
-- Type: `(obj: unknown) => string`
-- This method is called when stringifying data to be sent to the client.
-
-#### `transformer.parse` method
-
-- Type: `(str: string) => unknown`
-- This method is called when parsing the string encoded by the server.
-
### `trailingSlash` property
- Type: `'always' | 'never' | 'preserve'`
diff --git a/docs/framework/react/guide/ssr.md b/docs/framework/react/guide/ssr.md
index 8959423a8b..997d918c7d 100644
--- a/docs/framework/react/guide/ssr.md
+++ b/docs/framework/react/guide/ssr.md
@@ -201,28 +201,32 @@ This pattern can be useful for pages that have slow or high-latency data fetchin
Streaming dehydration/hydration is an advanced pattern that goes beyond markup and allows you to dehydrate and stream any supporting data from the server to the client and rehydrate it on arrival. This is useful for applications that may need to further use/manage the underlying data that was used to render the initial markup on the server.
-## Data Transformers
+## Data Serialization
-When using SSR, data passed between the server and the client must be serialized before it is sent across network-boundaries. By default, TanStack Router will serialize data using a very lightweight serializer that supports a few basic types beyond JSON.stringify/JSON.parse.
+When using SSR, data passed between the server and the client must be serialized before it is sent across network-boundaries. TanStack Router handles this serialization using a very lightweight serializer that supports common data types beyond JSON.stringify/JSON.parse.
Out of the box, the following types are supported:
-- `Date`
- `undefined`
+- `Date`
+- `Error`
+- `FormData`
If you feel that there are other types that should be supported by default, please open an issue on the TanStack Router repository.
-If you are using more complex data types like `Map`, `Set`, `BigInt`, etc, you may need to use a custom serializer to ensure that your type-definitions are accurate and your data is correctly serialized and deserialized. This is where the `transformer` option on `createRouter` comes in.
+If you are using more complex data types like `Map`, `Set`, `BigInt`, etc, you may need to use a custom serializer to ensure that your type-definitions are accurate and your data is correctly serialized and deserialized. We are currently working on both a more robust serializer and a way to customize the serializer for your application. Open an issue if you are interested in helping out!
+
+
-The Data Transformer API allows the usage of a custom serializer that can allow us to transparently use these data types when communicating across the network.
+The Data Serialization API allows the usage of a custom serializer that can allow us to transparently use these data types when communicating across the network.
-The following example shows usage with [SuperJSON](https://github.com/blitz-js/superjson), however, anything that implements [`Router Transformer`](../api/router/RouterOptionsType.md#transformer-property) can be used.
+
```tsx
import { SuperJSON } from 'superjson'
const router = createRouter({
- transformer: SuperJSON,
+ serializer: SuperJSON,
})
```
diff --git a/e2e/start/basic/app/routes/stream.tsx b/e2e/start/basic/app/routes/stream.tsx
index 7a6401ac3d..49383bdd55 100644
--- a/e2e/start/basic/app/routes/stream.tsx
+++ b/e2e/start/basic/app/routes/stream.tsx
@@ -1,5 +1,5 @@
import { Await, createFileRoute } from '@tanstack/react-router'
-import React, { useEffect, useState } from 'react'
+import { useEffect, useState } from 'react'
export const Route = createFileRoute('/stream')({
component: Home,
@@ -9,8 +9,11 @@ export const Route = createFileRoute('/stream')({
setTimeout(() => resolve('promise-data'), 150),
),
stream: new ReadableStream({
- start(controller) {
- controller.enqueue('stream-data')
+ async start(controller) {
+ for (let i = 0; i < 5; i++) {
+ await new Promise((resolve) => setTimeout(resolve, 200))
+ controller.enqueue(`stream-data-${i} `)
+ }
controller.close()
},
}),
@@ -18,40 +21,41 @@ export const Route = createFileRoute('/stream')({
},
})
+const decoder = new TextDecoder('utf-8')
+
function Home() {
const { promise, stream } = Route.useLoaderData()
-
- const [streamData, setStreamData] = useState('')
+ const [streamData, setStreamData] = useState([])
useEffect(() => {
async function fetchStream() {
const reader = stream.getReader()
- const decoder = new TextDecoder('utf-8')
- let result = ''
- let done = false
+ let chunk
- while (!done) {
- const { value, done: readerDone } = await reader.read()
- done = readerDone
- if (value) {
- result += decoder.decode(value, { stream: !done })
- }
+ while (!(chunk = await reader.read()).done) {
+ const decoded = decoder.decode(chunk.value, { stream: !chunk.done })
+ setStreamData((prev) => [...prev, decoded])
}
- setStreamData(result)
}
fetchStream()
}, [])
return (
- (
-
-
{promiseData}
-
{streamData}
-
- )}
- >
+ <>
+ (
+
+ {promiseData}
+
+ {streamData.map((d) => (
+
{d}
+ ))}
+
+
+ )}
+ />
+ >
)
}
diff --git a/examples/react/basic-ssr-streaming-file-based/src/router.tsx b/examples/react/basic-ssr-streaming-file-based/src/router.tsx
index 07c33c6d9f..5fef76e635 100644
--- a/examples/react/basic-ssr-streaming-file-based/src/router.tsx
+++ b/examples/react/basic-ssr-streaming-file-based/src/router.tsx
@@ -1,7 +1,7 @@
import { createRouter as createReactRouter } from '@tanstack/react-router'
import { routeTree } from './routeTree.gen'
-import SuperJSON from 'superjson'
+// import SuperJSON from 'superjson'
export function createRouter() {
return createReactRouter({
@@ -10,7 +10,7 @@ export function createRouter() {
head: '',
},
defaultPreload: 'intent',
- transformer: SuperJSON,
+ // serializer: SuperJSON,
})
}
diff --git a/package.json b/package.json
index 390493a1fa..e255fbb52b 100644
--- a/package.json
+++ b/package.json
@@ -44,7 +44,7 @@
"eslint-plugin-unused-imports": "^4.1.4",
"tinyglobby": "^0.2.10",
"jsdom": "^25.0.1",
- "nx": "^20.3.0",
+ "nx": "^20.3.3",
"prettier": "^3.4.2",
"publint": "^0.2.12",
"react": "^18.3.1",
@@ -65,6 +65,8 @@
},
"pnpm": {
"overrides": {
+ "react": "$react",
+ "react-dom": "$react-dom",
"eslint": "$eslint",
"vite": "$vite",
"@tanstack/react-query": "5.62.3",
diff --git a/packages/react-router-with-query/src/index.tsx b/packages/react-router-with-query/src/index.tsx
index 255cb0d536..4291c2ee98 100644
--- a/packages/react-router-with-query/src/index.tsx
+++ b/packages/react-router-with-query/src/index.tsx
@@ -49,7 +49,7 @@ export function routerWithQueryClient(
}
} else {
// On the client, pick up the deferred data from the stream
- const dehydratedClient = router.getStreamedValue(
+ const dehydratedClient = router.clientSsr!.getStreamedValue(
'__QueryClient__' + hash(options.queryKey),
)
@@ -75,7 +75,7 @@ export function routerWithQueryClient(
) {
streamedQueryKeys.add(hash(options.queryKey))
- router.streamValue(
+ router.serverSsr!.streamValue(
'__QueryClient__' + hash(options.queryKey),
dehydrate(queryClient, {
shouldDehydrateMutation: () => false,
diff --git a/packages/react-router/src/Match.tsx b/packages/react-router/src/Match.tsx
index b0602e646a..dd5e4f0ddd 100644
--- a/packages/react-router/src/Match.tsx
+++ b/packages/react-router/src/Match.tsx
@@ -10,7 +10,6 @@ import { createControlledPromise, pick } from './utils'
import { CatchNotFound, isNotFound } from './not-found'
import { isRedirect } from './redirects'
import { matchContext } from './matchContext'
-import { defaultDeserializeError, isServerSideError } from './isServerSideError'
import { SafeFragment } from './SafeFragment'
import { renderRouteNotFound } from './renderRouteNotFound'
import { rootRouteId } from './root'
@@ -157,19 +156,8 @@ export const MatchInner = React.memo(function MatchInnerImpl({
ErrorComponent
if (match.status === 'notFound') {
- let error: unknown
- if (isServerSideError(match.error)) {
- const deserializeError =
- router.options.errorSerializer?.deserialize ?? defaultDeserializeError
-
- error = deserializeError(match.error.data)
- } else {
- error = match.error
- }
-
- invariant(isNotFound(error), 'Expected a notFound error')
-
- return renderRouteNotFound(router, route, error)
+ invariant(isNotFound(match.error), 'Expected a notFound error')
+ return renderRouteNotFound(router, route, match.error)
}
if (match.status === 'redirected') {
@@ -201,13 +189,7 @@ export const MatchInner = React.memo(function MatchInnerImpl({
)
}
- if (isServerSideError(match.error)) {
- const deserializeError =
- router.options.errorSerializer?.deserialize ?? defaultDeserializeError
- throw deserializeError(match.error.data)
- } else {
- throw match.error
- }
+ throw match.error
}
if (match.status === 'pending') {
@@ -241,14 +223,7 @@ export const MatchInner = React.memo(function MatchInnerImpl({
throw router.getMatch(match.id)?.loadPromise
}
- return (
- <>
- {out}
- {router.AfterEachMatch ? (
-
- ) : null}
- >
- )
+ return out
})
export const Outlet = React.memo(function OutletImpl() {
diff --git a/packages/react-router/src/Matches.tsx b/packages/react-router/src/Matches.tsx
index 324ac95686..a70ed9e9a8 100644
--- a/packages/react-router/src/Matches.tsx
+++ b/packages/react-router/src/Matches.tsx
@@ -219,7 +219,7 @@ export function Matches() {
// Do not render a root Suspense during SSR or hydrating from SSR
const ResolvedSuspense =
- router.isServer || (typeof document !== 'undefined' && window.__TSR__)
+ router.isServer || (typeof document !== 'undefined' && router.clientSsr)
? SafeFragment
: React.Suspense
diff --git a/packages/react-router/src/RouterProvider.tsx b/packages/react-router/src/RouterProvider.tsx
index e5b40ebaa2..59f8e53347 100644
--- a/packages/react-router/src/RouterProvider.tsx
+++ b/packages/react-router/src/RouterProvider.tsx
@@ -54,8 +54,6 @@ export type BuildLocationFn = <
},
) => ParsedLocation
-export type InjectedHtmlEntry = string | (() => Promise | string)
-
export function RouterContextProvider<
TRouter extends AnyRouter = RegisteredRouter,
TDehydrated extends Record = Record,
@@ -79,7 +77,9 @@ export function RouterContextProvider<
const routerContext = getRouterContext()
const provider = (
- {children}
+
+ {children}
+
)
if (router.options.Wrap) {
diff --git a/packages/react-router/src/ScriptOnce.tsx b/packages/react-router/src/ScriptOnce.tsx
index 4a851a7e6a..2ad9ea23b2 100644
--- a/packages/react-router/src/ScriptOnce.tsx
+++ b/packages/react-router/src/ScriptOnce.tsx
@@ -1,25 +1,17 @@
import jsesc from 'jsesc'
-import { useRouter } from './useRouter'
export function ScriptOnce({
children,
log,
- sync,
}: {
children: string
log?: boolean
sync?: boolean
}) {
- const router = useRouter()
if (typeof document !== 'undefined') {
return null
}
- if (!sync) {
- router.injectScript(children, { logScript: log })
- return null
- }
-
return (
`,
- )
- }
-
- streamedKeys: Set = new Set()
-
- getStreamedValue = (key: string): T | undefined => {
- if (this.isServer) {
- return undefined
- }
-
- const streamedValue = window.__TSR__?.streamedValues[key]
-
- if (!streamedValue) {
- return
- }
-
- if (!streamedValue.parsed) {
- streamedValue.parsed = this.options.transformer.parse(streamedValue.value)
- }
-
- return streamedValue.parsed
- }
-
- streamValue = (key: string, value: any) => {
- warning(
- !this.streamedKeys.has(key),
- 'Key has already been streamed: ' + key,
- )
-
- this.streamedKeys.add(key)
- this.injectScript(
- `__TSR__.streamedValues['${key}'] = { value: ${this.serializer?.(this.options.transformer.stringify(value))}}`,
- )
+ clientSsr?: {
+ getStreamedValue: (key: string) => T | undefined
}
_handleNotFound = (
diff --git a/packages/react-router/src/routerContext.tsx b/packages/react-router/src/routerContext.tsx
index 1e5ed33540..8c54b1dc44 100644
--- a/packages/react-router/src/routerContext.tsx
+++ b/packages/react-router/src/routerContext.tsx
@@ -1,5 +1,11 @@
import * as React from 'react'
-import type { Router } from './router'
+import type { AnyRouter, Router } from './router'
+
+declare global {
+ interface Window {
+ __TSR_ROUTER_CONTEXT__?: React.Context
+ }
+}
const routerContext = React.createContext>(null!)
diff --git a/packages/react-router/src/serializer.ts b/packages/react-router/src/serializer.ts
new file mode 100644
index 0000000000..f315cdec33
--- /dev/null
+++ b/packages/react-router/src/serializer.ts
@@ -0,0 +1,24 @@
+export interface StartSerializer {
+ stringify: (obj: unknown) => string
+ parse: (str: string) => unknown
+ encode: (value: T) => T
+ decode: (value: T) => T
+}
+
+export type SerializerStringifyBy = T extends TSerializable
+ ? T
+ : T extends (...args: Array) => any
+ ? 'Function is not serializable'
+ : { [K in keyof T]: SerializerStringifyBy }
+
+export type SerializerParseBy = T extends TSerializable
+ ? T
+ : T extends React.JSX.Element
+ ? ReadableStream
+ : { [K in keyof T]: SerializerParseBy }
+
+export type Serializable = Date | undefined | Error | FormData
+
+export type SerializerStringify = SerializerStringifyBy
+
+export type SerializerParse = SerializerParseBy
diff --git a/packages/react-router/tests/router.test.tsx b/packages/react-router/tests/router.test.tsx
index 8f6ffcb67a..c14da8e9e0 100644
--- a/packages/react-router/tests/router.test.tsx
+++ b/packages/react-router/tests/router.test.tsx
@@ -743,17 +743,6 @@ describe('router rendering stability', () => {
})
})
-describe('transformer functions are defined', () => {
- it('should have default transformer functions', () => {
- const rootRoute = createRootRoute({})
- const routeTree = rootRoute.addChildren([])
- const router = createRouter({ routeTree })
-
- expect(router.options.transformer.parse).toBeInstanceOf(Function)
- expect(router.options.transformer.stringify).toBeInstanceOf(Function)
- })
-})
-
describe('router matches URLs to route definitions', () => {
it('solo splat route matches index route', async () => {
const { router } = createTestRouter({
diff --git a/packages/react-router/tsconfig.json b/packages/react-router/tsconfig.json
index 2c71ee70f6..372bb7b491 100644
--- a/packages/react-router/tsconfig.json
+++ b/packages/react-router/tsconfig.json
@@ -4,11 +4,5 @@
"jsx": "react-jsx",
"jsxImportSource": "react"
},
- "include": [
- "src",
- "tests",
- "vite.config.ts",
- "eslint.config.ts",
- "../start/src/client/DehydrateRouter.tsx"
- ]
+ "include": ["src", "tests", "vite.config.ts", "eslint.config.ts"]
}
diff --git a/packages/start-client/package.json b/packages/start-client/package.json
index b82523dada..c4adbc4930 100644
--- a/packages/start-client/package.json
+++ b/packages/start-client/package.json
@@ -61,17 +61,16 @@
"node": ">=12"
},
"dependencies": {
- "@tanstack/react-router": "workspace:^",
"@tanstack/react-cross-context": "workspace:^",
- "tiny-invariant": "^1.3.3",
+ "@tanstack/react-router": "workspace:^",
"jsesc": "^3.0.2",
+ "tiny-invariant": "^1.3.3",
"vinxi": "^0.5.1"
},
"devDependencies": {
+ "@testing-library/react": "^16.1.0",
"@types/jsesc": "^3.0.3",
- "@vitejs/plugin-react": "^4.3.4",
- "esbuild": "^0.24.2",
- "@testing-library/react": "^16.1.0"
+ "@vitejs/plugin-react": "^4.3.4"
},
"peerDependencies": {
"react": ">=18.0.0 || >=19.0.0",
diff --git a/packages/start-client/src/DehydrateRouter.tsx b/packages/start-client/src/DehydrateRouter.tsx
deleted file mode 100644
index 4d227858e8..0000000000
--- a/packages/start-client/src/DehydrateRouter.tsx
+++ /dev/null
@@ -1,3 +0,0 @@
-export function DehydrateRouter() {
- return null
-}
diff --git a/packages/start-client/src/Meta.tsx b/packages/start-client/src/Meta.tsx
index 54e1bdb72e..ebfa96b811 100644
--- a/packages/start-client/src/Meta.tsx
+++ b/packages/start-client/src/Meta.tsx
@@ -1,9 +1,6 @@
-import { ScriptOnce, useRouter, useRouterState } from '@tanstack/react-router'
+import { useRouter, useRouterState } from '@tanstack/react-router'
import * as React from 'react'
-import jsesc from 'jsesc'
-import { Context } from '@tanstack/react-cross-context'
import { Asset } from './Asset'
-import minifiedScript from './tsrScript?script-string'
import type { RouterManagedTag } from '@tanstack/react-router'
export const useMeta = () => {
@@ -81,7 +78,7 @@ export const useMeta = () => {
state.matches
.map((match) => router.looseRoutesById[match.routeId]!)
.forEach((route) =>
- router.manifest?.routes[route.id]?.preloads
+ router.ssr?.manifest?.routes[route.id]?.preloads
?.filter(Boolean)
.forEach((preload) => {
preloadMeta.push({
@@ -108,32 +105,13 @@ export const useMeta = () => {
}
export const useMetaElements = () => {
- const router = useRouter()
const meta = useMeta()
- const dehydratedCtx = React.useContext(
- Context.get('TanStackRouterHydrationContext', {}),
- )
-
return (
<>
- {meta.map((asset, i) => (
+ {meta.map((asset) => (
))}
- <>
-
-
- >
>
)
}
diff --git a/packages/start-client/src/Scripts.tsx b/packages/start-client/src/Scripts.tsx
index 19d6b305d3..9865f1db2b 100644
--- a/packages/start-client/src/Scripts.tsx
+++ b/packages/start-client/src/Scripts.tsx
@@ -1,4 +1,4 @@
-import { useRouter, useRouterState } from '@tanstack/react-router'
+import { useRouter, useRouterState, warning } from '@tanstack/react-router'
import { Asset } from './Asset'
import type { RouterManagedTag } from '@tanstack/react-router'
@@ -8,11 +8,17 @@ export const Scripts = () => {
const assetScripts = useRouterState({
select: (state) => {
const assetScripts: Array = []
+ const manifest = router.ssr?.manifest
+
+ if (!manifest) {
+ warning(false, ' found no manifest')
+ return []
+ }
state.matches
.map((match) => router.looseRoutesById[match.routeId]!)
.forEach((route) =>
- router.manifest?.routes[route.id]?.assets
+ manifest.routes[route.id]?.assets
?.filter((d) => d.tag === 'script')
.forEach((asset) => {
assetScripts.push({
diff --git a/packages/start-client/src/StartClient.tsx b/packages/start-client/src/StartClient.tsx
index 499d81d161..8cf0378cb7 100644
--- a/packages/start-client/src/StartClient.tsx
+++ b/packages/start-client/src/StartClient.tsx
@@ -1,11 +1,10 @@
import { RouterProvider } from '@tanstack/react-router'
-import { afterHydrate } from './serialization'
+import { hydrate } from './ssr-client'
import type { AnyRouter } from '@tanstack/react-router'
export function StartClient(props: { router: AnyRouter }) {
if (!props.router.state.matches.length) {
- props.router.hydrate()
- afterHydrate({ router: props.router })
+ hydrate(props.router)
}
return
diff --git a/packages/start-client/src/createMiddleware.ts b/packages/start-client/src/createMiddleware.ts
index cdc4a09804..566a082b2c 100644
--- a/packages/start-client/src/createMiddleware.ts
+++ b/packages/start-client/src/createMiddleware.ts
@@ -2,10 +2,10 @@ import type { ConstrainValidator, Method } from './createServerFn'
import type {
Assign,
Constrain,
- DefaultTransformerStringify,
Expand,
ResolveValidatorInput,
ResolveValidatorOutput,
+ SerializerStringify,
} from '@tanstack/react-router'
export type MergeAllMiddleware<
@@ -111,7 +111,7 @@ export type MiddlewareServerNextFn = <
TNewClientAfterContext = undefined,
>(ctx?: {
context?: TNewServerContext
- sendContext?: DefaultTransformerStringify
+ sendContext?: SerializerStringify
}) => Promise<
ServerResultWithContext
>
@@ -148,7 +148,7 @@ export type MiddlewareClientNextFn = <
TNewClientContext = undefined,
>(ctx?: {
context?: TNewClientContext
- sendContext?: DefaultTransformerStringify
+ sendContext?: SerializerStringify
headers?: HeadersInit
}) => Promise>
diff --git a/packages/start-client/src/createServerFn.ts b/packages/start-client/src/createServerFn.ts
index 45f3e2a9ed..fb3723a51b 100644
--- a/packages/start-client/src/createServerFn.ts
+++ b/packages/start-client/src/createServerFn.ts
@@ -1,19 +1,15 @@
-import {
- defaultTransformer,
- isNotFound,
- isRedirect,
- warning,
-} from '@tanstack/react-router'
+import { isNotFound, isRedirect, warning } from '@tanstack/react-router'
import { mergeHeaders } from './headers'
import { globalMiddleware } from './registerGlobalMiddleware'
+import { startSerializer } from './serializer'
import type {
AnyValidator,
Constrain,
- DefaultTransformerParse,
- DefaultTransformerStringify,
Expand,
ResolveValidatorInput,
- TransformerStringify,
+ SerializerParse,
+ SerializerStringify,
+ SerializerStringifyBy,
Validator,
} from '@tanstack/react-router'
import type {
@@ -81,8 +77,8 @@ export interface OptionalFetcherDataOptions
export type FetcherData =
TResponse extends JsonResponse
- ? DefaultTransformerParse>
- : DefaultTransformerParse
+ ? SerializerParse>
+ : SerializerParse
export type RscStream = {
__cacheState: T
@@ -92,9 +88,7 @@ export type Method = 'GET' | 'POST'
export type ServerFn = (
ctx: ServerFnCtx,
-) =>
- | Promise>
- | DefaultTransformerStringify
+) => Promise> | SerializerStringify
export interface ServerFnCtx {
method: TMethod
@@ -125,8 +119,8 @@ type ServerFnBaseOptions<
functionId: string
}
-export type ValidatorTransformerStringify = Validator<
- TransformerStringify<
+export type ValidatorSerializerStringify = Validator<
+ SerializerStringifyBy<
ResolveValidatorInput,
Date | undefined | FormData
>,
@@ -135,7 +129,7 @@ export type ValidatorTransformerStringify = Validator<
export type ConstrainValidator = unknown extends TValidator
? TValidator
- : Constrain>
+ : Constrain>
export interface ServerFnMiddleware {
middleware: (
@@ -307,12 +301,12 @@ function extractFormDataContext(formData: FormData) {
}
try {
- const context = defaultTransformer.parse(serializedContext)
+ const context = startSerializer.parse(serializedContext)
return {
context,
data: formData,
}
- } catch (e) {
+ } catch {
return {
data: formData,
}
diff --git a/packages/start-client/src/index.tsx b/packages/start-client/src/index.tsx
index a55f07395d..bbdb5c0dfe 100644
--- a/packages/start-client/src/index.tsx
+++ b/packages/start-client/src/index.tsx
@@ -62,7 +62,6 @@ export {
globalMiddleware,
} from './registerGlobalMiddleware'
export { serverOnly, clientOnly } from './envOnly'
-export { DehydrateRouter } from './DehydrateRouter'
export { json } from './json'
export { Meta } from './Meta'
export { Scripts } from './Scripts'
@@ -71,4 +70,16 @@ export { mergeHeaders } from './headers'
export { renderRsc } from './renderRSC'
export { useServerFn } from './useServerFn'
export { serverFnFetcher } from './serverFnFetcher'
-export { serializeLoaderData, AfterEachMatch } from './serialization'
+export {
+ type DehydratedRouterState,
+ type DehydratedRouteMatch,
+ type DehydratedRouter,
+ type ClientExtractedBaseEntry,
+ type StartSsrGlobal,
+ type ClientExtractedEntry,
+ type SsrMatch,
+ type ClientExtractedPromise,
+ type ClientExtractedStream,
+ type ResolvePromiseState,
+} from './ssr-client'
+export { startSerializer } from './serializer'
diff --git a/packages/start-client/src/serialization.tsx b/packages/start-client/src/serialization.tsx
deleted file mode 100644
index 173329c1d9..0000000000
--- a/packages/start-client/src/serialization.tsx
+++ /dev/null
@@ -1,444 +0,0 @@
-import * as React from 'react'
-import {
- ScriptOnce,
- TSR_DEFERRED_PROMISE,
- createControlledPromise,
- defer,
- isPlainArray,
- isPlainObject,
- pick,
- useRouter,
-} from '@tanstack/react-router'
-import jsesc from 'jsesc'
-import invariant from 'tiny-invariant'
-import type {
- AnyRouteMatch,
- AnyRouter,
- ClientExtractedBaseEntry,
- DeferredPromise,
- StreamState,
- TSRGlobalMatch,
-} from '@tanstack/react-router'
-import type { ResolvePromiseState } from './tsrScript'
-
-export interface ServerExtractedBaseEntry extends ClientExtractedBaseEntry {
- dataType: '__beforeLoadContext' | 'loaderData'
- id: number
- matchIndex: number
-}
-
-export interface ServerExtractedStream extends ServerExtractedBaseEntry {
- type: 'stream'
- streamState: StreamState
-}
-
-export type ServerExtractedEntry =
- | ServerExtractedStream
- | ServerExtractedPromise
-export interface ServerExtractedPromise extends ServerExtractedBaseEntry {
- type: 'promise'
- promise: DeferredPromise
-}
-
-export function serializeLoaderData(
- dataType: '__beforeLoadContext' | 'loaderData',
- data: any,
- ctx: {
- match: AnyRouteMatch
- router: AnyRouter
- },
-) {
- if (!ctx.router.isServer) {
- return data
- }
-
- ;(ctx.match as any).extracted = (ctx.match as any).extracted || []
-
- const extracted = (ctx.match as any).extracted
-
- const replacedLoaderData = replaceBy(data, (value, path) => {
- const type =
- value instanceof ReadableStream
- ? 'stream'
- : value instanceof Promise
- ? 'promise'
- : undefined
-
- // If it's a stream, we need to tee it so we can read it multiple times
- if (type === 'stream') {
- const [copy1, copy2] = value.tee()
- const entry: ServerExtractedStream = {
- dataType,
- type,
- path,
- id: extracted.length,
- matchIndex: ctx.match.index,
- streamState: createStreamState({ stream: copy2 }),
- }
-
- extracted.push(entry)
- return copy1
- } else if (type === 'promise') {
- const deferredPromise = defer(value)
- const entry: ServerExtractedPromise = {
- dataType,
- type,
- path,
- id: extracted.length,
- matchIndex: ctx.match.index,
- promise: deferredPromise,
- }
- extracted.push(entry)
- }
-
- return value
- })
-
- return replacedLoaderData
-}
-
-// Right after hydration and before the first render, we need to rehydrate each match
-// This includes rehydrating the loaderData and also using the beforeLoadContext
-// to reconstruct any context that was serialized on the server
-export function afterHydrate({ router }: { router: AnyRouter }) {
- router.state.matches.forEach((match) => {
- const route = router.looseRoutesById[match.routeId]!
- const dMatch = window.__TSR__?.matches[match.index]
- if (dMatch) {
- const parentMatch = router.state.matches[match.index - 1]
- const parentContext = parentMatch?.context ?? router.options.context ?? {}
- if (dMatch.__beforeLoadContext) {
- match.__beforeLoadContext = router.options.transformer.parse(
- dMatch.__beforeLoadContext,
- ) as any
-
- match.context = {
- ...parentContext,
- ...match.context,
- ...match.__beforeLoadContext,
- }
- }
-
- if (dMatch.loaderData) {
- match.loaderData = router.options.transformer.parse(dMatch.loaderData)
- }
-
- const extracted = dMatch.extracted
-
- // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
- if (extracted) {
- Object.entries(extracted).forEach(([_, ex]: any) => {
- deepMutableSetByPath(match, ['loaderData', ...ex.path], ex.value)
- })
- }
- }
-
- const headFnContent = route.options.head?.({
- matches: router.state.matches,
- match,
- params: match.params,
- loaderData: match.loaderData,
- })
-
- Object.assign(match, {
- meta: headFnContent?.meta,
- links: headFnContent?.links,
- scripts: headFnContent?.scripts,
- })
- })
-}
-
-export function AfterEachMatch(props: { match: any; matchIndex: number }) {
- const router = useRouter()
-
- const fullMatch = router.state.matches[props.matchIndex]!
-
- if (!router.isServer) {
- return null
- }
-
- const extracted = (fullMatch as any).extracted as
- | undefined
- | Array
-
- const [serializedBeforeLoadData, serializedLoaderData] = (
- ['__beforeLoadContext', 'loaderData'] as const
- ).map((dataType) => {
- return extracted
- ? extracted.reduce(
- (acc: any, entry: ServerExtractedEntry) => {
- if (entry.dataType !== dataType) {
- return deepImmutableSetByPath(
- acc,
- ['temp', ...entry.path],
- undefined,
- )
- }
- return acc
- },
- { temp: fullMatch[dataType] },
- ).temp
- : fullMatch[dataType]
- })
-
- if (
- serializedBeforeLoadData !== undefined ||
- serializedLoaderData !== undefined ||
- extracted?.length
- ) {
- const initCode = `__TSR__.initMatch(${jsesc(
- {
- index: props.matchIndex,
- __beforeLoadContext: router.options.transformer.stringify(
- serializedBeforeLoadData,
- ),
- loaderData: router.options.transformer.stringify(serializedLoaderData),
- extracted: extracted
- ? Object.fromEntries(
- extracted.map((entry) => {
- return [entry.id, pick(entry, ['type', 'path'])]
- }),
- )
- : {},
- } satisfies TSRGlobalMatch,
- {
- isScriptContext: true,
- wrap: true,
- json: true,
- },
- )})`
-
- return (
- <>
-
- {extracted
- ? extracted.map((d) => {
- if (d.type === 'stream') {
- return
- }
-
- return
- })
- : null}
- >
- )
- }
-
- return null
-}
-
-export function replaceBy(
- obj: T,
- cb: (value: any, path: Array) => any,
- path: Array = [],
-): T {
- if (isPlainArray(obj)) {
- return obj.map((value, i) => replaceBy(value, cb, [...path, `${i}`])) as any
- }
-
- if (isPlainObject(obj)) {
- // Do not allow objects with illegal
- const newObj: any = {}
-
- for (const key in obj) {
- newObj[key] = replaceBy(obj[key], cb, [...path, key])
- }
-
- return newObj
- }
-
- // // Detect classes, functions, and other non-serializable objects
- // // and return undefined. Exclude some known types that are serializable
- // if (
- // typeof obj === 'function' ||
- // (typeof obj === 'object' &&
- // ![Object, Promise, ReadableStream].includes((obj as any)?.constructor))
- // ) {
- // console.info(obj)
- // warning(false, `Non-serializable value ☝️ found at ${path.join('.')}`)
- // return undefined as any
- // }
-
- const newObj = cb(obj, path)
-
- if (newObj !== obj) {
- return newObj
- }
-
- return obj
-}
-
-function DehydratePromise({ entry }: { entry: ServerExtractedPromise }) {
- return (
-