Skip to content
Merged
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
19 changes: 19 additions & 0 deletions e2e/react-start/basic/src/components/WindowSize.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/**
* This component accesses `window` at module scope, which would throw
* if this module is ever imported on the server. The compiler optimization
* for <ClientOnly> ensures this module is DCE'd from the server bundle
* entirely, preventing the error.
*/

// This throws at module import time on the server since `window` doesn't exist
const initialWidth = window.innerWidth
const initialHeight = window.innerHeight

export function WindowSize() {
return (
<div data-testid="window-size">
<p data-testid="window-width">Window width: {initialWidth}</p>
<p data-testid="window-height">Window height: {initialHeight}</p>
</div>
)
}
21 changes: 21 additions & 0 deletions e2e/react-start/basic/src/routeTree.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { Route as PostsRouteImport } from './routes/posts'
import { Route as LinksRouteImport } from './routes/links'
import { Route as InlineScriptsRouteImport } from './routes/inline-scripts'
import { Route as DeferredRouteImport } from './routes/deferred'
import { Route as ClientOnlyRouteImport } from './routes/client-only'
import { Route as LayoutRouteImport } from './routes/_layout'
import { Route as SearchParamsRouteRouteImport } from './routes/search-params/route'
import { Route as NotFoundRouteRouteImport } from './routes/not-found/route'
Expand Down Expand Up @@ -92,6 +93,11 @@ const DeferredRoute = DeferredRouteImport.update({
path: '/deferred',
getParentRoute: () => rootRouteImport,
} as any)
const ClientOnlyRoute = ClientOnlyRouteImport.update({
id: '/client-only',
path: '/client-only',
getParentRoute: () => rootRouteImport,
} as any)
const LayoutRoute = LayoutRouteImport.update({
id: '/_layout',
getParentRoute: () => rootRouteImport,
Expand Down Expand Up @@ -268,6 +274,7 @@ export interface FileRoutesByFullPath {
'/': typeof IndexRoute
'/not-found': typeof NotFoundRouteRouteWithChildren
'/search-params': typeof SearchParamsRouteRouteWithChildren
'/client-only': typeof ClientOnlyRoute
'/deferred': typeof DeferredRoute
'/inline-scripts': typeof InlineScriptsRoute
'/links': typeof LinksRoute
Expand Down Expand Up @@ -307,6 +314,7 @@ export interface FileRoutesByFullPath {
}
export interface FileRoutesByTo {
'/': typeof IndexRoute
'/client-only': typeof ClientOnlyRoute
'/deferred': typeof DeferredRoute
'/inline-scripts': typeof InlineScriptsRoute
'/links': typeof LinksRoute
Expand Down Expand Up @@ -346,6 +354,7 @@ export interface FileRoutesById {
'/not-found': typeof NotFoundRouteRouteWithChildren
'/search-params': typeof SearchParamsRouteRouteWithChildren
'/_layout': typeof LayoutRouteWithChildren
'/client-only': typeof ClientOnlyRoute
'/deferred': typeof DeferredRoute
'/inline-scripts': typeof InlineScriptsRoute
'/links': typeof LinksRoute
Expand Down Expand Up @@ -390,6 +399,7 @@ export interface FileRouteTypes {
| '/'
| '/not-found'
| '/search-params'
| '/client-only'
| '/deferred'
| '/inline-scripts'
| '/links'
Expand Down Expand Up @@ -429,6 +439,7 @@ export interface FileRouteTypes {
fileRoutesByTo: FileRoutesByTo
to:
| '/'
| '/client-only'
| '/deferred'
| '/inline-scripts'
| '/links'
Expand Down Expand Up @@ -467,6 +478,7 @@ export interface FileRouteTypes {
| '/not-found'
| '/search-params'
| '/_layout'
| '/client-only'
| '/deferred'
| '/inline-scripts'
| '/links'
Expand Down Expand Up @@ -511,6 +523,7 @@ export interface RootRouteChildren {
NotFoundRouteRoute: typeof NotFoundRouteRouteWithChildren
SearchParamsRouteRoute: typeof SearchParamsRouteRouteWithChildren
LayoutRoute: typeof LayoutRouteWithChildren
ClientOnlyRoute: typeof ClientOnlyRoute
DeferredRoute: typeof DeferredRoute
InlineScriptsRoute: typeof InlineScriptsRoute
LinksRoute: typeof LinksRoute
Expand Down Expand Up @@ -586,6 +599,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof DeferredRouteImport
parentRoute: typeof rootRouteImport
}
'/client-only': {
id: '/client-only'
path: '/client-only'
fullPath: '/client-only'
preLoaderRoute: typeof ClientOnlyRouteImport
parentRoute: typeof rootRouteImport
}
'/_layout': {
id: '/_layout'
path: ''
Expand Down Expand Up @@ -955,6 +975,7 @@ const rootRouteChildren: RootRouteChildren = {
NotFoundRouteRoute: NotFoundRouteRouteWithChildren,
SearchParamsRouteRoute: SearchParamsRouteRouteWithChildren,
LayoutRoute: LayoutRouteWithChildren,
ClientOnlyRoute: ClientOnlyRoute,
DeferredRoute: DeferredRoute,
InlineScriptsRoute: InlineScriptsRoute,
LinksRoute: LinksRoute,
Expand Down
8 changes: 8 additions & 0 deletions e2e/react-start/basic/src/routes/__root.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,14 @@ function RootDocument({ children }: { children: React.ReactNode }) {
>
redirect
</Link>{' '}
<Link
to="/client-only"
activeProps={{
className: 'font-bold',
}}
>
Client Only
</Link>{' '}
<Link
// @ts-expect-error
to="/this-route-does-not-exist"
Expand Down
25 changes: 25 additions & 0 deletions e2e/react-start/basic/src/routes/client-only.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { ClientOnly, createFileRoute } from '@tanstack/react-router'
import { WindowSize } from '~/components/WindowSize'

export const Route = createFileRoute('/client-only')({
component: ClientOnlyPage,
})

function ClientOnlyPage() {
return (
<div className="p-2">
<h3 data-testid="client-only-heading">Client Only Demo</h3>
<p>
The component below uses <code>window</code> APIs that only exist in the
browser.
</p>
<ClientOnly
fallback={
<div data-testid="loading-fallback">Loading window size...</div>
}
>
<WindowSize />
</ClientOnly>
</div>
)
}
72 changes: 72 additions & 0 deletions e2e/react-start/basic/tests/client-only.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { expect } from '@playwright/test'
import { test } from '@tanstack/router-e2e-utils'

/**
* These tests verify the <ClientOnly> compiler optimization.
*
* The WindowSize component accesses `window` at module scope, which would
* throw if the module is ever imported on the server. The compiler optimization
* strips <ClientOnly> children from the server bundle, allowing DCE to remove
* the WindowSize import entirely. This prevents the server from crashing.
*
* If these tests pass in SSR mode, it proves the compiler is correctly
* removing client-only code from the server bundle.
*/

test('ClientOnly renders fallback on server, then client content after hydration', async ({
page,
}) => {
// Navigate directly to the client-only route
// If the compiler optimization isn't working, this would crash the server
// because WindowSize.tsx accesses `window` at module scope
await page.goto('/client-only')
await page.waitForURL('/client-only')

// The heading should be visible
await expect(page.getByTestId('client-only-heading')).toContainText(
'Client Only Demo',
)

// After hydration, the WindowSize component should render with actual values
// Wait for the client-side component to render
await expect(page.getByTestId('window-size')).toBeVisible()
await expect(page.getByTestId('window-width')).toContainText('Window width:')
await expect(page.getByTestId('window-height')).toContainText(
'Window height:',
)
})

test('ClientOnly works with client-side navigation', async ({ page }) => {
// Start from home
await page.goto('/')
await page.waitForURL('/')

// Navigate to client-only route via client-side navigation
await page.getByRole('link', { name: 'Client Only' }).click()
await page.waitForURL('/client-only')

// The WindowSize component should render
await expect(page.getByTestId('window-size')).toBeVisible()
await expect(page.getByTestId('window-width')).toContainText('Window width:')
})

test('ClientOnly component displays actual window dimensions', async ({
page,
}) => {
// Set a specific viewport size
await page.setViewportSize({ width: 800, height: 600 })

await page.goto('/client-only')
await page.waitForURL('/client-only')

// Wait for client-side hydration
await expect(page.getByTestId('window-size')).toBeVisible()

// Check that the displayed dimensions match the viewport
await expect(page.getByTestId('window-width')).toContainText(
'Window width: 800',
)
await expect(page.getByTestId('window-height')).toContainText(
'Window height: 600',
)
})
Loading
Loading