Skip to content
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

Full page reload when navigating to new root layout #40751

Merged
merged 17 commits into from Sep 30, 2022
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
78 changes: 74 additions & 4 deletions packages/next/server/app-render.tsx
Expand Up @@ -603,6 +603,53 @@ async function renderToString(element: React.ReactElement) {
return streamToString(renderStream)
}

function getRootLayoutPath(
[segment, parallelRoutes, { layout }]: LoaderTree,
rootLayoutPath = ''
): string | undefined {
rootLayoutPath += `${segment}/`
const isLayout = typeof layout !== 'undefined'
if (isLayout) return rootLayoutPath
// Will never be anything else than `children`, parallel routes layout will have already been found
const child = parallelRoutes.children
if (!child) return
return getRootLayoutPath(child, rootLayoutPath)
}

function findRootLayoutInFlightRouterState(
[segment, parallelRoutes]: FlightRouterState,
rootLayoutSegments: string,
segments = ''
): boolean {
segments += `${segment}/`
if (segments === rootLayoutSegments) {
return true
} else if (segments.length > rootLayoutSegments.length) {
return false
}
const child = parallelRoutes.children
if (!child) return false
return findRootLayoutInFlightRouterState(child, rootLayoutSegments, segments)
}

function isNavigatingToNewRootLayout(
loaderTree: LoaderTree,
flightRouterState: FlightRouterState
): boolean {
const newRootLayout = getRootLayoutPath(loaderTree)
// should always have a root layout
if (newRootLayout) {
const hasSameRootLayout = findRootLayoutInFlightRouterState(
flightRouterState,
newRootLayout
)

return !hasSameRootLayout
}

return false
}

export async function renderToHTMLOrFlight(
req: IncomingMessage,
res: ServerResponse,
Expand Down Expand Up @@ -703,6 +750,33 @@ export async function renderToHTMLOrFlight(
: {}
: undefined

/**
* The tree created in next-app-loader that holds component segments and modules
*/
const loaderTree: LoaderTree = ComponentMod.tree

// If navigating to a new root layout we need to do a full page navigation.
if (
isFlight &&
Array.isArray(providedFlightRouterState) &&
isNavigatingToNewRootLayout(loaderTree, providedFlightRouterState)
) {
stripInternalQueries(query)
const search = stringifyQuery(query)

// Empty so that the client-side router will do a full page navigation.
const flightData: FlightData = req.url! + (search ? `?${search}` : '')
return new FlightRenderResult(
ComponentMod.renderToReadableStream(
flightData,
serverComponentManifest,
{
onError: flightDataRendererErrorHandler,
}
).pipeThrough(createBufferedTransformStream())
)
}

stripInternalQueries(query)

const LayoutRouter =
Expand All @@ -713,10 +787,6 @@ export async function renderToHTMLOrFlight(
| typeof import('../client/components/hot-reloader.client').default
| null

/**
* The tree created in next-app-loader that holds component segments and modules
*/
const loaderTree: LoaderTree = ComponentMod.tree
/**
* Server Context is specifically only available in Server Components.
* It has to hold values that can't change while rendering from the common layout down.
Expand Down
129 changes: 129 additions & 0 deletions test/e2e/app-dir/mpa-navigation.test.ts
@@ -0,0 +1,129 @@
import path from 'path'
import { createNext, FileRef } from 'e2e-utils'
import { NextInstance } from 'test/lib/next-modes/base'
import webdriver from 'next-webdriver'

describe('app-dir mpa navigation', () => {
if ((global as any).isNextDeploy) {
it('should skip next deploy for now', () => {})
return
}

if (process.env.NEXT_TEST_REACT_VERSION === '^17') {
it('should skip for react v17', () => {})
return
}
let next: NextInstance

beforeAll(async () => {
next = await createNext({
files: {
app: new FileRef(path.join(__dirname, 'mpa-navigation/app')),
'next.config.js': new FileRef(
path.join(__dirname, 'mpa-navigation/next.config.js')
),
},
dependencies: {
react: 'experimental',
'react-dom': 'experimental',
},
})
})
afterAll(() => next.destroy())

describe('Should do a mpa navigation when switching root layout', () => {
it('should work with basic routes', async () => {
const browser = await webdriver(next.url, '/basic-route')

expect(await browser.elementById('basic-route').text()).toBe(
'Basic route'
)
await browser.eval('window.__TEST_NO_RELOAD = true')

// Navigate to page with same root layout
await browser.elementByCss('a').click()
expect(
await browser.waitForElementByCss('#inner-basic-route').text()
).toBe('Inner basic route')
expect(await browser.eval('window.__TEST_NO_RELOAD')).toBeTrue()

// Navigate to page with different root layout
await browser.elementByCss('a').click()
expect(await browser.waitForElementByCss('#route-group').text()).toBe(
'Route group'
)
expect(await browser.eval('window.__TEST_NO_RELOAD')).toBeUndefined()
})

it('should work with route groups', async () => {
const browser = await webdriver(next.url, '/route-group')

expect(await browser.elementById('route-group').text()).toBe(
'Route group'
)
await browser.eval('window.__TEST_NO_RELOAD = true')

// Navigate to page with same root layout
await browser.elementByCss('a').click()
expect(
await browser.waitForElementByCss('#nested-route-group').text()
).toBe('Nested route group')
expect(await browser.eval('window.__TEST_NO_RELOAD')).toBeTrue()

// Navigate to page with different root layout
await browser.elementByCss('a').click()
expect(await browser.waitForElementByCss('#parallel-one').text()).toBe(
'One'
)
expect(await browser.waitForElementByCss('#parallel-two').text()).toBe(
'Two'
)
expect(await browser.eval('window.__TEST_NO_RELOAD')).toBeUndefined()
})

it('should work with parallel routes', async () => {
const browser = await webdriver(next.url, '/with-parallel-routes')

expect(await browser.elementById('parallel-one').text()).toBe('One')
expect(await browser.elementById('parallel-two').text()).toBe('Two')
await browser.eval('window.__TEST_NO_RELOAD = true')

// Navigate to page with same root layout
await browser.elementByCss('a').click()
expect(
await browser.waitForElementByCss('#parallel-one-inner').text()
).toBe('One inner')
expect(await browser.eval('window.__TEST_NO_RELOAD')).toBeTrue()

// Navigate to page with different root layout
await browser.elementByCss('a').click()
expect(await browser.waitForElementByCss('#dynamic-hello').text()).toBe(
'dynamic hello'
)
expect(await browser.eval('window.__TEST_NO_RELOAD')).toBeUndefined()
})

it('should work with dynamic routes', async () => {
const browser = await webdriver(next.url, '/dynamic/first/route')

expect(await browser.elementById('dynamic-route').text()).toBe(
'dynamic route'
)
await browser.eval('window.__TEST_NO_RELOAD = true')

// Navigate to page with same root layout
await browser.elementByCss('a').click()
expect(
await browser.waitForElementByCss('#dynamic-second-hello').text()
).toBe('dynamic hello')
expect(await browser.eval('window.__TEST_NO_RELOAD')).toBeTrue()

// Navigate to page with different root layout
await browser.elementByCss('a').click()
expect(
await browser.waitForElementByCss('#inner-basic-route').text()
).toBe('Inner basic route')
expect(await browser.eval('window.__TEST_NO_RELOAD')).toBeUndefined()
})
})
})
@@ -0,0 +1,10 @@
import Link from 'next/link'

export default function Page() {
return (
<>
<Link href="/with-parallel-routes">To with-parallel-routes</Link>
<p id="nested-route-group">Nested route group</p>
</>
)
}
10 changes: 10 additions & 0 deletions test/e2e/app-dir/mpa-navigation/app/(route-group)/layout.js
@@ -0,0 +1,10 @@
export default function Root({ children }) {
return (
<html>
<head>
<title>Hello</title>
</head>
<body>{children}</body>
</html>
)
}
@@ -0,0 +1,10 @@
import Link from 'next/link'

export default function Page() {
return (
<>
<Link href="/nested-route-group">To nested route group</Link>
<p id="route-group">Route group</p>
</>
)
}
10 changes: 10 additions & 0 deletions test/e2e/app-dir/mpa-navigation/app/basic-route/inner/page.js
@@ -0,0 +1,10 @@
import Link from 'next/link'

export default function Page() {
return (
<>
<Link href="/route-group">To route group</Link>
<p id="inner-basic-route">Inner basic route</p>
</>
)
}
10 changes: 10 additions & 0 deletions test/e2e/app-dir/mpa-navigation/app/basic-route/layout.js
@@ -0,0 +1,10 @@
export default function Root({ children }) {
return (
<html>
<head>
<title>Hello</title>
</head>
<body>{children}</body>
</html>
)
}
10 changes: 10 additions & 0 deletions test/e2e/app-dir/mpa-navigation/app/basic-route/page.js
@@ -0,0 +1,10 @@
import Link from 'next/link'

export default function Page() {
return (
<>
<Link href="/basic-route/inner">To inner basic route</Link>
<p id="basic-route">Basic route</p>
</>
)
}
@@ -0,0 +1,10 @@
import Link from 'next/link'

export default function Page({ params }) {
return (
<>
<Link href="/dynamic/second/hello">To second dynamic</Link>
<p id={`dynamic-${params.param}`}>dynamic {params.param}</p>
</>
)
}
10 changes: 10 additions & 0 deletions test/e2e/app-dir/mpa-navigation/app/dynamic/layout.js
@@ -0,0 +1,10 @@
export default function Root({ children }) {
return (
<html>
<head>
<title>Hello</title>
</head>
<body>{children}</body>
</html>
)
}
@@ -0,0 +1,10 @@
import Link from 'next/link'

export default function Page({ params }) {
return (
<>
<Link href="/basic-route/inner">To basic inner</Link>
<p id={`dynamic-second-${params.param}`}>dynamic {params.param}</p>
</>
)
}
10 changes: 10 additions & 0 deletions test/e2e/app-dir/mpa-navigation/app/to-pages-dir/layout.js
@@ -0,0 +1,10 @@
export default function Root({ children }) {
return (
<html>
<head>
<title>Hello</title>
</head>
<body>{children}</body>
</html>
)
}
10 changes: 10 additions & 0 deletions test/e2e/app-dir/mpa-navigation/app/to-pages-dir/page.js
@@ -0,0 +1,10 @@
import Link from 'next/link'

export default function Page() {
return (
<>
<p id="in-app-dir">In app dir</p>
<Link href="/pages-dir">To pages dir</Link>
</>
)
}
@@ -0,0 +1,10 @@
import Link from 'next/link'

export default function Page() {
return (
<>
<Link href="/dynamic/first/hello">To dynamic route</Link>
<p id="parallel-one-inner">One inner</p>
</>
)
}
@@ -0,0 +1,10 @@
import Link from 'next/link'

export default function Page() {
return (
<>
<Link href="/with-parallel-routes/inner">To parallel inner</Link>
<p id="parallel-one">One</p>
</>
)
}
@@ -0,0 +1,3 @@
export default function Page() {
return <p id="parallel-two">Two</p>
}
@@ -0,0 +1,13 @@
export default function Root({ one, two }) {
return (
<html>
<head>
<title>Hello</title>
</head>
<body>
{one}
{two}
</body>
</html>
)
}
5 changes: 5 additions & 0 deletions test/e2e/app-dir/mpa-navigation/next.config.js
@@ -0,0 +1,5 @@
module.exports = {
experimental: {
appDir: true,
},
}