Skip to content

SPA mode #3497

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions e2e/react-start/basic-auth/src/routes/index.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,27 @@
import { createFileRoute } from '@tanstack/react-router'

export const Route = createFileRoute('/')({
ssr: false,
beforeLoad: () => {
console.log('beforeLoad')
},
loader: async () => {
console.log('loader')
await new Promise((resolve) => setTimeout(resolve, 500))
return {
message: 'Hello from the server!',
}
},
pendingComponent: () => <div className="p-2">Loading...</div>,
component: Home,
})

function Home() {
const { message } = Route.useLoaderData()
return (
<div className="p-2">
<h3>Welcome Home!!!</h3>
<p>{message}</p>
</div>
)
}
16 changes: 14 additions & 2 deletions e2e/react-start/basic/src/routes/index.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,27 @@
import { createFileRoute } from '@tanstack/react-router'
import { CustomMessage } from '~/components/CustomMessage'

export const Route = createFileRoute('/')({
ssr: false,
beforeLoad: () => {
console.log('beforeLoad')
},
loader: async () => {
console.log('loader')
await new Promise((resolve) => setTimeout(resolve, 500))
return {
message: 'Hello from the server!',
}
},
pendingComponent: () => <div className="p-2">Loading...</div>,
component: Home,
})

function Home() {
const { message } = Route.useLoaderData()
return (
<div className="p-2">
<h3>Welcome Home!!!</h3>
<CustomMessage message="Hello from a custom component!" />
<p>{message}</p>
</div>
)
}
6 changes: 4 additions & 2 deletions examples/react/start-basic/src/utils/loggingMiddleware.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { createMiddleware } from '@tanstack/react-start'

const preLogMiddleware = createMiddleware()
//

const serverLogMiddleware = createMiddleware()
.client(async (ctx) => {
const clientTime = new Date()

Expand All @@ -26,7 +28,7 @@ const preLogMiddleware = createMiddleware()
})

export const logMiddleware = createMiddleware()
.middleware([preLogMiddleware])
.middleware([serverLogMiddleware])
.client(async (ctx) => {
const res = await ctx.next()

Expand Down
10 changes: 9 additions & 1 deletion packages/react-router/src/Match.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import invariant from 'tiny-invariant'
import warning from 'tiny-warning'
import {
createControlledPromise,
createSsrError,
getLocationChangeInfo,
isNotFound,
isRedirect,
Expand Down Expand Up @@ -57,7 +58,9 @@ export const Match = React.memo(function MatchImpl({
(!route.isRoot || route.options.wrapInSuspense) &&
(route.options.wrapInSuspense ??
PendingComponent ??
(route.options.errorComponent as any)?.preload)
((route.options.errorComponent as any)?.preload ||
!route.ssr ||
route.ssr === 'data-only'))
? React.Suspense
: SafeFragment

Expand Down Expand Up @@ -222,6 +225,10 @@ export const MatchInner = React.memo(function MatchInnerImpl({
throw router.getMatch(match.id)?.loadPromise
}

if (router.isServer && (!route.ssr || route.ssr === 'data-only')) {
throw createSsrError()
}

if (match.status === 'error') {
// If we're on the server, we need to use React's new and super
// wonky api for throwing errors from a server side render inside
Expand Down Expand Up @@ -271,6 +278,7 @@ export const MatchInner = React.memo(function MatchInnerImpl({
}, pendingMinMs)
}
}

throw router.getMatch(match.id)?.loadPromise
}

Expand Down
19 changes: 7 additions & 12 deletions packages/react-router/src/Transitioner.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -68,24 +68,19 @@ export function Transitioner() {

// Try to load the initial location
useLayoutEffect(() => {
// Don't load if we're already mounted
if (
(typeof window !== 'undefined' && router.clientSsr) ||
(mountLoadForRouter.current.router === router &&
mountLoadForRouter.current.mounted)
mountLoadForRouter.current.router === router &&
mountLoadForRouter.current.mounted
) {
return
}
mountLoadForRouter.current = { router, mounted: true }

const tryLoad = async () => {
try {
await router.load()
} catch (err) {
console.error(err)
}
}
mountLoadForRouter.current = { router, mounted: true }

tryLoad()
router.load().catch((err) => {
console.error(err)
})
}, [router])

useLayoutEffect(() => {
Expand Down
2 changes: 2 additions & 0 deletions packages/react-router/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -367,3 +367,5 @@ export { ScriptOnce } from './ScriptOnce'
export { Asset } from './Asset'
export { HeadContent } from './HeadContent'
export { Scripts } from './Scripts'

export { createSsrError, isSsrError } from '@tanstack/router-core'
14 changes: 0 additions & 14 deletions packages/react-router/src/lazyRouteComponent.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
import * as React from 'react'
import { Outlet } from './Match'
import { ClientOnly } from './ClientOnly'
import type { AsyncRouteComponent } from './route'

// If the load fails due to module not found, it may mean a new version of
Expand All @@ -26,7 +24,6 @@ export function lazyRouteComponent<
>(
importer: () => Promise<T>,
exportName?: TKey,
ssr?: () => boolean,
): T[TKey] extends (props: infer TProps) => any
? AsyncRouteComponent<TProps>
: never {
Expand All @@ -36,10 +33,6 @@ export function lazyRouteComponent<
let reload: boolean

const load = () => {
if (typeof document === 'undefined' && ssr?.() === false) {
comp = (() => null) as any
return Promise.resolve()
}
if (!loadPromise) {
loadPromise = importer()
.then((res) => {
Expand Down Expand Up @@ -91,13 +84,6 @@ export function lazyRouteComponent<
throw load()
}

if (ssr?.() === false) {
return (
<ClientOnly fallback={<Outlet />}>
{React.createElement(comp, props)}
</ClientOnly>
)
}
return React.createElement(comp, props)
}

Expand Down
10 changes: 8 additions & 2 deletions packages/react-start-server/src/defaultStreamHandler.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import { PassThrough } from 'node:stream'
import { isbot } from 'isbot'
import ReactDOMServer from 'react-dom/server'

import { isSsrError } from '@tanstack/router-core'

import {
defineHandlerCallback,
transformPipeableStreamWithRouter,
Expand Down Expand Up @@ -54,12 +56,16 @@ export const defaultStreamHandler = defineHandlerCallback(
},
}),
onError: (error, info) => {
console.error('Error in renderToPipeableStream:', error, info)
if (!isSsrError(error)) {
console.error('Error in renderToPipeableStream:', error, info)
}
},
},
)
} catch (e) {
console.error('Error in renderToPipeableStream:', e)
if (!isSsrError(e)) {
console.error('Error in renderToPipeableStream:', e)
}
}

const responseStream = transformPipeableStreamWithRouter(
Expand Down
1 change: 1 addition & 0 deletions packages/router-core/src/Matches.ts
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,7 @@ export interface RouteMatch<
staticData: StaticDataRouteOption
minPendingPromise?: ControlledPromise<void>
pendingTimeout?: ReturnType<typeof setTimeout>
dehydrated?: boolean
}

export type MakeRouteMatchFromRoute<TRoute extends AnyRoute> = RouteMatch<
Expand Down
2 changes: 2 additions & 0 deletions packages/router-core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -410,3 +410,5 @@ export type {
ValidateUseSearchResult,
ValidateUseParamsResult,
} from './typePrimitives'

export { createSsrError, isSsrError } from './ssr-error'
3 changes: 2 additions & 1 deletion packages/router-core/src/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -567,6 +567,7 @@ export interface Route<
fullPath: TFullPath
path: TPath
id: TId
ssr: boolean | 'data-only'
parentRoute: TParentRoute
children?: TChildren
types: RouteTypes<
Expand Down Expand Up @@ -1309,7 +1310,7 @@ export class BaseRoute<
private _path!: TPath
private _fullPath!: TFullPath
private _to!: TrimPathRight<TFullPath>
private _ssr!: boolean
private _ssr!: boolean | 'data-only'

public get to() {
return this._to
Expand Down
35 changes: 34 additions & 1 deletion packages/router-core/src/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2320,6 +2320,26 @@ export class RouterCore<
}
}

const shouldSkipLoader = (matchId: string) => {
const match = this.getMatch(matchId)!
const route = this.looseRoutesById[match.routeId]!

// Check if any parent route has ssr: false
const parentMatches = matches.slice(
0,
matches.findIndex((m) => m.id === matchId),
)

const isNonSsr =
!route.ssr ||
parentMatches.some((m) => {
const parentRoute = this.looseRoutesById[m.routeId]!
return !parentRoute.ssr
})

return (this.isServer && isNonSsr) || (!this.isServer && match.dehydrated)
}

try {
await new Promise<void>((resolveAll, rejectAll) => {
;(async () => {
Expand Down Expand Up @@ -2372,6 +2392,10 @@ export class RouterCore<

const route = this.looseRoutesById[routeId]!

if (shouldSkipLoader(matchId)) {
continue
}

const pendingMs =
route.options.pendingMs ?? this.options.defaultPendingMs

Expand All @@ -2388,7 +2412,9 @@ export class RouterCore<
(this.options as any)?.defaultPendingComponent)
)

let executeBeforeLoad = true
// By default, execute the beforeLoad if the match is not dehydrated
// We'll unset this after the loader skips down below
let executeBeforeLoad = !existingMatch.dehydrated
if (
// If we are in the middle of a load, either of these will be present
// (not to be confused with `loadPromise`, which is always defined)
Expand Down Expand Up @@ -2536,12 +2562,18 @@ export class RouterCore<
validResolvedMatches.forEach(({ id: matchId, routeId }, index) => {
matchPromises.push(
(async () => {
const route = this.looseRoutesById[routeId]!
const { loaderPromise: prevLoaderPromise } =
this.getMatch(matchId)!

let loaderShouldRunAsync = false
let loaderIsRunningAsync = false

// Do not run the loader if the route is not SSR'able
if (shouldSkipLoader(matchId)) {
return this.getMatch(matchId)!
}

if (prevLoaderPromise) {
await prevLoaderPromise
const match = this.getMatch(matchId)!
Expand Down Expand Up @@ -2768,6 +2800,7 @@ export class RouterCore<
? prev.loaderPromise
: undefined,
invalid: false,
dehydrated: false,
}))
return this.getMatch(matchId)!
})(),
Expand Down
9 changes: 9 additions & 0 deletions packages/router-core/src/ssr-error.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
const message = 'SSR has been disabled for this route'

export function createSsrError() {
return new Error(message)
}

export function isSsrError(error: any): error is Error {
return error instanceof Error && error.message.includes(message)
}
13 changes: 3 additions & 10 deletions packages/router-plugin/src/core/code-splitter/compilers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -277,16 +277,9 @@ export function compileCodeSplitReferenceRoute(
])
}

// If it's a component, we need to pass the function to check the Route.ssr value
if (key === 'component') {
prop.value = template.expression(
`${LAZY_ROUTE_COMPONENT_IDENT}(${splitNodeMeta.localImporterIdent}, '${splitNodeMeta.exporterIdent}', () => Route.ssr)`,
)()
} else {
prop.value = template.expression(
`${LAZY_ROUTE_COMPONENT_IDENT}(${splitNodeMeta.localImporterIdent}, '${splitNodeMeta.exporterIdent}')`,
)()
}
prop.value = template.expression(
`${LAZY_ROUTE_COMPONENT_IDENT}(${splitNodeMeta.localImporterIdent}, '${splitNodeMeta.exporterIdent}')`,
)()

// If the TSRDummyComponent is not defined, define it
if (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { createFileRoute } from '@tanstack/react-router';
import { fetchPosts } from '../posts';
export const Route = createFileRoute('/posts')({
loader: fetchPosts,
component: lazyRouteComponent($$splitComponentImporter, 'component', () => Route.ssr)
component: lazyRouteComponent($$splitComponentImporter, 'component')
});
export function TSRDummyComponent() {
return null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { lazyRouteComponent } from '@tanstack/react-router';
import * as React from 'react';
import { createFileRoute } from '@tanstack/react-router';
export const Route = createFileRoute('/')({
component: lazyRouteComponent($$splitComponentImporter, 'component', () => Route.ssr)
component: lazyRouteComponent($$splitComponentImporter, 'component')
});
interface DemoProps {
title: string;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { isEnabled } from '@features/feature-flags';
import TrueImport from '@modules/true-component';
import { falseLoader } from '@modules/false-component';
export const Route = createFileRoute('/posts')({
component: lazyRouteComponent($$splitComponentImporter, 'component', () => Route.ssr),
component: lazyRouteComponent($$splitComponentImporter, 'component'),
loader: isEnabled ? TrueImport.loader : falseLoader
});
export function TSRDummyComponent() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { lazyRouteComponent } from '@tanstack/react-router';
import { createFileRoute } from '@tanstack/react-router';
import { importedLoader } from '../../shared/imported';
export const Route = createFileRoute('/')({
component: lazyRouteComponent($$splitComponentImporter, 'component', () => Route.ssr),
component: lazyRouteComponent($$splitComponentImporter, 'component'),
loader: importedLoader
});
export function TSRDummyComponent() {
Expand Down
Loading
Loading