2022-09-21 15:47:31 +02:00
import type { IncomingHttpHeaders , IncomingMessage , ServerResponse } from 'http'
2022-05-03 12:37:23 +02:00
import type { LoadComponentsReturnType } from './load-components'
2022-08-07 21:16:10 +02:00
import type { ServerRuntime } from '../types'
2022-10-06 21:24:42 +02:00
import type { FontLoaderManifest } from '../build/webpack/plugins/font-loader-manifest-plugin'
2022-05-03 12:37:23 +02:00
2022-09-19 14:17:20 +02:00
// TODO-APP: change to React.use once it becomes stable
2022-09-21 20:45:33 +02:00
// @ts-ignore
2022-09-19 14:17:20 +02:00
import React , { experimental_use as use } from 'react'
2022-09-21 20:45:33 +02:00
2022-09-30 17:55:22 +02:00
import { ParsedUrlQuery , stringify as stringifyQuery } from 'querystring'
2022-05-03 12:37:23 +02:00
import { createFromReadableStream } from 'next/dist/compiled/react-server-dom-webpack'
import { NextParsedUrlQuery } from './request-meta'
import RenderResult from './render-result'
import {
readableStreamTee ,
encodeText ,
decodeText ,
renderToInitialStream ,
createBufferedTransformStream ,
continueFromInitialStream ,
2022-09-29 10:56:28 +02:00
streamToString ,
2022-05-03 12:37:23 +02:00
} from './node-web-streams-helper'
2022-09-09 00:17:15 +02:00
import { ESCAPE_REGEX , htmlEscapeJsonString } from './htmlescape'
2022-09-15 16:53:51 +02:00
import { shouldUseReactRoot } from './utils'
2022-07-07 15:52:07 +02:00
import { matchSegment } from '../client/components/match-segments'
2022-08-12 15:01:19 +02:00
import {
FlightCSSManifest ,
FlightManifest ,
} from '../build/webpack/plugins/flight-manifest-plugin'
2022-10-03 15:43:35 +02:00
import { ServerInsertedHTMLContext } from '../shared/lib/server-inserted-html'
2022-09-15 16:53:51 +02:00
import { stripInternalQueries } from './internal-utils'
2022-09-09 14:44:12 +02:00
import type { ComponentsType } from '../build/webpack/loaders/next-app-loader'
2022-09-20 15:28:07 +02:00
import { REDIRECT_ERROR_CODE } from '../client/components/redirect'
2022-09-25 11:45:00 +02:00
import { NextCookies } from './web/spec-extension/cookies'
2022-09-25 21:35:05 +02:00
import { DYNAMIC_ERROR_CODE } from '../client/components/hooks-server-context'
2022-09-26 13:16:20 +02:00
import { NOT_FOUND_ERROR_CODE } from '../client/components/not-found'
2022-10-09 17:08:51 +02:00
import { HeadManagerContext } from '../shared/lib/head-manager-context'
2022-10-06 21:43:23 +02:00
import { Writable } from 'stream'
2022-09-25 11:45:00 +02:00
const INTERNAL_HEADERS_INSTANCE = Symbol ( 'internal for headers readonly' )
function readonlyHeadersError() {
return new Error ( 'ReadonlyHeaders cannot be modified' )
}
class ReadonlyHeaders {
[ INTERNAL_HEADERS_INSTANCE ] : Headers
entries : Headers [ 'entries' ]
forEach : Headers [ 'forEach' ]
get : Headers [ 'get' ]
has : Headers [ 'has' ]
keys : Headers [ 'keys' ]
values : Headers [ 'values' ]
constructor ( headers : IncomingHttpHeaders ) {
// Since `new Headers` uses `this.append()` to fill the headers object ReadonlyHeaders can't extend from Headers directly as it would throw.
const headersInstance = new Headers ( headers as any )
this [ INTERNAL_HEADERS_INSTANCE ] = headersInstance
this . entries = headersInstance . entries . bind ( headersInstance )
this . forEach = headersInstance . forEach . bind ( headersInstance )
this . get = headersInstance . get . bind ( headersInstance )
this . has = headersInstance . has . bind ( headersInstance )
this . keys = headersInstance . keys . bind ( headersInstance )
this . values = headersInstance . values . bind ( headersInstance )
}
[ Symbol . iterator ] ( ) {
return this [ INTERNAL_HEADERS_INSTANCE ] [ Symbol . iterator ] ( )
}
append() {
throw readonlyHeadersError ( )
}
delete ( ) {
throw readonlyHeadersError ( )
}
set ( ) {
throw readonlyHeadersError ( )
}
}
const INTERNAL_COOKIES_INSTANCE = Symbol ( 'internal for cookies readonly' )
function readonlyCookiesError() {
return new Error ( 'ReadonlyCookies cannot be modified' )
}
2022-10-13 17:19:10 +02:00
2022-09-25 11:45:00 +02:00
class ReadonlyNextCookies {
[ INTERNAL_COOKIES_INSTANCE ] : NextCookies
entries : NextCookies [ 'entries' ]
forEach : NextCookies [ 'forEach' ]
get : NextCookies [ 'get' ]
getWithOptions : NextCookies [ 'getWithOptions' ]
has : NextCookies [ 'has' ]
keys : NextCookies [ 'keys' ]
values : NextCookies [ 'values' ]
constructor ( request : {
headers : {
get ( key : 'cookie' ) : string | null | undefined
}
} ) {
// Since `new Headers` uses `this.append()` to fill the headers object ReadonlyHeaders can't extend from Headers directly as it would throw.
// Request overridden to not have to provide a fully request object.
const cookiesInstance = new NextCookies ( request as Request )
this [ INTERNAL_COOKIES_INSTANCE ] = cookiesInstance
this . entries = cookiesInstance . entries . bind ( cookiesInstance )
this . forEach = cookiesInstance . forEach . bind ( cookiesInstance )
this . get = cookiesInstance . get . bind ( cookiesInstance )
this . getWithOptions = cookiesInstance . getWithOptions . bind ( cookiesInstance )
this . has = cookiesInstance . has . bind ( cookiesInstance )
this . keys = cookiesInstance . keys . bind ( cookiesInstance )
this . values = cookiesInstance . values . bind ( cookiesInstance )
}
[ Symbol . iterator ] ( ) {
return this [ INTERNAL_COOKIES_INSTANCE ] [ Symbol . iterator ] ( )
}
clear() {
throw readonlyCookiesError ( )
}
delete ( ) {
throw readonlyCookiesError ( )
}
set ( ) {
throw readonlyCookiesError ( )
}
}
2022-05-03 12:37:23 +02:00
2022-07-07 22:42:44 +02:00
// this needs to be required lazily so that `next-server` can set
// the env before we require
const ReactDOMServer = shouldUseReactRoot
2022-05-07 20:45:40 +02:00
? require ( 'react-dom/server.browser' )
: require ( 'react-dom/server' )
2022-05-03 12:37:23 +02:00
export type RenderOptsPartial = {
err? : Error | null
dev? : boolean
2022-08-16 19:00:23 +02:00
serverComponentManifest? : FlightManifest
serverCSSManifest? : FlightCSSManifest
2022-05-03 12:37:23 +02:00
supportsDynamicHTML? : boolean
2022-06-27 03:02:24 +02:00
runtime? : ServerRuntime
2022-05-03 12:37:23 +02:00
serverComponents? : boolean
2022-08-02 00:34:23 +02:00
assetPrefix? : string
2022-10-06 21:24:42 +02:00
fontLoaderManifest? : FontLoaderManifest
2022-05-03 12:37:23 +02:00
}
export type RenderOpts = LoadComponentsReturnType & RenderOptsPartial
2022-09-18 22:49:05 +02:00
/ * *
2022-09-20 15:28:07 +02:00
* Flight Response is always set to application / octet - stream to ensure it does not get interpreted as HTML .
2022-09-18 22:49:05 +02:00
* /
class FlightRenderResult extends RenderResult {
constructor ( response : string | ReadableStream < Uint8Array > ) {
super ( response , { contentType : 'application/octet-stream' } )
}
}
2022-07-26 12:41:51 +02:00
/ * *
* Interop between "export default" and "module.exports" .
* /
2022-06-02 12:02:05 +02:00
function interopDefault ( mod : any ) {
return mod . default || mod
}
2022-09-19 20:05:28 +02:00
// tolerate dynamic server errors during prerendering so console
// isn't spammed with unactionable errors
2022-09-20 15:28:07 +02:00
/ * *
* Create error handler for renderers .
* /
function createErrorHandler (
/ * *
* Used for debugging
* /
2022-09-23 04:44:30 +02:00
_source : string ,
capturedErrors : Error [ ]
2022-09-20 15:28:07 +02:00
) {
return ( err : any ) = > {
if (
// TODO-APP: Handle redirect throw
2022-09-25 21:35:05 +02:00
err . digest !== DYNAMIC_ERROR_CODE &&
2022-09-26 13:16:20 +02:00
err . digest !== NOT_FOUND_ERROR_CODE &&
! err . digest ? . startsWith ( REDIRECT_ERROR_CODE )
2022-09-20 15:28:07 +02:00
) {
// Used for debugging error source
// console.error(_source, err)
console . error ( err )
2022-09-23 04:44:30 +02:00
capturedErrors . push ( err )
2022-09-25 21:35:05 +02:00
return err . digest || err . message
2022-09-20 15:28:07 +02:00
}
2022-09-25 21:35:05 +02:00
return err . digest
2022-05-03 12:37:23 +02:00
}
}
2022-09-19 20:05:28 +02:00
let isFetchPatched = false
2022-05-03 12:37:23 +02:00
2022-09-19 20:05:28 +02:00
// we patch fetch to collect cache information used for
// determining if a page is static or not
2022-09-21 20:45:33 +02:00
function patchFetch ( ComponentMod : any ) {
2022-09-19 20:05:28 +02:00
if ( isFetchPatched ) return
isFetchPatched = true
const { DynamicServerError } =
2022-09-21 20:45:33 +02:00
ComponentMod . serverHooks as typeof import ( '../client/components/hooks-server-context' )
2022-09-19 20:05:28 +02:00
2022-09-23 04:44:30 +02:00
const staticGenerationAsyncStorage = ComponentMod . staticGenerationAsyncStorage
2022-09-19 20:05:28 +02:00
const origFetch = ( global as any ) . fetch
; ( global as any ) . fetch = async ( init : any , opts : any ) = > {
2022-09-21 02:00:19 +02:00
const staticGenerationStore =
'getStore' in staticGenerationAsyncStorage
? staticGenerationAsyncStorage . getStore ( )
: staticGenerationAsyncStorage
2022-09-19 20:05:28 +02:00
const { isStaticGeneration , fetchRevalidate , pathname } =
2022-09-21 02:00:19 +02:00
staticGenerationStore || { }
2022-09-19 20:05:28 +02:00
2022-09-21 02:00:19 +02:00
if ( staticGenerationStore && isStaticGeneration ) {
2022-09-19 20:05:28 +02:00
if ( opts && typeof opts === 'object' ) {
if ( opts . cache === 'no-store' ) {
2022-09-21 02:00:19 +02:00
staticGenerationStore . revalidate = 0
2022-09-19 20:05:28 +02:00
// TODO: ensure this error isn't logged to the user
// seems it's slipping through currently
throw new DynamicServerError (
` no-store fetch ${ init } ${ pathname ? ` ${ pathname } ` : '' } `
)
}
2022-05-03 12:37:23 +02:00
2022-09-19 20:05:28 +02:00
if (
typeof opts . revalidate === 'number' &&
( typeof fetchRevalidate === 'undefined' ||
opts . revalidate < fetchRevalidate )
) {
2022-09-21 02:00:19 +02:00
staticGenerationStore . fetchRevalidate = opts . revalidate
2022-09-19 20:05:28 +02:00
}
}
}
return origFetch ( init , opts )
}
2022-05-03 12:37:23 +02:00
}
2022-09-19 14:17:20 +02:00
interface FlightResponseRef {
current : Promise < JSX.Element > | null
}
2022-07-26 12:41:51 +02:00
/ * *
* Render Flight stream .
* This is only used for renderToHTML , the Flight response does not need additional wrappers .
* /
2022-05-18 13:18:28 +02:00
function useFlightResponse (
writable : WritableStream < Uint8Array > ,
2022-06-02 17:43:25 +02:00
req : ReadableStream < Uint8Array > ,
2022-09-09 00:17:15 +02:00
serverComponentManifest : any ,
2022-09-19 20:05:28 +02:00
rscChunks : Uint8Array [ ] ,
2022-09-19 14:17:20 +02:00
flightResponseRef : FlightResponseRef ,
2022-09-09 00:17:15 +02:00
nonce? : string
2022-09-19 14:17:20 +02:00
) : Promise < JSX.Element > {
if ( flightResponseRef . current !== null ) {
2022-09-12 14:45:37 +02:00
return flightResponseRef . current
}
const [ renderStream , forwardStream ] = readableStreamTee ( req )
2022-09-19 14:17:20 +02:00
const res = createFromReadableStream ( renderStream , {
2022-09-28 12:29:22 +02:00
moduleMap :
process . env . NEXT_RUNTIME === 'edge'
? serverComponentManifest . __edge_ssr_module_mapping__
: serverComponentManifest . __ssr_module_mapping__ ,
2022-09-12 14:45:37 +02:00
} )
2022-09-19 14:17:20 +02:00
flightResponseRef . current = res
2022-09-12 14:45:37 +02:00
let bootstrapped = false
// We only attach CSS chunks to the inlined data.
const forwardReader = forwardStream . getReader ( )
const writer = writable . getWriter ( )
const startScriptTag = nonce
? ` <script nonce= ${ JSON . stringify ( nonce ) } > `
: '<script>'
2022-09-28 12:29:22 +02:00
function read() {
2022-09-12 14:45:37 +02:00
forwardReader . read ( ) . then ( ( { done , value } ) = > {
2022-09-19 20:05:28 +02:00
if ( value ) {
rscChunks . push ( value )
}
2022-09-12 14:45:37 +02:00
if ( ! bootstrapped ) {
bootstrapped = true
writer . write (
encodeText (
2022-10-09 17:08:51 +02:00
` ${ startScriptTag } (self.__next_f=self.__next_f||[]).push( ${ htmlEscapeJsonString (
2022-09-12 14:45:37 +02:00
JSON . stringify ( [ 0 ] )
) } ) < / script > `
2022-05-18 13:18:28 +02:00
)
2022-09-12 14:45:37 +02:00
)
}
if ( done ) {
flightResponseRef . current = null
writer . close ( )
} else {
const responsePartial = decodeText ( value )
2022-10-09 17:08:51 +02:00
const scripts = ` ${ startScriptTag } self.__next_f.push( ${ htmlEscapeJsonString (
2022-09-12 14:45:37 +02:00
JSON . stringify ( [ 1 , responsePartial ] )
) } ) < / script > `
writer . write ( encodeText ( scripts ) )
2022-09-28 12:29:22 +02:00
read ( )
2022-09-12 14:45:37 +02:00
}
} )
2022-05-03 12:37:23 +02:00
}
2022-09-28 12:29:22 +02:00
read ( )
2022-09-12 14:45:37 +02:00
2022-09-19 14:17:20 +02:00
return res
2022-05-03 12:37:23 +02:00
}
2022-07-26 12:41:51 +02:00
/ * *
* Create a component that renders the Flight stream .
* This is only used for renderToHTML , the Flight response does not need additional wrappers .
* /
2022-05-03 12:37:23 +02:00
function createServerComponentRenderer (
ComponentToRender : React.ComponentType ,
2022-07-26 12:41:51 +02:00
ComponentMod : {
2022-09-21 20:45:33 +02:00
renderToReadableStream : any
2022-07-26 12:41:51 +02:00
__next_app_webpack_require__? : any
} ,
2022-05-03 12:37:23 +02:00
{
transformStream ,
serverComponentManifest ,
Implement new client-side router (#37551)
## Client-side router for `app` directory
This PR implements the new router that leverages React 18 concurrent features like Suspense and startTransition.
It also integrates with React Server Components and builds on top of it to allow server-centric routing that only renders the part of the page that has to change.
It's one of the pieces of the implementation of https://nextjs.org/blog/layouts-rfc.
## Details
I'm going to document the differences with the current router here (will be reworked for the upgrade guide)
### Client-side cache
In the current router we have an in-memory cache for getStaticProps data so that if you prefetch and then navigate to a route that has been prefetched it'll be near-instant. For getServerSideProps the behavior is different, any navigation to a page with getServerSideProps fetches the data again.
In the new model the cache is a fundamental piece, it's more granular than at the page level and is set up to ensure consistency across concurrent renders. It can also be invalidated at any level.
#### Push/Replace (also applies to next/link)
The new router still has a `router.push` / `router.replace` method.
There are a few differences in how it works though:
- It only takes `href` as an argument, historically you had to provide `href` (the page path) and `as` (the actual url path) to do dynamic routing. In later versions of Next.js this is no longer required and in the majority of cases `as` was no longer needed. In the new router there's no way to reason about `href` vs `as` because there is no notion of "pages" in the browser.
- Both methods now use `startTransition`, you can wrap these in your own `startTransition` to get `isPending`
- The push/replace support concurrent rendering. When a render is bailed by clicking a different link to navigate to a completely different page that still works and doesn't cause race conditions.
- Support for optimistic loading states when navigating
##### Hard/Soft push/replace
Because of the client-side cache being reworked this now allows us to cover two cases: hard push and soft push.
The main difference between the two is if the cache is reused while navigating. The default for `next/link` is a `hard` push which means that the part of the cache affected by the navigation will be invalidated, e.g. if you already navigated to `/dashboard` and you `router.push('/dashboard')` again it'll get the latest version. This is similar to the existing `getServerSideProps` handling.
In case of a soft push (API to be defined but for testing added `router.softPush('/')`) it'll reuse the existing cache and not invalidate parts that are already filled in. In practice this means it's more like the `getStaticProps` client-side navigation because it does not fetch on navigation except if a part of the page is missing.
#### Back/Forward navigation
Back and Forward navigation ([popstate](https://developer.mozilla.org/en-US/docs/Web/API/Window/popstate_event)) are always handled as a soft navigation, meaning that the cache is reused, this ensures back/forward navigation is near-instant when it's in the client-side cache. This will also allow back/forward navigation to be a high priority update instead of a transition as it is based on user interaction. Note: in this PR it still uses `startTransition` as there's no way to handle the high priority update suspending which happens in case of missing data in the cache. We're working with the React team on a solution for this particular case.
### Layouts
Note: this section assumes you've read [The layouts RFC](https://nextjs.org/blog/layouts-rfc) and [React Server Components RFC](https://reactjs.org/blog/2020/12/21/data-fetching-with-react-server-components.html)
React Server Components rendering leverages the Flight streaming mechanism in React 18, this allows sending a serializable representation of the rendered React tree on the server to the browser, the client-side React can use this serialized representation to render components client-side without the JavaScript being sent to the browser. This is one of the building blocks of Server Components. This allows a bunch of interesting features but for now I'll keep it to how it affects layouts.
When you have a `app/dashboard/layout.js` and `app/dashboard/page.js` the page will render as children of the layout, when you add another page like `app/dashboard/integrations/page.js` that page falls under the dashboard layout as well. When client-side navigating the new router automatically figures out if the page you're navigating to can be a smaller render than the whole page, in this case `app/dashboard/page.js` and `app/dashboard/integrations/page.js` share the `app/dashboard/layout.js` so instead of rendering the whole page we render below the layout component, this means the layout itself does not get re-rendered, the layout's `getServerSideProps` would not be called, and the Flight response would only hold the result of `app/dashboard/integrations/page.js`, effectively giving you the smallest patch for the UI.
---
Note: the commits in this PR were mostly work in progress to ensure it wasn't lost along the way. The implementation was reworked a bunch of times to where it is now.
Co-authored-by: Jiachi Liu <4800338+huozhi@users.noreply.github.com>
Co-authored-by: JJ Kasper <22380829+ijjk@users.noreply.github.com>
2022-07-06 23:16:47 +02:00
serverContexts ,
2022-09-19 20:05:28 +02:00
rscChunks ,
2022-05-03 12:37:23 +02:00
} : {
transformStream : TransformStream < Uint8Array , Uint8Array >
serverComponentManifest : NonNullable < RenderOpts [ ' serverComponentManifest ' ] >
2022-07-26 12:41:51 +02:00
serverContexts : Array <
[ ServerContextName : string , JSONValue : Object | number | string ]
>
2022-09-19 20:05:28 +02:00
rscChunks : Uint8Array [ ]
2022-09-09 00:17:15 +02:00
} ,
2022-09-23 04:44:30 +02:00
serverComponentsErrorHandler : ReturnType < typeof createErrorHandler > ,
2022-09-09 00:17:15 +02:00
nonce? : string
2022-09-19 14:17:20 +02:00
) : ( ) = > JSX . Element {
2022-05-03 12:37:23 +02:00
// We need to expose the `__webpack_require__` API globally for
// react-server-dom-webpack. This is a hack until we find a better way.
2022-09-28 00:03:49 +02:00
if ( ComponentMod . __next_app_webpack_require__ ) {
2022-05-03 12:37:23 +02:00
// @ts-ignore
2022-09-28 00:03:49 +02:00
globalThis . __next_require__ = ComponentMod . __next_app_webpack_require__
2022-05-03 12:37:23 +02:00
// @ts-ignore
2022-05-25 00:04:27 +02:00
globalThis . __next_chunk_load__ = ( ) = > Promise . resolve ( )
2022-05-03 12:37:23 +02:00
}
2022-07-11 17:23:21 +02:00
let RSCStream : ReadableStream < Uint8Array >
2022-06-29 02:30:55 +02:00
const createRSCStream = ( ) = > {
if ( ! RSCStream ) {
2022-09-21 20:45:33 +02:00
RSCStream = ComponentMod . renderToReadableStream (
2022-06-29 02:30:55 +02:00
< ComponentToRender / > ,
Implement new client-side router (#37551)
## Client-side router for `app` directory
This PR implements the new router that leverages React 18 concurrent features like Suspense and startTransition.
It also integrates with React Server Components and builds on top of it to allow server-centric routing that only renders the part of the page that has to change.
It's one of the pieces of the implementation of https://nextjs.org/blog/layouts-rfc.
## Details
I'm going to document the differences with the current router here (will be reworked for the upgrade guide)
### Client-side cache
In the current router we have an in-memory cache for getStaticProps data so that if you prefetch and then navigate to a route that has been prefetched it'll be near-instant. For getServerSideProps the behavior is different, any navigation to a page with getServerSideProps fetches the data again.
In the new model the cache is a fundamental piece, it's more granular than at the page level and is set up to ensure consistency across concurrent renders. It can also be invalidated at any level.
#### Push/Replace (also applies to next/link)
The new router still has a `router.push` / `router.replace` method.
There are a few differences in how it works though:
- It only takes `href` as an argument, historically you had to provide `href` (the page path) and `as` (the actual url path) to do dynamic routing. In later versions of Next.js this is no longer required and in the majority of cases `as` was no longer needed. In the new router there's no way to reason about `href` vs `as` because there is no notion of "pages" in the browser.
- Both methods now use `startTransition`, you can wrap these in your own `startTransition` to get `isPending`
- The push/replace support concurrent rendering. When a render is bailed by clicking a different link to navigate to a completely different page that still works and doesn't cause race conditions.
- Support for optimistic loading states when navigating
##### Hard/Soft push/replace
Because of the client-side cache being reworked this now allows us to cover two cases: hard push and soft push.
The main difference between the two is if the cache is reused while navigating. The default for `next/link` is a `hard` push which means that the part of the cache affected by the navigation will be invalidated, e.g. if you already navigated to `/dashboard` and you `router.push('/dashboard')` again it'll get the latest version. This is similar to the existing `getServerSideProps` handling.
In case of a soft push (API to be defined but for testing added `router.softPush('/')`) it'll reuse the existing cache and not invalidate parts that are already filled in. In practice this means it's more like the `getStaticProps` client-side navigation because it does not fetch on navigation except if a part of the page is missing.
#### Back/Forward navigation
Back and Forward navigation ([popstate](https://developer.mozilla.org/en-US/docs/Web/API/Window/popstate_event)) are always handled as a soft navigation, meaning that the cache is reused, this ensures back/forward navigation is near-instant when it's in the client-side cache. This will also allow back/forward navigation to be a high priority update instead of a transition as it is based on user interaction. Note: in this PR it still uses `startTransition` as there's no way to handle the high priority update suspending which happens in case of missing data in the cache. We're working with the React team on a solution for this particular case.
### Layouts
Note: this section assumes you've read [The layouts RFC](https://nextjs.org/blog/layouts-rfc) and [React Server Components RFC](https://reactjs.org/blog/2020/12/21/data-fetching-with-react-server-components.html)
React Server Components rendering leverages the Flight streaming mechanism in React 18, this allows sending a serializable representation of the rendered React tree on the server to the browser, the client-side React can use this serialized representation to render components client-side without the JavaScript being sent to the browser. This is one of the building blocks of Server Components. This allows a bunch of interesting features but for now I'll keep it to how it affects layouts.
When you have a `app/dashboard/layout.js` and `app/dashboard/page.js` the page will render as children of the layout, when you add another page like `app/dashboard/integrations/page.js` that page falls under the dashboard layout as well. When client-side navigating the new router automatically figures out if the page you're navigating to can be a smaller render than the whole page, in this case `app/dashboard/page.js` and `app/dashboard/integrations/page.js` share the `app/dashboard/layout.js` so instead of rendering the whole page we render below the layout component, this means the layout itself does not get re-rendered, the layout's `getServerSideProps` would not be called, and the Flight response would only hold the result of `app/dashboard/integrations/page.js`, effectively giving you the smallest patch for the UI.
---
Note: the commits in this PR were mostly work in progress to ensure it wasn't lost along the way. The implementation was reworked a bunch of times to where it is now.
Co-authored-by: Jiachi Liu <4800338+huozhi@users.noreply.github.com>
Co-authored-by: JJ Kasper <22380829+ijjk@users.noreply.github.com>
2022-07-06 23:16:47 +02:00
serverComponentManifest ,
{
context : serverContexts ,
2022-09-20 15:28:07 +02:00
onError : serverComponentsErrorHandler ,
Implement new client-side router (#37551)
## Client-side router for `app` directory
This PR implements the new router that leverages React 18 concurrent features like Suspense and startTransition.
It also integrates with React Server Components and builds on top of it to allow server-centric routing that only renders the part of the page that has to change.
It's one of the pieces of the implementation of https://nextjs.org/blog/layouts-rfc.
## Details
I'm going to document the differences with the current router here (will be reworked for the upgrade guide)
### Client-side cache
In the current router we have an in-memory cache for getStaticProps data so that if you prefetch and then navigate to a route that has been prefetched it'll be near-instant. For getServerSideProps the behavior is different, any navigation to a page with getServerSideProps fetches the data again.
In the new model the cache is a fundamental piece, it's more granular than at the page level and is set up to ensure consistency across concurrent renders. It can also be invalidated at any level.
#### Push/Replace (also applies to next/link)
The new router still has a `router.push` / `router.replace` method.
There are a few differences in how it works though:
- It only takes `href` as an argument, historically you had to provide `href` (the page path) and `as` (the actual url path) to do dynamic routing. In later versions of Next.js this is no longer required and in the majority of cases `as` was no longer needed. In the new router there's no way to reason about `href` vs `as` because there is no notion of "pages" in the browser.
- Both methods now use `startTransition`, you can wrap these in your own `startTransition` to get `isPending`
- The push/replace support concurrent rendering. When a render is bailed by clicking a different link to navigate to a completely different page that still works and doesn't cause race conditions.
- Support for optimistic loading states when navigating
##### Hard/Soft push/replace
Because of the client-side cache being reworked this now allows us to cover two cases: hard push and soft push.
The main difference between the two is if the cache is reused while navigating. The default for `next/link` is a `hard` push which means that the part of the cache affected by the navigation will be invalidated, e.g. if you already navigated to `/dashboard` and you `router.push('/dashboard')` again it'll get the latest version. This is similar to the existing `getServerSideProps` handling.
In case of a soft push (API to be defined but for testing added `router.softPush('/')`) it'll reuse the existing cache and not invalidate parts that are already filled in. In practice this means it's more like the `getStaticProps` client-side navigation because it does not fetch on navigation except if a part of the page is missing.
#### Back/Forward navigation
Back and Forward navigation ([popstate](https://developer.mozilla.org/en-US/docs/Web/API/Window/popstate_event)) are always handled as a soft navigation, meaning that the cache is reused, this ensures back/forward navigation is near-instant when it's in the client-side cache. This will also allow back/forward navigation to be a high priority update instead of a transition as it is based on user interaction. Note: in this PR it still uses `startTransition` as there's no way to handle the high priority update suspending which happens in case of missing data in the cache. We're working with the React team on a solution for this particular case.
### Layouts
Note: this section assumes you've read [The layouts RFC](https://nextjs.org/blog/layouts-rfc) and [React Server Components RFC](https://reactjs.org/blog/2020/12/21/data-fetching-with-react-server-components.html)
React Server Components rendering leverages the Flight streaming mechanism in React 18, this allows sending a serializable representation of the rendered React tree on the server to the browser, the client-side React can use this serialized representation to render components client-side without the JavaScript being sent to the browser. This is one of the building blocks of Server Components. This allows a bunch of interesting features but for now I'll keep it to how it affects layouts.
When you have a `app/dashboard/layout.js` and `app/dashboard/page.js` the page will render as children of the layout, when you add another page like `app/dashboard/integrations/page.js` that page falls under the dashboard layout as well. When client-side navigating the new router automatically figures out if the page you're navigating to can be a smaller render than the whole page, in this case `app/dashboard/page.js` and `app/dashboard/integrations/page.js` share the `app/dashboard/layout.js` so instead of rendering the whole page we render below the layout component, this means the layout itself does not get re-rendered, the layout's `getServerSideProps` would not be called, and the Flight response would only hold the result of `app/dashboard/integrations/page.js`, effectively giving you the smallest patch for the UI.
---
Note: the commits in this PR were mostly work in progress to ensure it wasn't lost along the way. The implementation was reworked a bunch of times to where it is now.
Co-authored-by: Jiachi Liu <4800338+huozhi@users.noreply.github.com>
Co-authored-by: JJ Kasper <22380829+ijjk@users.noreply.github.com>
2022-07-06 23:16:47 +02:00
}
2022-07-12 11:42:46 +02:00
)
2022-06-29 02:30:55 +02:00
}
return RSCStream
}
2022-05-03 12:37:23 +02:00
2022-09-19 14:17:20 +02:00
const flightResponseRef : FlightResponseRef = { current : null }
2022-09-12 14:45:37 +02:00
2022-06-29 02:30:55 +02:00
const writable = transformStream . writable
2022-09-19 14:17:20 +02:00
return function ServerComponentWrapper ( ) : JSX . Element {
2022-06-29 02:30:55 +02:00
const reqStream = createRSCStream ( )
2022-05-03 12:37:23 +02:00
const response = useFlightResponse (
writable ,
2022-06-02 17:43:25 +02:00
reqStream ,
2022-09-09 00:17:15 +02:00
serverComponentManifest ,
2022-09-19 20:05:28 +02:00
rscChunks ,
2022-09-12 14:45:37 +02:00
flightResponseRef ,
2022-09-09 00:17:15 +02:00
nonce
2022-05-03 12:37:23 +02:00
)
2022-09-15 21:28:12 +02:00
return use ( response )
2022-05-03 12:37:23 +02:00
}
}
2022-07-11 14:34:10 +02:00
type DynamicParamTypes = 'catchall' | 'optional-catchall' | 'dynamic'
// c = catchall
// oc = optional catchall
// d = dynamic
export type DynamicParamTypesShort = 'c' | 'oc' | 'd'
2022-07-27 14:43:43 +02:00
/ * *
* Shorten the dynamic param in order to make it smaller when transmitted to the browser .
* /
2022-07-11 14:34:10 +02:00
function getShortDynamicParamType (
type : DynamicParamTypes
) : DynamicParamTypesShort {
switch ( type ) {
case 'catchall' :
return 'c'
case 'optional-catchall' :
return 'oc'
case 'dynamic' :
return 'd'
default :
throw new Error ( 'Unknown dynamic param type' )
}
}
2022-07-27 14:43:43 +02:00
/ * *
* Segment in the router state .
* /
2022-07-11 14:34:10 +02:00
export type Segment =
| string
| [ param : string , value : string , type : DynamicParamTypesShort ]
2022-07-07 15:52:07 +02:00
2022-07-27 14:43:43 +02:00
/ * *
* LoaderTree is generated in next - app - loader .
* /
Implement new client-side router (#37551)
## Client-side router for `app` directory
This PR implements the new router that leverages React 18 concurrent features like Suspense and startTransition.
It also integrates with React Server Components and builds on top of it to allow server-centric routing that only renders the part of the page that has to change.
It's one of the pieces of the implementation of https://nextjs.org/blog/layouts-rfc.
## Details
I'm going to document the differences with the current router here (will be reworked for the upgrade guide)
### Client-side cache
In the current router we have an in-memory cache for getStaticProps data so that if you prefetch and then navigate to a route that has been prefetched it'll be near-instant. For getServerSideProps the behavior is different, any navigation to a page with getServerSideProps fetches the data again.
In the new model the cache is a fundamental piece, it's more granular than at the page level and is set up to ensure consistency across concurrent renders. It can also be invalidated at any level.
#### Push/Replace (also applies to next/link)
The new router still has a `router.push` / `router.replace` method.
There are a few differences in how it works though:
- It only takes `href` as an argument, historically you had to provide `href` (the page path) and `as` (the actual url path) to do dynamic routing. In later versions of Next.js this is no longer required and in the majority of cases `as` was no longer needed. In the new router there's no way to reason about `href` vs `as` because there is no notion of "pages" in the browser.
- Both methods now use `startTransition`, you can wrap these in your own `startTransition` to get `isPending`
- The push/replace support concurrent rendering. When a render is bailed by clicking a different link to navigate to a completely different page that still works and doesn't cause race conditions.
- Support for optimistic loading states when navigating
##### Hard/Soft push/replace
Because of the client-side cache being reworked this now allows us to cover two cases: hard push and soft push.
The main difference between the two is if the cache is reused while navigating. The default for `next/link` is a `hard` push which means that the part of the cache affected by the navigation will be invalidated, e.g. if you already navigated to `/dashboard` and you `router.push('/dashboard')` again it'll get the latest version. This is similar to the existing `getServerSideProps` handling.
In case of a soft push (API to be defined but for testing added `router.softPush('/')`) it'll reuse the existing cache and not invalidate parts that are already filled in. In practice this means it's more like the `getStaticProps` client-side navigation because it does not fetch on navigation except if a part of the page is missing.
#### Back/Forward navigation
Back and Forward navigation ([popstate](https://developer.mozilla.org/en-US/docs/Web/API/Window/popstate_event)) are always handled as a soft navigation, meaning that the cache is reused, this ensures back/forward navigation is near-instant when it's in the client-side cache. This will also allow back/forward navigation to be a high priority update instead of a transition as it is based on user interaction. Note: in this PR it still uses `startTransition` as there's no way to handle the high priority update suspending which happens in case of missing data in the cache. We're working with the React team on a solution for this particular case.
### Layouts
Note: this section assumes you've read [The layouts RFC](https://nextjs.org/blog/layouts-rfc) and [React Server Components RFC](https://reactjs.org/blog/2020/12/21/data-fetching-with-react-server-components.html)
React Server Components rendering leverages the Flight streaming mechanism in React 18, this allows sending a serializable representation of the rendered React tree on the server to the browser, the client-side React can use this serialized representation to render components client-side without the JavaScript being sent to the browser. This is one of the building blocks of Server Components. This allows a bunch of interesting features but for now I'll keep it to how it affects layouts.
When you have a `app/dashboard/layout.js` and `app/dashboard/page.js` the page will render as children of the layout, when you add another page like `app/dashboard/integrations/page.js` that page falls under the dashboard layout as well. When client-side navigating the new router automatically figures out if the page you're navigating to can be a smaller render than the whole page, in this case `app/dashboard/page.js` and `app/dashboard/integrations/page.js` share the `app/dashboard/layout.js` so instead of rendering the whole page we render below the layout component, this means the layout itself does not get re-rendered, the layout's `getServerSideProps` would not be called, and the Flight response would only hold the result of `app/dashboard/integrations/page.js`, effectively giving you the smallest patch for the UI.
---
Note: the commits in this PR were mostly work in progress to ensure it wasn't lost along the way. The implementation was reworked a bunch of times to where it is now.
Co-authored-by: Jiachi Liu <4800338+huozhi@users.noreply.github.com>
Co-authored-by: JJ Kasper <22380829+ijjk@users.noreply.github.com>
2022-07-06 23:16:47 +02:00
type LoaderTree = [
segment : string ,
parallelRoutes : { [ parallelRouterKey : string ] : LoaderTree } ,
2022-09-09 14:44:12 +02:00
components : ComponentsType
Implement new client-side router (#37551)
## Client-side router for `app` directory
This PR implements the new router that leverages React 18 concurrent features like Suspense and startTransition.
It also integrates with React Server Components and builds on top of it to allow server-centric routing that only renders the part of the page that has to change.
It's one of the pieces of the implementation of https://nextjs.org/blog/layouts-rfc.
## Details
I'm going to document the differences with the current router here (will be reworked for the upgrade guide)
### Client-side cache
In the current router we have an in-memory cache for getStaticProps data so that if you prefetch and then navigate to a route that has been prefetched it'll be near-instant. For getServerSideProps the behavior is different, any navigation to a page with getServerSideProps fetches the data again.
In the new model the cache is a fundamental piece, it's more granular than at the page level and is set up to ensure consistency across concurrent renders. It can also be invalidated at any level.
#### Push/Replace (also applies to next/link)
The new router still has a `router.push` / `router.replace` method.
There are a few differences in how it works though:
- It only takes `href` as an argument, historically you had to provide `href` (the page path) and `as` (the actual url path) to do dynamic routing. In later versions of Next.js this is no longer required and in the majority of cases `as` was no longer needed. In the new router there's no way to reason about `href` vs `as` because there is no notion of "pages" in the browser.
- Both methods now use `startTransition`, you can wrap these in your own `startTransition` to get `isPending`
- The push/replace support concurrent rendering. When a render is bailed by clicking a different link to navigate to a completely different page that still works and doesn't cause race conditions.
- Support for optimistic loading states when navigating
##### Hard/Soft push/replace
Because of the client-side cache being reworked this now allows us to cover two cases: hard push and soft push.
The main difference between the two is if the cache is reused while navigating. The default for `next/link` is a `hard` push which means that the part of the cache affected by the navigation will be invalidated, e.g. if you already navigated to `/dashboard` and you `router.push('/dashboard')` again it'll get the latest version. This is similar to the existing `getServerSideProps` handling.
In case of a soft push (API to be defined but for testing added `router.softPush('/')`) it'll reuse the existing cache and not invalidate parts that are already filled in. In practice this means it's more like the `getStaticProps` client-side navigation because it does not fetch on navigation except if a part of the page is missing.
#### Back/Forward navigation
Back and Forward navigation ([popstate](https://developer.mozilla.org/en-US/docs/Web/API/Window/popstate_event)) are always handled as a soft navigation, meaning that the cache is reused, this ensures back/forward navigation is near-instant when it's in the client-side cache. This will also allow back/forward navigation to be a high priority update instead of a transition as it is based on user interaction. Note: in this PR it still uses `startTransition` as there's no way to handle the high priority update suspending which happens in case of missing data in the cache. We're working with the React team on a solution for this particular case.
### Layouts
Note: this section assumes you've read [The layouts RFC](https://nextjs.org/blog/layouts-rfc) and [React Server Components RFC](https://reactjs.org/blog/2020/12/21/data-fetching-with-react-server-components.html)
React Server Components rendering leverages the Flight streaming mechanism in React 18, this allows sending a serializable representation of the rendered React tree on the server to the browser, the client-side React can use this serialized representation to render components client-side without the JavaScript being sent to the browser. This is one of the building blocks of Server Components. This allows a bunch of interesting features but for now I'll keep it to how it affects layouts.
When you have a `app/dashboard/layout.js` and `app/dashboard/page.js` the page will render as children of the layout, when you add another page like `app/dashboard/integrations/page.js` that page falls under the dashboard layout as well. When client-side navigating the new router automatically figures out if the page you're navigating to can be a smaller render than the whole page, in this case `app/dashboard/page.js` and `app/dashboard/integrations/page.js` share the `app/dashboard/layout.js` so instead of rendering the whole page we render below the layout component, this means the layout itself does not get re-rendered, the layout's `getServerSideProps` would not be called, and the Flight response would only hold the result of `app/dashboard/integrations/page.js`, effectively giving you the smallest patch for the UI.
---
Note: the commits in this PR were mostly work in progress to ensure it wasn't lost along the way. The implementation was reworked a bunch of times to where it is now.
Co-authored-by: Jiachi Liu <4800338+huozhi@users.noreply.github.com>
Co-authored-by: JJ Kasper <22380829+ijjk@users.noreply.github.com>
2022-07-06 23:16:47 +02:00
]
2022-07-27 14:43:43 +02:00
/ * *
* Router state
* /
Implement new client-side router (#37551)
## Client-side router for `app` directory
This PR implements the new router that leverages React 18 concurrent features like Suspense and startTransition.
It also integrates with React Server Components and builds on top of it to allow server-centric routing that only renders the part of the page that has to change.
It's one of the pieces of the implementation of https://nextjs.org/blog/layouts-rfc.
## Details
I'm going to document the differences with the current router here (will be reworked for the upgrade guide)
### Client-side cache
In the current router we have an in-memory cache for getStaticProps data so that if you prefetch and then navigate to a route that has been prefetched it'll be near-instant. For getServerSideProps the behavior is different, any navigation to a page with getServerSideProps fetches the data again.
In the new model the cache is a fundamental piece, it's more granular than at the page level and is set up to ensure consistency across concurrent renders. It can also be invalidated at any level.
#### Push/Replace (also applies to next/link)
The new router still has a `router.push` / `router.replace` method.
There are a few differences in how it works though:
- It only takes `href` as an argument, historically you had to provide `href` (the page path) and `as` (the actual url path) to do dynamic routing. In later versions of Next.js this is no longer required and in the majority of cases `as` was no longer needed. In the new router there's no way to reason about `href` vs `as` because there is no notion of "pages" in the browser.
- Both methods now use `startTransition`, you can wrap these in your own `startTransition` to get `isPending`
- The push/replace support concurrent rendering. When a render is bailed by clicking a different link to navigate to a completely different page that still works and doesn't cause race conditions.
- Support for optimistic loading states when navigating
##### Hard/Soft push/replace
Because of the client-side cache being reworked this now allows us to cover two cases: hard push and soft push.
The main difference between the two is if the cache is reused while navigating. The default for `next/link` is a `hard` push which means that the part of the cache affected by the navigation will be invalidated, e.g. if you already navigated to `/dashboard` and you `router.push('/dashboard')` again it'll get the latest version. This is similar to the existing `getServerSideProps` handling.
In case of a soft push (API to be defined but for testing added `router.softPush('/')`) it'll reuse the existing cache and not invalidate parts that are already filled in. In practice this means it's more like the `getStaticProps` client-side navigation because it does not fetch on navigation except if a part of the page is missing.
#### Back/Forward navigation
Back and Forward navigation ([popstate](https://developer.mozilla.org/en-US/docs/Web/API/Window/popstate_event)) are always handled as a soft navigation, meaning that the cache is reused, this ensures back/forward navigation is near-instant when it's in the client-side cache. This will also allow back/forward navigation to be a high priority update instead of a transition as it is based on user interaction. Note: in this PR it still uses `startTransition` as there's no way to handle the high priority update suspending which happens in case of missing data in the cache. We're working with the React team on a solution for this particular case.
### Layouts
Note: this section assumes you've read [The layouts RFC](https://nextjs.org/blog/layouts-rfc) and [React Server Components RFC](https://reactjs.org/blog/2020/12/21/data-fetching-with-react-server-components.html)
React Server Components rendering leverages the Flight streaming mechanism in React 18, this allows sending a serializable representation of the rendered React tree on the server to the browser, the client-side React can use this serialized representation to render components client-side without the JavaScript being sent to the browser. This is one of the building blocks of Server Components. This allows a bunch of interesting features but for now I'll keep it to how it affects layouts.
When you have a `app/dashboard/layout.js` and `app/dashboard/page.js` the page will render as children of the layout, when you add another page like `app/dashboard/integrations/page.js` that page falls under the dashboard layout as well. When client-side navigating the new router automatically figures out if the page you're navigating to can be a smaller render than the whole page, in this case `app/dashboard/page.js` and `app/dashboard/integrations/page.js` share the `app/dashboard/layout.js` so instead of rendering the whole page we render below the layout component, this means the layout itself does not get re-rendered, the layout's `getServerSideProps` would not be called, and the Flight response would only hold the result of `app/dashboard/integrations/page.js`, effectively giving you the smallest patch for the UI.
---
Note: the commits in this PR were mostly work in progress to ensure it wasn't lost along the way. The implementation was reworked a bunch of times to where it is now.
Co-authored-by: Jiachi Liu <4800338+huozhi@users.noreply.github.com>
Co-authored-by: JJ Kasper <22380829+ijjk@users.noreply.github.com>
2022-07-06 23:16:47 +02:00
export type FlightRouterState = [
2022-07-07 15:52:07 +02:00
segment : Segment ,
Implement new client-side router (#37551)
## Client-side router for `app` directory
This PR implements the new router that leverages React 18 concurrent features like Suspense and startTransition.
It also integrates with React Server Components and builds on top of it to allow server-centric routing that only renders the part of the page that has to change.
It's one of the pieces of the implementation of https://nextjs.org/blog/layouts-rfc.
## Details
I'm going to document the differences with the current router here (will be reworked for the upgrade guide)
### Client-side cache
In the current router we have an in-memory cache for getStaticProps data so that if you prefetch and then navigate to a route that has been prefetched it'll be near-instant. For getServerSideProps the behavior is different, any navigation to a page with getServerSideProps fetches the data again.
In the new model the cache is a fundamental piece, it's more granular than at the page level and is set up to ensure consistency across concurrent renders. It can also be invalidated at any level.
#### Push/Replace (also applies to next/link)
The new router still has a `router.push` / `router.replace` method.
There are a few differences in how it works though:
- It only takes `href` as an argument, historically you had to provide `href` (the page path) and `as` (the actual url path) to do dynamic routing. In later versions of Next.js this is no longer required and in the majority of cases `as` was no longer needed. In the new router there's no way to reason about `href` vs `as` because there is no notion of "pages" in the browser.
- Both methods now use `startTransition`, you can wrap these in your own `startTransition` to get `isPending`
- The push/replace support concurrent rendering. When a render is bailed by clicking a different link to navigate to a completely different page that still works and doesn't cause race conditions.
- Support for optimistic loading states when navigating
##### Hard/Soft push/replace
Because of the client-side cache being reworked this now allows us to cover two cases: hard push and soft push.
The main difference between the two is if the cache is reused while navigating. The default for `next/link` is a `hard` push which means that the part of the cache affected by the navigation will be invalidated, e.g. if you already navigated to `/dashboard` and you `router.push('/dashboard')` again it'll get the latest version. This is similar to the existing `getServerSideProps` handling.
In case of a soft push (API to be defined but for testing added `router.softPush('/')`) it'll reuse the existing cache and not invalidate parts that are already filled in. In practice this means it's more like the `getStaticProps` client-side navigation because it does not fetch on navigation except if a part of the page is missing.
#### Back/Forward navigation
Back and Forward navigation ([popstate](https://developer.mozilla.org/en-US/docs/Web/API/Window/popstate_event)) are always handled as a soft navigation, meaning that the cache is reused, this ensures back/forward navigation is near-instant when it's in the client-side cache. This will also allow back/forward navigation to be a high priority update instead of a transition as it is based on user interaction. Note: in this PR it still uses `startTransition` as there's no way to handle the high priority update suspending which happens in case of missing data in the cache. We're working with the React team on a solution for this particular case.
### Layouts
Note: this section assumes you've read [The layouts RFC](https://nextjs.org/blog/layouts-rfc) and [React Server Components RFC](https://reactjs.org/blog/2020/12/21/data-fetching-with-react-server-components.html)
React Server Components rendering leverages the Flight streaming mechanism in React 18, this allows sending a serializable representation of the rendered React tree on the server to the browser, the client-side React can use this serialized representation to render components client-side without the JavaScript being sent to the browser. This is one of the building blocks of Server Components. This allows a bunch of interesting features but for now I'll keep it to how it affects layouts.
When you have a `app/dashboard/layout.js` and `app/dashboard/page.js` the page will render as children of the layout, when you add another page like `app/dashboard/integrations/page.js` that page falls under the dashboard layout as well. When client-side navigating the new router automatically figures out if the page you're navigating to can be a smaller render than the whole page, in this case `app/dashboard/page.js` and `app/dashboard/integrations/page.js` share the `app/dashboard/layout.js` so instead of rendering the whole page we render below the layout component, this means the layout itself does not get re-rendered, the layout's `getServerSideProps` would not be called, and the Flight response would only hold the result of `app/dashboard/integrations/page.js`, effectively giving you the smallest patch for the UI.
---
Note: the commits in this PR were mostly work in progress to ensure it wasn't lost along the way. The implementation was reworked a bunch of times to where it is now.
Co-authored-by: Jiachi Liu <4800338+huozhi@users.noreply.github.com>
Co-authored-by: JJ Kasper <22380829+ijjk@users.noreply.github.com>
2022-07-06 23:16:47 +02:00
parallelRoutes : { [ parallelRouterKey : string ] : FlightRouterState } ,
url? : string ,
2022-09-06 19:29:09 +02:00
refresh ? : 'refetch'
Implement new client-side router (#37551)
## Client-side router for `app` directory
This PR implements the new router that leverages React 18 concurrent features like Suspense and startTransition.
It also integrates with React Server Components and builds on top of it to allow server-centric routing that only renders the part of the page that has to change.
It's one of the pieces of the implementation of https://nextjs.org/blog/layouts-rfc.
## Details
I'm going to document the differences with the current router here (will be reworked for the upgrade guide)
### Client-side cache
In the current router we have an in-memory cache for getStaticProps data so that if you prefetch and then navigate to a route that has been prefetched it'll be near-instant. For getServerSideProps the behavior is different, any navigation to a page with getServerSideProps fetches the data again.
In the new model the cache is a fundamental piece, it's more granular than at the page level and is set up to ensure consistency across concurrent renders. It can also be invalidated at any level.
#### Push/Replace (also applies to next/link)
The new router still has a `router.push` / `router.replace` method.
There are a few differences in how it works though:
- It only takes `href` as an argument, historically you had to provide `href` (the page path) and `as` (the actual url path) to do dynamic routing. In later versions of Next.js this is no longer required and in the majority of cases `as` was no longer needed. In the new router there's no way to reason about `href` vs `as` because there is no notion of "pages" in the browser.
- Both methods now use `startTransition`, you can wrap these in your own `startTransition` to get `isPending`
- The push/replace support concurrent rendering. When a render is bailed by clicking a different link to navigate to a completely different page that still works and doesn't cause race conditions.
- Support for optimistic loading states when navigating
##### Hard/Soft push/replace
Because of the client-side cache being reworked this now allows us to cover two cases: hard push and soft push.
The main difference between the two is if the cache is reused while navigating. The default for `next/link` is a `hard` push which means that the part of the cache affected by the navigation will be invalidated, e.g. if you already navigated to `/dashboard` and you `router.push('/dashboard')` again it'll get the latest version. This is similar to the existing `getServerSideProps` handling.
In case of a soft push (API to be defined but for testing added `router.softPush('/')`) it'll reuse the existing cache and not invalidate parts that are already filled in. In practice this means it's more like the `getStaticProps` client-side navigation because it does not fetch on navigation except if a part of the page is missing.
#### Back/Forward navigation
Back and Forward navigation ([popstate](https://developer.mozilla.org/en-US/docs/Web/API/Window/popstate_event)) are always handled as a soft navigation, meaning that the cache is reused, this ensures back/forward navigation is near-instant when it's in the client-side cache. This will also allow back/forward navigation to be a high priority update instead of a transition as it is based on user interaction. Note: in this PR it still uses `startTransition` as there's no way to handle the high priority update suspending which happens in case of missing data in the cache. We're working with the React team on a solution for this particular case.
### Layouts
Note: this section assumes you've read [The layouts RFC](https://nextjs.org/blog/layouts-rfc) and [React Server Components RFC](https://reactjs.org/blog/2020/12/21/data-fetching-with-react-server-components.html)
React Server Components rendering leverages the Flight streaming mechanism in React 18, this allows sending a serializable representation of the rendered React tree on the server to the browser, the client-side React can use this serialized representation to render components client-side without the JavaScript being sent to the browser. This is one of the building blocks of Server Components. This allows a bunch of interesting features but for now I'll keep it to how it affects layouts.
When you have a `app/dashboard/layout.js` and `app/dashboard/page.js` the page will render as children of the layout, when you add another page like `app/dashboard/integrations/page.js` that page falls under the dashboard layout as well. When client-side navigating the new router automatically figures out if the page you're navigating to can be a smaller render than the whole page, in this case `app/dashboard/page.js` and `app/dashboard/integrations/page.js` share the `app/dashboard/layout.js` so instead of rendering the whole page we render below the layout component, this means the layout itself does not get re-rendered, the layout's `getServerSideProps` would not be called, and the Flight response would only hold the result of `app/dashboard/integrations/page.js`, effectively giving you the smallest patch for the UI.
---
Note: the commits in this PR were mostly work in progress to ensure it wasn't lost along the way. The implementation was reworked a bunch of times to where it is now.
Co-authored-by: Jiachi Liu <4800338+huozhi@users.noreply.github.com>
Co-authored-by: JJ Kasper <22380829+ijjk@users.noreply.github.com>
2022-07-06 23:16:47 +02:00
]
2022-07-27 14:43:43 +02:00
/ * *
* Individual Flight response path
* /
Implement new client-side router (#37551)
## Client-side router for `app` directory
This PR implements the new router that leverages React 18 concurrent features like Suspense and startTransition.
It also integrates with React Server Components and builds on top of it to allow server-centric routing that only renders the part of the page that has to change.
It's one of the pieces of the implementation of https://nextjs.org/blog/layouts-rfc.
## Details
I'm going to document the differences with the current router here (will be reworked for the upgrade guide)
### Client-side cache
In the current router we have an in-memory cache for getStaticProps data so that if you prefetch and then navigate to a route that has been prefetched it'll be near-instant. For getServerSideProps the behavior is different, any navigation to a page with getServerSideProps fetches the data again.
In the new model the cache is a fundamental piece, it's more granular than at the page level and is set up to ensure consistency across concurrent renders. It can also be invalidated at any level.
#### Push/Replace (also applies to next/link)
The new router still has a `router.push` / `router.replace` method.
There are a few differences in how it works though:
- It only takes `href` as an argument, historically you had to provide `href` (the page path) and `as` (the actual url path) to do dynamic routing. In later versions of Next.js this is no longer required and in the majority of cases `as` was no longer needed. In the new router there's no way to reason about `href` vs `as` because there is no notion of "pages" in the browser.
- Both methods now use `startTransition`, you can wrap these in your own `startTransition` to get `isPending`
- The push/replace support concurrent rendering. When a render is bailed by clicking a different link to navigate to a completely different page that still works and doesn't cause race conditions.
- Support for optimistic loading states when navigating
##### Hard/Soft push/replace
Because of the client-side cache being reworked this now allows us to cover two cases: hard push and soft push.
The main difference between the two is if the cache is reused while navigating. The default for `next/link` is a `hard` push which means that the part of the cache affected by the navigation will be invalidated, e.g. if you already navigated to `/dashboard` and you `router.push('/dashboard')` again it'll get the latest version. This is similar to the existing `getServerSideProps` handling.
In case of a soft push (API to be defined but for testing added `router.softPush('/')`) it'll reuse the existing cache and not invalidate parts that are already filled in. In practice this means it's more like the `getStaticProps` client-side navigation because it does not fetch on navigation except if a part of the page is missing.
#### Back/Forward navigation
Back and Forward navigation ([popstate](https://developer.mozilla.org/en-US/docs/Web/API/Window/popstate_event)) are always handled as a soft navigation, meaning that the cache is reused, this ensures back/forward navigation is near-instant when it's in the client-side cache. This will also allow back/forward navigation to be a high priority update instead of a transition as it is based on user interaction. Note: in this PR it still uses `startTransition` as there's no way to handle the high priority update suspending which happens in case of missing data in the cache. We're working with the React team on a solution for this particular case.
### Layouts
Note: this section assumes you've read [The layouts RFC](https://nextjs.org/blog/layouts-rfc) and [React Server Components RFC](https://reactjs.org/blog/2020/12/21/data-fetching-with-react-server-components.html)
React Server Components rendering leverages the Flight streaming mechanism in React 18, this allows sending a serializable representation of the rendered React tree on the server to the browser, the client-side React can use this serialized representation to render components client-side without the JavaScript being sent to the browser. This is one of the building blocks of Server Components. This allows a bunch of interesting features but for now I'll keep it to how it affects layouts.
When you have a `app/dashboard/layout.js` and `app/dashboard/page.js` the page will render as children of the layout, when you add another page like `app/dashboard/integrations/page.js` that page falls under the dashboard layout as well. When client-side navigating the new router automatically figures out if the page you're navigating to can be a smaller render than the whole page, in this case `app/dashboard/page.js` and `app/dashboard/integrations/page.js` share the `app/dashboard/layout.js` so instead of rendering the whole page we render below the layout component, this means the layout itself does not get re-rendered, the layout's `getServerSideProps` would not be called, and the Flight response would only hold the result of `app/dashboard/integrations/page.js`, effectively giving you the smallest patch for the UI.
---
Note: the commits in this PR were mostly work in progress to ensure it wasn't lost along the way. The implementation was reworked a bunch of times to where it is now.
Co-authored-by: Jiachi Liu <4800338+huozhi@users.noreply.github.com>
Co-authored-by: JJ Kasper <22380829+ijjk@users.noreply.github.com>
2022-07-06 23:16:47 +02:00
export type FlightSegmentPath =
2022-07-27 14:43:43 +02:00
// Uses `any` as repeating pattern can't be typed.
Implement new client-side router (#37551)
## Client-side router for `app` directory
This PR implements the new router that leverages React 18 concurrent features like Suspense and startTransition.
It also integrates with React Server Components and builds on top of it to allow server-centric routing that only renders the part of the page that has to change.
It's one of the pieces of the implementation of https://nextjs.org/blog/layouts-rfc.
## Details
I'm going to document the differences with the current router here (will be reworked for the upgrade guide)
### Client-side cache
In the current router we have an in-memory cache for getStaticProps data so that if you prefetch and then navigate to a route that has been prefetched it'll be near-instant. For getServerSideProps the behavior is different, any navigation to a page with getServerSideProps fetches the data again.
In the new model the cache is a fundamental piece, it's more granular than at the page level and is set up to ensure consistency across concurrent renders. It can also be invalidated at any level.
#### Push/Replace (also applies to next/link)
The new router still has a `router.push` / `router.replace` method.
There are a few differences in how it works though:
- It only takes `href` as an argument, historically you had to provide `href` (the page path) and `as` (the actual url path) to do dynamic routing. In later versions of Next.js this is no longer required and in the majority of cases `as` was no longer needed. In the new router there's no way to reason about `href` vs `as` because there is no notion of "pages" in the browser.
- Both methods now use `startTransition`, you can wrap these in your own `startTransition` to get `isPending`
- The push/replace support concurrent rendering. When a render is bailed by clicking a different link to navigate to a completely different page that still works and doesn't cause race conditions.
- Support for optimistic loading states when navigating
##### Hard/Soft push/replace
Because of the client-side cache being reworked this now allows us to cover two cases: hard push and soft push.
The main difference between the two is if the cache is reused while navigating. The default for `next/link` is a `hard` push which means that the part of the cache affected by the navigation will be invalidated, e.g. if you already navigated to `/dashboard` and you `router.push('/dashboard')` again it'll get the latest version. This is similar to the existing `getServerSideProps` handling.
In case of a soft push (API to be defined but for testing added `router.softPush('/')`) it'll reuse the existing cache and not invalidate parts that are already filled in. In practice this means it's more like the `getStaticProps` client-side navigation because it does not fetch on navigation except if a part of the page is missing.
#### Back/Forward navigation
Back and Forward navigation ([popstate](https://developer.mozilla.org/en-US/docs/Web/API/Window/popstate_event)) are always handled as a soft navigation, meaning that the cache is reused, this ensures back/forward navigation is near-instant when it's in the client-side cache. This will also allow back/forward navigation to be a high priority update instead of a transition as it is based on user interaction. Note: in this PR it still uses `startTransition` as there's no way to handle the high priority update suspending which happens in case of missing data in the cache. We're working with the React team on a solution for this particular case.
### Layouts
Note: this section assumes you've read [The layouts RFC](https://nextjs.org/blog/layouts-rfc) and [React Server Components RFC](https://reactjs.org/blog/2020/12/21/data-fetching-with-react-server-components.html)
React Server Components rendering leverages the Flight streaming mechanism in React 18, this allows sending a serializable representation of the rendered React tree on the server to the browser, the client-side React can use this serialized representation to render components client-side without the JavaScript being sent to the browser. This is one of the building blocks of Server Components. This allows a bunch of interesting features but for now I'll keep it to how it affects layouts.
When you have a `app/dashboard/layout.js` and `app/dashboard/page.js` the page will render as children of the layout, when you add another page like `app/dashboard/integrations/page.js` that page falls under the dashboard layout as well. When client-side navigating the new router automatically figures out if the page you're navigating to can be a smaller render than the whole page, in this case `app/dashboard/page.js` and `app/dashboard/integrations/page.js` share the `app/dashboard/layout.js` so instead of rendering the whole page we render below the layout component, this means the layout itself does not get re-rendered, the layout's `getServerSideProps` would not be called, and the Flight response would only hold the result of `app/dashboard/integrations/page.js`, effectively giving you the smallest patch for the UI.
---
Note: the commits in this PR were mostly work in progress to ensure it wasn't lost along the way. The implementation was reworked a bunch of times to where it is now.
Co-authored-by: Jiachi Liu <4800338+huozhi@users.noreply.github.com>
Co-authored-by: JJ Kasper <22380829+ijjk@users.noreply.github.com>
2022-07-06 23:16:47 +02:00
| any [ ]
// Looks somewhat like this
| [
2022-07-07 15:52:07 +02:00
segment : Segment ,
Implement new client-side router (#37551)
## Client-side router for `app` directory
This PR implements the new router that leverages React 18 concurrent features like Suspense and startTransition.
It also integrates with React Server Components and builds on top of it to allow server-centric routing that only renders the part of the page that has to change.
It's one of the pieces of the implementation of https://nextjs.org/blog/layouts-rfc.
## Details
I'm going to document the differences with the current router here (will be reworked for the upgrade guide)
### Client-side cache
In the current router we have an in-memory cache for getStaticProps data so that if you prefetch and then navigate to a route that has been prefetched it'll be near-instant. For getServerSideProps the behavior is different, any navigation to a page with getServerSideProps fetches the data again.
In the new model the cache is a fundamental piece, it's more granular than at the page level and is set up to ensure consistency across concurrent renders. It can also be invalidated at any level.
#### Push/Replace (also applies to next/link)
The new router still has a `router.push` / `router.replace` method.
There are a few differences in how it works though:
- It only takes `href` as an argument, historically you had to provide `href` (the page path) and `as` (the actual url path) to do dynamic routing. In later versions of Next.js this is no longer required and in the majority of cases `as` was no longer needed. In the new router there's no way to reason about `href` vs `as` because there is no notion of "pages" in the browser.
- Both methods now use `startTransition`, you can wrap these in your own `startTransition` to get `isPending`
- The push/replace support concurrent rendering. When a render is bailed by clicking a different link to navigate to a completely different page that still works and doesn't cause race conditions.
- Support for optimistic loading states when navigating
##### Hard/Soft push/replace
Because of the client-side cache being reworked this now allows us to cover two cases: hard push and soft push.
The main difference between the two is if the cache is reused while navigating. The default for `next/link` is a `hard` push which means that the part of the cache affected by the navigation will be invalidated, e.g. if you already navigated to `/dashboard` and you `router.push('/dashboard')` again it'll get the latest version. This is similar to the existing `getServerSideProps` handling.
In case of a soft push (API to be defined but for testing added `router.softPush('/')`) it'll reuse the existing cache and not invalidate parts that are already filled in. In practice this means it's more like the `getStaticProps` client-side navigation because it does not fetch on navigation except if a part of the page is missing.
#### Back/Forward navigation
Back and Forward navigation ([popstate](https://developer.mozilla.org/en-US/docs/Web/API/Window/popstate_event)) are always handled as a soft navigation, meaning that the cache is reused, this ensures back/forward navigation is near-instant when it's in the client-side cache. This will also allow back/forward navigation to be a high priority update instead of a transition as it is based on user interaction. Note: in this PR it still uses `startTransition` as there's no way to handle the high priority update suspending which happens in case of missing data in the cache. We're working with the React team on a solution for this particular case.
### Layouts
Note: this section assumes you've read [The layouts RFC](https://nextjs.org/blog/layouts-rfc) and [React Server Components RFC](https://reactjs.org/blog/2020/12/21/data-fetching-with-react-server-components.html)
React Server Components rendering leverages the Flight streaming mechanism in React 18, this allows sending a serializable representation of the rendered React tree on the server to the browser, the client-side React can use this serialized representation to render components client-side without the JavaScript being sent to the browser. This is one of the building blocks of Server Components. This allows a bunch of interesting features but for now I'll keep it to how it affects layouts.
When you have a `app/dashboard/layout.js` and `app/dashboard/page.js` the page will render as children of the layout, when you add another page like `app/dashboard/integrations/page.js` that page falls under the dashboard layout as well. When client-side navigating the new router automatically figures out if the page you're navigating to can be a smaller render than the whole page, in this case `app/dashboard/page.js` and `app/dashboard/integrations/page.js` share the `app/dashboard/layout.js` so instead of rendering the whole page we render below the layout component, this means the layout itself does not get re-rendered, the layout's `getServerSideProps` would not be called, and the Flight response would only hold the result of `app/dashboard/integrations/page.js`, effectively giving you the smallest patch for the UI.
---
Note: the commits in this PR were mostly work in progress to ensure it wasn't lost along the way. The implementation was reworked a bunch of times to where it is now.
Co-authored-by: Jiachi Liu <4800338+huozhi@users.noreply.github.com>
Co-authored-by: JJ Kasper <22380829+ijjk@users.noreply.github.com>
2022-07-06 23:16:47 +02:00
parallelRouterKey : string ,
2022-07-07 15:52:07 +02:00
segment : Segment ,
Implement new client-side router (#37551)
## Client-side router for `app` directory
This PR implements the new router that leverages React 18 concurrent features like Suspense and startTransition.
It also integrates with React Server Components and builds on top of it to allow server-centric routing that only renders the part of the page that has to change.
It's one of the pieces of the implementation of https://nextjs.org/blog/layouts-rfc.
## Details
I'm going to document the differences with the current router here (will be reworked for the upgrade guide)
### Client-side cache
In the current router we have an in-memory cache for getStaticProps data so that if you prefetch and then navigate to a route that has been prefetched it'll be near-instant. For getServerSideProps the behavior is different, any navigation to a page with getServerSideProps fetches the data again.
In the new model the cache is a fundamental piece, it's more granular than at the page level and is set up to ensure consistency across concurrent renders. It can also be invalidated at any level.
#### Push/Replace (also applies to next/link)
The new router still has a `router.push` / `router.replace` method.
There are a few differences in how it works though:
- It only takes `href` as an argument, historically you had to provide `href` (the page path) and `as` (the actual url path) to do dynamic routing. In later versions of Next.js this is no longer required and in the majority of cases `as` was no longer needed. In the new router there's no way to reason about `href` vs `as` because there is no notion of "pages" in the browser.
- Both methods now use `startTransition`, you can wrap these in your own `startTransition` to get `isPending`
- The push/replace support concurrent rendering. When a render is bailed by clicking a different link to navigate to a completely different page that still works and doesn't cause race conditions.
- Support for optimistic loading states when navigating
##### Hard/Soft push/replace
Because of the client-side cache being reworked this now allows us to cover two cases: hard push and soft push.
The main difference between the two is if the cache is reused while navigating. The default for `next/link` is a `hard` push which means that the part of the cache affected by the navigation will be invalidated, e.g. if you already navigated to `/dashboard` and you `router.push('/dashboard')` again it'll get the latest version. This is similar to the existing `getServerSideProps` handling.
In case of a soft push (API to be defined but for testing added `router.softPush('/')`) it'll reuse the existing cache and not invalidate parts that are already filled in. In practice this means it's more like the `getStaticProps` client-side navigation because it does not fetch on navigation except if a part of the page is missing.
#### Back/Forward navigation
Back and Forward navigation ([popstate](https://developer.mozilla.org/en-US/docs/Web/API/Window/popstate_event)) are always handled as a soft navigation, meaning that the cache is reused, this ensures back/forward navigation is near-instant when it's in the client-side cache. This will also allow back/forward navigation to be a high priority update instead of a transition as it is based on user interaction. Note: in this PR it still uses `startTransition` as there's no way to handle the high priority update suspending which happens in case of missing data in the cache. We're working with the React team on a solution for this particular case.
### Layouts
Note: this section assumes you've read [The layouts RFC](https://nextjs.org/blog/layouts-rfc) and [React Server Components RFC](https://reactjs.org/blog/2020/12/21/data-fetching-with-react-server-components.html)
React Server Components rendering leverages the Flight streaming mechanism in React 18, this allows sending a serializable representation of the rendered React tree on the server to the browser, the client-side React can use this serialized representation to render components client-side without the JavaScript being sent to the browser. This is one of the building blocks of Server Components. This allows a bunch of interesting features but for now I'll keep it to how it affects layouts.
When you have a `app/dashboard/layout.js` and `app/dashboard/page.js` the page will render as children of the layout, when you add another page like `app/dashboard/integrations/page.js` that page falls under the dashboard layout as well. When client-side navigating the new router automatically figures out if the page you're navigating to can be a smaller render than the whole page, in this case `app/dashboard/page.js` and `app/dashboard/integrations/page.js` share the `app/dashboard/layout.js` so instead of rendering the whole page we render below the layout component, this means the layout itself does not get re-rendered, the layout's `getServerSideProps` would not be called, and the Flight response would only hold the result of `app/dashboard/integrations/page.js`, effectively giving you the smallest patch for the UI.
---
Note: the commits in this PR were mostly work in progress to ensure it wasn't lost along the way. The implementation was reworked a bunch of times to where it is now.
Co-authored-by: Jiachi Liu <4800338+huozhi@users.noreply.github.com>
Co-authored-by: JJ Kasper <22380829+ijjk@users.noreply.github.com>
2022-07-06 23:16:47 +02:00
parallelRouterKey : string ,
2022-07-07 15:52:07 +02:00
segment : Segment ,
Implement new client-side router (#37551)
## Client-side router for `app` directory
This PR implements the new router that leverages React 18 concurrent features like Suspense and startTransition.
It also integrates with React Server Components and builds on top of it to allow server-centric routing that only renders the part of the page that has to change.
It's one of the pieces of the implementation of https://nextjs.org/blog/layouts-rfc.
## Details
I'm going to document the differences with the current router here (will be reworked for the upgrade guide)
### Client-side cache
In the current router we have an in-memory cache for getStaticProps data so that if you prefetch and then navigate to a route that has been prefetched it'll be near-instant. For getServerSideProps the behavior is different, any navigation to a page with getServerSideProps fetches the data again.
In the new model the cache is a fundamental piece, it's more granular than at the page level and is set up to ensure consistency across concurrent renders. It can also be invalidated at any level.
#### Push/Replace (also applies to next/link)
The new router still has a `router.push` / `router.replace` method.
There are a few differences in how it works though:
- It only takes `href` as an argument, historically you had to provide `href` (the page path) and `as` (the actual url path) to do dynamic routing. In later versions of Next.js this is no longer required and in the majority of cases `as` was no longer needed. In the new router there's no way to reason about `href` vs `as` because there is no notion of "pages" in the browser.
- Both methods now use `startTransition`, you can wrap these in your own `startTransition` to get `isPending`
- The push/replace support concurrent rendering. When a render is bailed by clicking a different link to navigate to a completely different page that still works and doesn't cause race conditions.
- Support for optimistic loading states when navigating
##### Hard/Soft push/replace
Because of the client-side cache being reworked this now allows us to cover two cases: hard push and soft push.
The main difference between the two is if the cache is reused while navigating. The default for `next/link` is a `hard` push which means that the part of the cache affected by the navigation will be invalidated, e.g. if you already navigated to `/dashboard` and you `router.push('/dashboard')` again it'll get the latest version. This is similar to the existing `getServerSideProps` handling.
In case of a soft push (API to be defined but for testing added `router.softPush('/')`) it'll reuse the existing cache and not invalidate parts that are already filled in. In practice this means it's more like the `getStaticProps` client-side navigation because it does not fetch on navigation except if a part of the page is missing.
#### Back/Forward navigation
Back and Forward navigation ([popstate](https://developer.mozilla.org/en-US/docs/Web/API/Window/popstate_event)) are always handled as a soft navigation, meaning that the cache is reused, this ensures back/forward navigation is near-instant when it's in the client-side cache. This will also allow back/forward navigation to be a high priority update instead of a transition as it is based on user interaction. Note: in this PR it still uses `startTransition` as there's no way to handle the high priority update suspending which happens in case of missing data in the cache. We're working with the React team on a solution for this particular case.
### Layouts
Note: this section assumes you've read [The layouts RFC](https://nextjs.org/blog/layouts-rfc) and [React Server Components RFC](https://reactjs.org/blog/2020/12/21/data-fetching-with-react-server-components.html)
React Server Components rendering leverages the Flight streaming mechanism in React 18, this allows sending a serializable representation of the rendered React tree on the server to the browser, the client-side React can use this serialized representation to render components client-side without the JavaScript being sent to the browser. This is one of the building blocks of Server Components. This allows a bunch of interesting features but for now I'll keep it to how it affects layouts.
When you have a `app/dashboard/layout.js` and `app/dashboard/page.js` the page will render as children of the layout, when you add another page like `app/dashboard/integrations/page.js` that page falls under the dashboard layout as well. When client-side navigating the new router automatically figures out if the page you're navigating to can be a smaller render than the whole page, in this case `app/dashboard/page.js` and `app/dashboard/integrations/page.js` share the `app/dashboard/layout.js` so instead of rendering the whole page we render below the layout component, this means the layout itself does not get re-rendered, the layout's `getServerSideProps` would not be called, and the Flight response would only hold the result of `app/dashboard/integrations/page.js`, effectively giving you the smallest patch for the UI.
---
Note: the commits in this PR were mostly work in progress to ensure it wasn't lost along the way. The implementation was reworked a bunch of times to where it is now.
Co-authored-by: Jiachi Liu <4800338+huozhi@users.noreply.github.com>
Co-authored-by: JJ Kasper <22380829+ijjk@users.noreply.github.com>
2022-07-06 23:16:47 +02:00
parallelRouterKey : string
]
export type FlightDataPath =
2022-07-27 14:43:43 +02:00
// Uses `any` as repeating pattern can't be typed.
Implement new client-side router (#37551)
## Client-side router for `app` directory
This PR implements the new router that leverages React 18 concurrent features like Suspense and startTransition.
It also integrates with React Server Components and builds on top of it to allow server-centric routing that only renders the part of the page that has to change.
It's one of the pieces of the implementation of https://nextjs.org/blog/layouts-rfc.
## Details
I'm going to document the differences with the current router here (will be reworked for the upgrade guide)
### Client-side cache
In the current router we have an in-memory cache for getStaticProps data so that if you prefetch and then navigate to a route that has been prefetched it'll be near-instant. For getServerSideProps the behavior is different, any navigation to a page with getServerSideProps fetches the data again.
In the new model the cache is a fundamental piece, it's more granular than at the page level and is set up to ensure consistency across concurrent renders. It can also be invalidated at any level.
#### Push/Replace (also applies to next/link)
The new router still has a `router.push` / `router.replace` method.
There are a few differences in how it works though:
- It only takes `href` as an argument, historically you had to provide `href` (the page path) and `as` (the actual url path) to do dynamic routing. In later versions of Next.js this is no longer required and in the majority of cases `as` was no longer needed. In the new router there's no way to reason about `href` vs `as` because there is no notion of "pages" in the browser.
- Both methods now use `startTransition`, you can wrap these in your own `startTransition` to get `isPending`
- The push/replace support concurrent rendering. When a render is bailed by clicking a different link to navigate to a completely different page that still works and doesn't cause race conditions.
- Support for optimistic loading states when navigating
##### Hard/Soft push/replace
Because of the client-side cache being reworked this now allows us to cover two cases: hard push and soft push.
The main difference between the two is if the cache is reused while navigating. The default for `next/link` is a `hard` push which means that the part of the cache affected by the navigation will be invalidated, e.g. if you already navigated to `/dashboard` and you `router.push('/dashboard')` again it'll get the latest version. This is similar to the existing `getServerSideProps` handling.
In case of a soft push (API to be defined but for testing added `router.softPush('/')`) it'll reuse the existing cache and not invalidate parts that are already filled in. In practice this means it's more like the `getStaticProps` client-side navigation because it does not fetch on navigation except if a part of the page is missing.
#### Back/Forward navigation
Back and Forward navigation ([popstate](https://developer.mozilla.org/en-US/docs/Web/API/Window/popstate_event)) are always handled as a soft navigation, meaning that the cache is reused, this ensures back/forward navigation is near-instant when it's in the client-side cache. This will also allow back/forward navigation to be a high priority update instead of a transition as it is based on user interaction. Note: in this PR it still uses `startTransition` as there's no way to handle the high priority update suspending which happens in case of missing data in the cache. We're working with the React team on a solution for this particular case.
### Layouts
Note: this section assumes you've read [The layouts RFC](https://nextjs.org/blog/layouts-rfc) and [React Server Components RFC](https://reactjs.org/blog/2020/12/21/data-fetching-with-react-server-components.html)
React Server Components rendering leverages the Flight streaming mechanism in React 18, this allows sending a serializable representation of the rendered React tree on the server to the browser, the client-side React can use this serialized representation to render components client-side without the JavaScript being sent to the browser. This is one of the building blocks of Server Components. This allows a bunch of interesting features but for now I'll keep it to how it affects layouts.
When you have a `app/dashboard/layout.js` and `app/dashboard/page.js` the page will render as children of the layout, when you add another page like `app/dashboard/integrations/page.js` that page falls under the dashboard layout as well. When client-side navigating the new router automatically figures out if the page you're navigating to can be a smaller render than the whole page, in this case `app/dashboard/page.js` and `app/dashboard/integrations/page.js` share the `app/dashboard/layout.js` so instead of rendering the whole page we render below the layout component, this means the layout itself does not get re-rendered, the layout's `getServerSideProps` would not be called, and the Flight response would only hold the result of `app/dashboard/integrations/page.js`, effectively giving you the smallest patch for the UI.
---
Note: the commits in this PR were mostly work in progress to ensure it wasn't lost along the way. The implementation was reworked a bunch of times to where it is now.
Co-authored-by: Jiachi Liu <4800338+huozhi@users.noreply.github.com>
Co-authored-by: JJ Kasper <22380829+ijjk@users.noreply.github.com>
2022-07-06 23:16:47 +02:00
| any [ ]
// Looks somewhat like this
| [
2022-07-27 14:43:43 +02:00
// Holds full path to the segment.
. . . FlightSegmentPath ,
/* segment of the rendered slice: */ Segment ,
/* treePatch */ FlightRouterState ,
2022-09-06 19:29:09 +02:00
/* subTreeData: */ React . ReactNode | null // Can be null during prefetch if there's no loading component
Implement new client-side router (#37551)
## Client-side router for `app` directory
This PR implements the new router that leverages React 18 concurrent features like Suspense and startTransition.
It also integrates with React Server Components and builds on top of it to allow server-centric routing that only renders the part of the page that has to change.
It's one of the pieces of the implementation of https://nextjs.org/blog/layouts-rfc.
## Details
I'm going to document the differences with the current router here (will be reworked for the upgrade guide)
### Client-side cache
In the current router we have an in-memory cache for getStaticProps data so that if you prefetch and then navigate to a route that has been prefetched it'll be near-instant. For getServerSideProps the behavior is different, any navigation to a page with getServerSideProps fetches the data again.
In the new model the cache is a fundamental piece, it's more granular than at the page level and is set up to ensure consistency across concurrent renders. It can also be invalidated at any level.
#### Push/Replace (also applies to next/link)
The new router still has a `router.push` / `router.replace` method.
There are a few differences in how it works though:
- It only takes `href` as an argument, historically you had to provide `href` (the page path) and `as` (the actual url path) to do dynamic routing. In later versions of Next.js this is no longer required and in the majority of cases `as` was no longer needed. In the new router there's no way to reason about `href` vs `as` because there is no notion of "pages" in the browser.
- Both methods now use `startTransition`, you can wrap these in your own `startTransition` to get `isPending`
- The push/replace support concurrent rendering. When a render is bailed by clicking a different link to navigate to a completely different page that still works and doesn't cause race conditions.
- Support for optimistic loading states when navigating
##### Hard/Soft push/replace
Because of the client-side cache being reworked this now allows us to cover two cases: hard push and soft push.
The main difference between the two is if the cache is reused while navigating. The default for `next/link` is a `hard` push which means that the part of the cache affected by the navigation will be invalidated, e.g. if you already navigated to `/dashboard` and you `router.push('/dashboard')` again it'll get the latest version. This is similar to the existing `getServerSideProps` handling.
In case of a soft push (API to be defined but for testing added `router.softPush('/')`) it'll reuse the existing cache and not invalidate parts that are already filled in. In practice this means it's more like the `getStaticProps` client-side navigation because it does not fetch on navigation except if a part of the page is missing.
#### Back/Forward navigation
Back and Forward navigation ([popstate](https://developer.mozilla.org/en-US/docs/Web/API/Window/popstate_event)) are always handled as a soft navigation, meaning that the cache is reused, this ensures back/forward navigation is near-instant when it's in the client-side cache. This will also allow back/forward navigation to be a high priority update instead of a transition as it is based on user interaction. Note: in this PR it still uses `startTransition` as there's no way to handle the high priority update suspending which happens in case of missing data in the cache. We're working with the React team on a solution for this particular case.
### Layouts
Note: this section assumes you've read [The layouts RFC](https://nextjs.org/blog/layouts-rfc) and [React Server Components RFC](https://reactjs.org/blog/2020/12/21/data-fetching-with-react-server-components.html)
React Server Components rendering leverages the Flight streaming mechanism in React 18, this allows sending a serializable representation of the rendered React tree on the server to the browser, the client-side React can use this serialized representation to render components client-side without the JavaScript being sent to the browser. This is one of the building blocks of Server Components. This allows a bunch of interesting features but for now I'll keep it to how it affects layouts.
When you have a `app/dashboard/layout.js` and `app/dashboard/page.js` the page will render as children of the layout, when you add another page like `app/dashboard/integrations/page.js` that page falls under the dashboard layout as well. When client-side navigating the new router automatically figures out if the page you're navigating to can be a smaller render than the whole page, in this case `app/dashboard/page.js` and `app/dashboard/integrations/page.js` share the `app/dashboard/layout.js` so instead of rendering the whole page we render below the layout component, this means the layout itself does not get re-rendered, the layout's `getServerSideProps` would not be called, and the Flight response would only hold the result of `app/dashboard/integrations/page.js`, effectively giving you the smallest patch for the UI.
---
Note: the commits in this PR were mostly work in progress to ensure it wasn't lost along the way. The implementation was reworked a bunch of times to where it is now.
Co-authored-by: Jiachi Liu <4800338+huozhi@users.noreply.github.com>
Co-authored-by: JJ Kasper <22380829+ijjk@users.noreply.github.com>
2022-07-06 23:16:47 +02:00
]
2022-07-27 14:43:43 +02:00
/ * *
* The Flight response data
* /
Implement new client-side router (#37551)
## Client-side router for `app` directory
This PR implements the new router that leverages React 18 concurrent features like Suspense and startTransition.
It also integrates with React Server Components and builds on top of it to allow server-centric routing that only renders the part of the page that has to change.
It's one of the pieces of the implementation of https://nextjs.org/blog/layouts-rfc.
## Details
I'm going to document the differences with the current router here (will be reworked for the upgrade guide)
### Client-side cache
In the current router we have an in-memory cache for getStaticProps data so that if you prefetch and then navigate to a route that has been prefetched it'll be near-instant. For getServerSideProps the behavior is different, any navigation to a page with getServerSideProps fetches the data again.
In the new model the cache is a fundamental piece, it's more granular than at the page level and is set up to ensure consistency across concurrent renders. It can also be invalidated at any level.
#### Push/Replace (also applies to next/link)
The new router still has a `router.push` / `router.replace` method.
There are a few differences in how it works though:
- It only takes `href` as an argument, historically you had to provide `href` (the page path) and `as` (the actual url path) to do dynamic routing. In later versions of Next.js this is no longer required and in the majority of cases `as` was no longer needed. In the new router there's no way to reason about `href` vs `as` because there is no notion of "pages" in the browser.
- Both methods now use `startTransition`, you can wrap these in your own `startTransition` to get `isPending`
- The push/replace support concurrent rendering. When a render is bailed by clicking a different link to navigate to a completely different page that still works and doesn't cause race conditions.
- Support for optimistic loading states when navigating
##### Hard/Soft push/replace
Because of the client-side cache being reworked this now allows us to cover two cases: hard push and soft push.
The main difference between the two is if the cache is reused while navigating. The default for `next/link` is a `hard` push which means that the part of the cache affected by the navigation will be invalidated, e.g. if you already navigated to `/dashboard` and you `router.push('/dashboard')` again it'll get the latest version. This is similar to the existing `getServerSideProps` handling.
In case of a soft push (API to be defined but for testing added `router.softPush('/')`) it'll reuse the existing cache and not invalidate parts that are already filled in. In practice this means it's more like the `getStaticProps` client-side navigation because it does not fetch on navigation except if a part of the page is missing.
#### Back/Forward navigation
Back and Forward navigation ([popstate](https://developer.mozilla.org/en-US/docs/Web/API/Window/popstate_event)) are always handled as a soft navigation, meaning that the cache is reused, this ensures back/forward navigation is near-instant when it's in the client-side cache. This will also allow back/forward navigation to be a high priority update instead of a transition as it is based on user interaction. Note: in this PR it still uses `startTransition` as there's no way to handle the high priority update suspending which happens in case of missing data in the cache. We're working with the React team on a solution for this particular case.
### Layouts
Note: this section assumes you've read [The layouts RFC](https://nextjs.org/blog/layouts-rfc) and [React Server Components RFC](https://reactjs.org/blog/2020/12/21/data-fetching-with-react-server-components.html)
React Server Components rendering leverages the Flight streaming mechanism in React 18, this allows sending a serializable representation of the rendered React tree on the server to the browser, the client-side React can use this serialized representation to render components client-side without the JavaScript being sent to the browser. This is one of the building blocks of Server Components. This allows a bunch of interesting features but for now I'll keep it to how it affects layouts.
When you have a `app/dashboard/layout.js` and `app/dashboard/page.js` the page will render as children of the layout, when you add another page like `app/dashboard/integrations/page.js` that page falls under the dashboard layout as well. When client-side navigating the new router automatically figures out if the page you're navigating to can be a smaller render than the whole page, in this case `app/dashboard/page.js` and `app/dashboard/integrations/page.js` share the `app/dashboard/layout.js` so instead of rendering the whole page we render below the layout component, this means the layout itself does not get re-rendered, the layout's `getServerSideProps` would not be called, and the Flight response would only hold the result of `app/dashboard/integrations/page.js`, effectively giving you the smallest patch for the UI.
---
Note: the commits in this PR were mostly work in progress to ensure it wasn't lost along the way. The implementation was reworked a bunch of times to where it is now.
Co-authored-by: Jiachi Liu <4800338+huozhi@users.noreply.github.com>
Co-authored-by: JJ Kasper <22380829+ijjk@users.noreply.github.com>
2022-07-06 23:16:47 +02:00
export type FlightData = Array < FlightDataPath > | string
2022-07-27 14:43:43 +02:00
/ * *
* Property holding the current subTreeData .
* /
2022-07-07 15:52:07 +02:00
export type ChildProp = {
2022-09-06 19:29:09 +02:00
/ * *
* Null indicates that the tree is partial
* /
current : React.ReactNode | null
2022-07-07 15:52:07 +02:00
segment : Segment
}
Implement new client-side router (#37551)
## Client-side router for `app` directory
This PR implements the new router that leverages React 18 concurrent features like Suspense and startTransition.
It also integrates with React Server Components and builds on top of it to allow server-centric routing that only renders the part of the page that has to change.
It's one of the pieces of the implementation of https://nextjs.org/blog/layouts-rfc.
## Details
I'm going to document the differences with the current router here (will be reworked for the upgrade guide)
### Client-side cache
In the current router we have an in-memory cache for getStaticProps data so that if you prefetch and then navigate to a route that has been prefetched it'll be near-instant. For getServerSideProps the behavior is different, any navigation to a page with getServerSideProps fetches the data again.
In the new model the cache is a fundamental piece, it's more granular than at the page level and is set up to ensure consistency across concurrent renders. It can also be invalidated at any level.
#### Push/Replace (also applies to next/link)
The new router still has a `router.push` / `router.replace` method.
There are a few differences in how it works though:
- It only takes `href` as an argument, historically you had to provide `href` (the page path) and `as` (the actual url path) to do dynamic routing. In later versions of Next.js this is no longer required and in the majority of cases `as` was no longer needed. In the new router there's no way to reason about `href` vs `as` because there is no notion of "pages" in the browser.
- Both methods now use `startTransition`, you can wrap these in your own `startTransition` to get `isPending`
- The push/replace support concurrent rendering. When a render is bailed by clicking a different link to navigate to a completely different page that still works and doesn't cause race conditions.
- Support for optimistic loading states when navigating
##### Hard/Soft push/replace
Because of the client-side cache being reworked this now allows us to cover two cases: hard push and soft push.
The main difference between the two is if the cache is reused while navigating. The default for `next/link` is a `hard` push which means that the part of the cache affected by the navigation will be invalidated, e.g. if you already navigated to `/dashboard` and you `router.push('/dashboard')` again it'll get the latest version. This is similar to the existing `getServerSideProps` handling.
In case of a soft push (API to be defined but for testing added `router.softPush('/')`) it'll reuse the existing cache and not invalidate parts that are already filled in. In practice this means it's more like the `getStaticProps` client-side navigation because it does not fetch on navigation except if a part of the page is missing.
#### Back/Forward navigation
Back and Forward navigation ([popstate](https://developer.mozilla.org/en-US/docs/Web/API/Window/popstate_event)) are always handled as a soft navigation, meaning that the cache is reused, this ensures back/forward navigation is near-instant when it's in the client-side cache. This will also allow back/forward navigation to be a high priority update instead of a transition as it is based on user interaction. Note: in this PR it still uses `startTransition` as there's no way to handle the high priority update suspending which happens in case of missing data in the cache. We're working with the React team on a solution for this particular case.
### Layouts
Note: this section assumes you've read [The layouts RFC](https://nextjs.org/blog/layouts-rfc) and [React Server Components RFC](https://reactjs.org/blog/2020/12/21/data-fetching-with-react-server-components.html)
React Server Components rendering leverages the Flight streaming mechanism in React 18, this allows sending a serializable representation of the rendered React tree on the server to the browser, the client-side React can use this serialized representation to render components client-side without the JavaScript being sent to the browser. This is one of the building blocks of Server Components. This allows a bunch of interesting features but for now I'll keep it to how it affects layouts.
When you have a `app/dashboard/layout.js` and `app/dashboard/page.js` the page will render as children of the layout, when you add another page like `app/dashboard/integrations/page.js` that page falls under the dashboard layout as well. When client-side navigating the new router automatically figures out if the page you're navigating to can be a smaller render than the whole page, in this case `app/dashboard/page.js` and `app/dashboard/integrations/page.js` share the `app/dashboard/layout.js` so instead of rendering the whole page we render below the layout component, this means the layout itself does not get re-rendered, the layout's `getServerSideProps` would not be called, and the Flight response would only hold the result of `app/dashboard/integrations/page.js`, effectively giving you the smallest patch for the UI.
---
Note: the commits in this PR were mostly work in progress to ensure it wasn't lost along the way. The implementation was reworked a bunch of times to where it is now.
Co-authored-by: Jiachi Liu <4800338+huozhi@users.noreply.github.com>
Co-authored-by: JJ Kasper <22380829+ijjk@users.noreply.github.com>
2022-07-06 23:16:47 +02:00
2022-07-26 12:41:51 +02:00
/ * *
* Parse dynamic route segment to type of parameter
* /
2022-07-08 12:29:41 +02:00
function getSegmentParam ( segment : string ) : {
param : string
2022-07-11 14:34:10 +02:00
type : DynamicParamTypes
2022-07-08 12:29:41 +02:00
} | null {
if ( segment . startsWith ( '[[...' ) && segment . endsWith ( ']]' ) ) {
return {
type : 'optional-catchall' ,
param : segment.slice ( 5 , - 2 ) ,
}
}
if ( segment . startsWith ( '[...' ) && segment . endsWith ( ']' ) ) {
return {
type : 'catchall' ,
param : segment.slice ( 4 , - 1 ) ,
}
}
if ( segment . startsWith ( '[' ) && segment . endsWith ( ']' ) ) {
return {
type : 'dynamic' ,
param : segment.slice ( 1 , - 1 ) ,
}
}
return null
}
2022-07-26 12:41:51 +02:00
/ * *
2022-08-12 15:01:19 +02:00
* Get inline < link > tags based on server CSS manifest . Only used when rendering to HTML .
2022-07-26 12:41:51 +02:00
* /
2022-08-17 12:56:52 +02:00
function getCssInlinedLinkTags (
serverComponentManifest : FlightManifest ,
serverCSSManifest : FlightCSSManifest ,
2022-10-12 15:41:19 +02:00
filePath : string ,
serverCSSForEntries : string [ ]
2022-08-17 12:56:52 +02:00
) : string [ ] {
2022-08-23 01:09:56 +02:00
const layoutOrPageCss =
serverCSSManifest [ filePath ] ||
serverComponentManifest . __client_css_manifest__ ? . [ filePath ]
2022-08-16 19:00:23 +02:00
2022-08-17 12:56:52 +02:00
if ( ! layoutOrPageCss ) {
return [ ]
}
2022-08-16 19:00:23 +02:00
2022-08-17 12:56:52 +02:00
const chunks = new Set < string > ( )
2022-08-16 19:00:23 +02:00
2022-08-17 12:56:52 +02:00
for ( const css of layoutOrPageCss ) {
2022-10-12 15:41:19 +02:00
// We only include the CSS if it's a global CSS, or it is used by this
// entrypoint.
if ( serverCSSForEntries . includes ( css ) || ! /\.module\.css/ . test ( css ) ) {
const mod = serverComponentManifest [ css ]
if ( mod ) {
for ( const chunk of mod . default . chunks ) {
chunks . add ( chunk )
}
2022-08-25 18:40:16 +02:00
}
2022-08-12 15:01:19 +02:00
}
}
2022-08-17 12:56:52 +02:00
return [ . . . chunks ]
2022-07-12 22:54:42 +02:00
}
2022-10-12 15:41:19 +02:00
function getServerCSSForEntries (
serverCSSManifest : FlightCSSManifest ,
entries : string [ ]
) {
const css = [ ]
for ( const entry of entries ) {
const entryName = entry . replace ( /\.[^.]+$/ , '' )
if (
serverCSSManifest . __entry_css__ &&
serverCSSManifest . __entry_css__ [ entryName ]
) {
css . push ( . . . serverCSSManifest . __entry_css__ [ entryName ] )
}
}
return css
}
2022-10-06 21:24:42 +02:00
/ * *
* Get inline < link rel = "preload" as = "font" > tags based on server CSS manifest and font loader manifest . Only used when rendering to HTML .
* /
function getPreloadedFontFilesInlineLinkTags (
serverComponentManifest : FlightManifest ,
serverCSSManifest : FlightCSSManifest ,
fontLoaderManifest : FontLoaderManifest | undefined ,
2022-10-12 15:41:19 +02:00
serverCSSForEntries : string [ ] ,
2022-10-06 21:24:42 +02:00
filePath? : string
) : string [ ] {
if ( ! fontLoaderManifest || ! filePath ) {
return [ ]
}
const layoutOrPageCss =
serverCSSManifest [ filePath ] ||
serverComponentManifest . __client_css_manifest__ ? . [ filePath ]
if ( ! layoutOrPageCss ) {
return [ ]
}
const fontFiles = new Set < string > ( )
for ( const css of layoutOrPageCss ) {
2022-10-13 13:26:12 +02:00
// We only include the CSS if it is used by this entrypoint.
if ( serverCSSForEntries . includes ( css ) ) {
2022-10-12 15:41:19 +02:00
const preloadedFontFiles = fontLoaderManifest . app [ css ]
if ( preloadedFontFiles ) {
for ( const fontFile of preloadedFontFiles ) {
fontFiles . add ( fontFile )
}
2022-10-06 21:24:42 +02:00
}
}
}
return [ . . . fontFiles ]
}
2022-09-09 00:17:15 +02:00
function getScriptNonceFromHeader ( cspHeaderValue : string ) : string | undefined {
const directives = cspHeaderValue
// Directives are split by ';'.
. split ( ';' )
. map ( ( directive ) = > directive . trim ( ) )
// First try to find the directive for the 'script-src', otherwise try to
// fallback to the 'default-src'.
const directive =
directives . find ( ( dir ) = > dir . startsWith ( 'script-src' ) ) ||
directives . find ( ( dir ) = > dir . startsWith ( 'default-src' ) )
// If no directive could be found, then we're done.
if ( ! directive ) {
return
}
// Extract the nonce from the directive
const nonce = directive
. split ( ' ' )
// Remove the 'strict-src'/'default-src' string, this can't be the nonce.
. slice ( 1 )
. map ( ( source ) = > source . trim ( ) )
// Find the first source with the 'nonce-' prefix.
. find (
( source ) = >
source . startsWith ( "'nonce-" ) &&
source . length > 8 &&
source . endsWith ( "'" )
)
// Grab the nonce by trimming the 'nonce-' prefix.
? . slice ( 7 , - 1 )
// If we could't find the nonce, then we're done.
if ( ! nonce ) {
return
}
// Don't accept the nonce value if it contains HTML escape characters.
// Technically, the spec requires a base64'd value, but this is just an
// extra layer.
if ( ESCAPE_REGEX . test ( nonce ) ) {
throw new Error (
'Nonce value from Content-Security-Policy contained HTML escape characters.\nLearn more: https://nextjs.org/docs/messages/nonce-contained-invalid-characters'
)
}
return nonce
}
2022-09-21 15:47:31 +02:00
const FLIGHT_PARAMETERS = [
2022-09-28 13:18:37 +02:00
'__rsc__' ,
'__next_router_state_tree__' ,
'__next_router_prefetch__' ,
2022-09-21 15:47:31 +02:00
] as const
function headersWithoutFlight ( headers : IncomingHttpHeaders ) {
const newHeaders = { . . . headers }
for ( const param of FLIGHT_PARAMETERS ) {
delete newHeaders [ param ]
}
return newHeaders
}
2022-09-29 10:56:28 +02:00
async function renderToString ( element : React.ReactElement ) {
if ( ! shouldUseReactRoot ) return ReactDOMServer . renderToString ( element )
const renderStream = await ReactDOMServer . renderToReadableStream ( element )
await renderStream . allReady
return streamToString ( renderStream )
}
2022-09-30 17:55:22 +02:00
function getRootLayoutPath (
[ segment , parallelRoutes , { layout } ] : LoaderTree ,
rootLayoutPath = ''
) : string | undefined {
rootLayoutPath += ` ${ segment } / `
const isLayout = typeof layout !== 'undefined'
if ( isLayout ) return rootLayoutPath
// We can't assume it's `parallelRoutes.children` here in case the root layout is `app/@something/layout.js`
// But it's not possible to be more than one parallelRoutes before the root layout is found
const child = Object . values ( parallelRoutes ) [ 0 ]
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
}
// We can't assume it's `parallelRoutes.children` here in case the root layout is `app/@something/layout.js`
// But it's not possible to be more than one parallelRoutes before the root layout is found
const child = Object . values ( parallelRoutes ) [ 0 ]
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
}
2022-07-26 12:41:51 +02:00
export async function renderToHTMLOrFlight (
2022-05-03 12:37:23 +02:00
req : IncomingMessage ,
res : ServerResponse ,
pathname : string ,
query : NextParsedUrlQuery ,
2022-10-06 21:43:23 +02:00
renderOpts : RenderOpts
2022-05-03 12:37:23 +02:00
) : Promise < RenderResult | null > {
2022-10-06 21:43:23 +02:00
/ * *
* Rules of Static & Dynamic HTML :
*
* 1 . ) We must generate static HTML unless the caller explicitly opts
* in to dynamic HTML support .
*
* 2 . ) If dynamic HTML support is requested , we must honor that request
* or throw an error . It is the sole responsibility of the caller to
* ensure they aren ' t e . g . requesting dynamic HTML for an AMP page .
*
* These rules help ensure that other existing features like request caching ,
* coalescing , and ISR continue working as intended .
* /
const isStaticGeneration = renderOpts . supportsDynamicHTML !== true
2022-09-28 16:30:49 +02:00
const isFlight = req . headers . __rsc__ !== undefined
2022-09-23 04:44:30 +02:00
const capturedErrors : Error [ ] = [ ]
const serverComponentsErrorHandler = createErrorHandler (
'serverComponentsRenderer' ,
capturedErrors
)
const flightDataRendererErrorHandler = createErrorHandler (
'flightDataRenderer' ,
capturedErrors
)
const htmlRendererErrorHandler = createErrorHandler (
'htmlRenderer' ,
capturedErrors
)
2022-09-21 20:45:33 +02:00
const {
buildManifest ,
subresourceIntegrityManifest ,
serverComponentManifest ,
serverCSSManifest = { } ,
ComponentMod ,
2022-10-03 21:27:16 +02:00
dev ,
2022-10-06 21:24:42 +02:00
fontLoaderManifest ,
2022-09-21 20:45:33 +02:00
} = renderOpts
patchFetch ( ComponentMod )
2022-09-19 20:05:28 +02:00
2022-09-23 04:44:30 +02:00
const staticGenerationAsyncStorage = ComponentMod . staticGenerationAsyncStorage
2022-09-25 11:45:00 +02:00
const requestAsyncStorage = ComponentMod . requestAsyncStorage
2022-09-19 20:05:28 +02:00
2022-09-21 02:00:19 +02:00
if (
2022-09-28 00:03:49 +02:00
staticGenerationAsyncStorage &&
2022-09-21 02:00:19 +02:00
! ( 'getStore' in staticGenerationAsyncStorage ) &&
staticGenerationAsyncStorage . inUse
) {
2022-07-13 10:20:51 +02:00
throw new Error (
2022-09-21 02:00:19 +02:00
` Invariant: A separate worker must be used for each render when AsyncLocalStorage is not available `
2022-07-13 10:20:51 +02:00
)
}
2022-09-21 02:00:19 +02:00
// we wrap the render in an AsyncLocalStorage context
const wrappedRender = async ( ) = > {
const staticGenerationStore =
'getStore' in staticGenerationAsyncStorage
? staticGenerationAsyncStorage . getStore ( )
: staticGenerationAsyncStorage
2022-05-03 12:37:23 +02:00
2022-09-21 02:00:19 +02:00
// don't modify original query object
query = Object . assign ( { } , query )
Implement new client-side router (#37551)
## Client-side router for `app` directory
This PR implements the new router that leverages React 18 concurrent features like Suspense and startTransition.
It also integrates with React Server Components and builds on top of it to allow server-centric routing that only renders the part of the page that has to change.
It's one of the pieces of the implementation of https://nextjs.org/blog/layouts-rfc.
## Details
I'm going to document the differences with the current router here (will be reworked for the upgrade guide)
### Client-side cache
In the current router we have an in-memory cache for getStaticProps data so that if you prefetch and then navigate to a route that has been prefetched it'll be near-instant. For getServerSideProps the behavior is different, any navigation to a page with getServerSideProps fetches the data again.
In the new model the cache is a fundamental piece, it's more granular than at the page level and is set up to ensure consistency across concurrent renders. It can also be invalidated at any level.
#### Push/Replace (also applies to next/link)
The new router still has a `router.push` / `router.replace` method.
There are a few differences in how it works though:
- It only takes `href` as an argument, historically you had to provide `href` (the page path) and `as` (the actual url path) to do dynamic routing. In later versions of Next.js this is no longer required and in the majority of cases `as` was no longer needed. In the new router there's no way to reason about `href` vs `as` because there is no notion of "pages" in the browser.
- Both methods now use `startTransition`, you can wrap these in your own `startTransition` to get `isPending`
- The push/replace support concurrent rendering. When a render is bailed by clicking a different link to navigate to a completely different page that still works and doesn't cause race conditions.
- Support for optimistic loading states when navigating
##### Hard/Soft push/replace
Because of the client-side cache being reworked this now allows us to cover two cases: hard push and soft push.
The main difference between the two is if the cache is reused while navigating. The default for `next/link` is a `hard` push which means that the part of the cache affected by the navigation will be invalidated, e.g. if you already navigated to `/dashboard` and you `router.push('/dashboard')` again it'll get the latest version. This is similar to the existing `getServerSideProps` handling.
In case of a soft push (API to be defined but for testing added `router.softPush('/')`) it'll reuse the existing cache and not invalidate parts that are already filled in. In practice this means it's more like the `getStaticProps` client-side navigation because it does not fetch on navigation except if a part of the page is missing.
#### Back/Forward navigation
Back and Forward navigation ([popstate](https://developer.mozilla.org/en-US/docs/Web/API/Window/popstate_event)) are always handled as a soft navigation, meaning that the cache is reused, this ensures back/forward navigation is near-instant when it's in the client-side cache. This will also allow back/forward navigation to be a high priority update instead of a transition as it is based on user interaction. Note: in this PR it still uses `startTransition` as there's no way to handle the high priority update suspending which happens in case of missing data in the cache. We're working with the React team on a solution for this particular case.
### Layouts
Note: this section assumes you've read [The layouts RFC](https://nextjs.org/blog/layouts-rfc) and [React Server Components RFC](https://reactjs.org/blog/2020/12/21/data-fetching-with-react-server-components.html)
React Server Components rendering leverages the Flight streaming mechanism in React 18, this allows sending a serializable representation of the rendered React tree on the server to the browser, the client-side React can use this serialized representation to render components client-side without the JavaScript being sent to the browser. This is one of the building blocks of Server Components. This allows a bunch of interesting features but for now I'll keep it to how it affects layouts.
When you have a `app/dashboard/layout.js` and `app/dashboard/page.js` the page will render as children of the layout, when you add another page like `app/dashboard/integrations/page.js` that page falls under the dashboard layout as well. When client-side navigating the new router automatically figures out if the page you're navigating to can be a smaller render than the whole page, in this case `app/dashboard/page.js` and `app/dashboard/integrations/page.js` share the `app/dashboard/layout.js` so instead of rendering the whole page we render below the layout component, this means the layout itself does not get re-rendered, the layout's `getServerSideProps` would not be called, and the Flight response would only hold the result of `app/dashboard/integrations/page.js`, effectively giving you the smallest patch for the UI.
---
Note: the commits in this PR were mostly work in progress to ensure it wasn't lost along the way. The implementation was reworked a bunch of times to where it is now.
Co-authored-by: Jiachi Liu <4800338+huozhi@users.noreply.github.com>
Co-authored-by: JJ Kasper <22380829+ijjk@users.noreply.github.com>
2022-07-06 23:16:47 +02:00
2022-09-28 13:18:37 +02:00
const isPrefetch = req . headers . __next_router_prefetch__ !== undefined
2022-09-21 02:00:19 +02:00
// TODO-APP: verify the tree is valid
// TODO-APP: verify query param is single value (not an array)
// TODO-APP: verify tree can't grow out of control
/ * *
* Router state provided from the client - side router . Used to handle rendering from the common layout down .
* /
2022-10-06 21:43:23 +02:00
let providedFlightRouterState : FlightRouterState = isFlight
2022-09-28 13:18:37 +02:00
? req . headers . __next_router_state_tree__
? JSON . parse ( req . headers . __next_router_state_tree__ as string )
2022-10-06 21:43:23 +02:00
: undefined
2022-09-21 02:00:19 +02:00
: undefined
Implement new client-side router (#37551)
## Client-side router for `app` directory
This PR implements the new router that leverages React 18 concurrent features like Suspense and startTransition.
It also integrates with React Server Components and builds on top of it to allow server-centric routing that only renders the part of the page that has to change.
It's one of the pieces of the implementation of https://nextjs.org/blog/layouts-rfc.
## Details
I'm going to document the differences with the current router here (will be reworked for the upgrade guide)
### Client-side cache
In the current router we have an in-memory cache for getStaticProps data so that if you prefetch and then navigate to a route that has been prefetched it'll be near-instant. For getServerSideProps the behavior is different, any navigation to a page with getServerSideProps fetches the data again.
In the new model the cache is a fundamental piece, it's more granular than at the page level and is set up to ensure consistency across concurrent renders. It can also be invalidated at any level.
#### Push/Replace (also applies to next/link)
The new router still has a `router.push` / `router.replace` method.
There are a few differences in how it works though:
- It only takes `href` as an argument, historically you had to provide `href` (the page path) and `as` (the actual url path) to do dynamic routing. In later versions of Next.js this is no longer required and in the majority of cases `as` was no longer needed. In the new router there's no way to reason about `href` vs `as` because there is no notion of "pages" in the browser.
- Both methods now use `startTransition`, you can wrap these in your own `startTransition` to get `isPending`
- The push/replace support concurrent rendering. When a render is bailed by clicking a different link to navigate to a completely different page that still works and doesn't cause race conditions.
- Support for optimistic loading states when navigating
##### Hard/Soft push/replace
Because of the client-side cache being reworked this now allows us to cover two cases: hard push and soft push.
The main difference between the two is if the cache is reused while navigating. The default for `next/link` is a `hard` push which means that the part of the cache affected by the navigation will be invalidated, e.g. if you already navigated to `/dashboard` and you `router.push('/dashboard')` again it'll get the latest version. This is similar to the existing `getServerSideProps` handling.
In case of a soft push (API to be defined but for testing added `router.softPush('/')`) it'll reuse the existing cache and not invalidate parts that are already filled in. In practice this means it's more like the `getStaticProps` client-side navigation because it does not fetch on navigation except if a part of the page is missing.
#### Back/Forward navigation
Back and Forward navigation ([popstate](https://developer.mozilla.org/en-US/docs/Web/API/Window/popstate_event)) are always handled as a soft navigation, meaning that the cache is reused, this ensures back/forward navigation is near-instant when it's in the client-side cache. This will also allow back/forward navigation to be a high priority update instead of a transition as it is based on user interaction. Note: in this PR it still uses `startTransition` as there's no way to handle the high priority update suspending which happens in case of missing data in the cache. We're working with the React team on a solution for this particular case.
### Layouts
Note: this section assumes you've read [The layouts RFC](https://nextjs.org/blog/layouts-rfc) and [React Server Components RFC](https://reactjs.org/blog/2020/12/21/data-fetching-with-react-server-components.html)
React Server Components rendering leverages the Flight streaming mechanism in React 18, this allows sending a serializable representation of the rendered React tree on the server to the browser, the client-side React can use this serialized representation to render components client-side without the JavaScript being sent to the browser. This is one of the building blocks of Server Components. This allows a bunch of interesting features but for now I'll keep it to how it affects layouts.
When you have a `app/dashboard/layout.js` and `app/dashboard/page.js` the page will render as children of the layout, when you add another page like `app/dashboard/integrations/page.js` that page falls under the dashboard layout as well. When client-side navigating the new router automatically figures out if the page you're navigating to can be a smaller render than the whole page, in this case `app/dashboard/page.js` and `app/dashboard/integrations/page.js` share the `app/dashboard/layout.js` so instead of rendering the whole page we render below the layout component, this means the layout itself does not get re-rendered, the layout's `getServerSideProps` would not be called, and the Flight response would only hold the result of `app/dashboard/integrations/page.js`, effectively giving you the smallest patch for the UI.
---
Note: the commits in this PR were mostly work in progress to ensure it wasn't lost along the way. The implementation was reworked a bunch of times to where it is now.
Co-authored-by: Jiachi Liu <4800338+huozhi@users.noreply.github.com>
Co-authored-by: JJ Kasper <22380829+ijjk@users.noreply.github.com>
2022-07-06 23:16:47 +02:00
2022-09-30 17:55:22 +02:00
/ * *
* 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 ( ) )
)
}
2022-09-21 02:00:19 +02:00
stripInternalQueries ( query )
Implement new client-side router (#37551)
## Client-side router for `app` directory
This PR implements the new router that leverages React 18 concurrent features like Suspense and startTransition.
It also integrates with React Server Components and builds on top of it to allow server-centric routing that only renders the part of the page that has to change.
It's one of the pieces of the implementation of https://nextjs.org/blog/layouts-rfc.
## Details
I'm going to document the differences with the current router here (will be reworked for the upgrade guide)
### Client-side cache
In the current router we have an in-memory cache for getStaticProps data so that if you prefetch and then navigate to a route that has been prefetched it'll be near-instant. For getServerSideProps the behavior is different, any navigation to a page with getServerSideProps fetches the data again.
In the new model the cache is a fundamental piece, it's more granular than at the page level and is set up to ensure consistency across concurrent renders. It can also be invalidated at any level.
#### Push/Replace (also applies to next/link)
The new router still has a `router.push` / `router.replace` method.
There are a few differences in how it works though:
- It only takes `href` as an argument, historically you had to provide `href` (the page path) and `as` (the actual url path) to do dynamic routing. In later versions of Next.js this is no longer required and in the majority of cases `as` was no longer needed. In the new router there's no way to reason about `href` vs `as` because there is no notion of "pages" in the browser.
- Both methods now use `startTransition`, you can wrap these in your own `startTransition` to get `isPending`
- The push/replace support concurrent rendering. When a render is bailed by clicking a different link to navigate to a completely different page that still works and doesn't cause race conditions.
- Support for optimistic loading states when navigating
##### Hard/Soft push/replace
Because of the client-side cache being reworked this now allows us to cover two cases: hard push and soft push.
The main difference between the two is if the cache is reused while navigating. The default for `next/link` is a `hard` push which means that the part of the cache affected by the navigation will be invalidated, e.g. if you already navigated to `/dashboard` and you `router.push('/dashboard')` again it'll get the latest version. This is similar to the existing `getServerSideProps` handling.
In case of a soft push (API to be defined but for testing added `router.softPush('/')`) it'll reuse the existing cache and not invalidate parts that are already filled in. In practice this means it's more like the `getStaticProps` client-side navigation because it does not fetch on navigation except if a part of the page is missing.
#### Back/Forward navigation
Back and Forward navigation ([popstate](https://developer.mozilla.org/en-US/docs/Web/API/Window/popstate_event)) are always handled as a soft navigation, meaning that the cache is reused, this ensures back/forward navigation is near-instant when it's in the client-side cache. This will also allow back/forward navigation to be a high priority update instead of a transition as it is based on user interaction. Note: in this PR it still uses `startTransition` as there's no way to handle the high priority update suspending which happens in case of missing data in the cache. We're working with the React team on a solution for this particular case.
### Layouts
Note: this section assumes you've read [The layouts RFC](https://nextjs.org/blog/layouts-rfc) and [React Server Components RFC](https://reactjs.org/blog/2020/12/21/data-fetching-with-react-server-components.html)
React Server Components rendering leverages the Flight streaming mechanism in React 18, this allows sending a serializable representation of the rendered React tree on the server to the browser, the client-side React can use this serialized representation to render components client-side without the JavaScript being sent to the browser. This is one of the building blocks of Server Components. This allows a bunch of interesting features but for now I'll keep it to how it affects layouts.
When you have a `app/dashboard/layout.js` and `app/dashboard/page.js` the page will render as children of the layout, when you add another page like `app/dashboard/integrations/page.js` that page falls under the dashboard layout as well. When client-side navigating the new router automatically figures out if the page you're navigating to can be a smaller render than the whole page, in this case `app/dashboard/page.js` and `app/dashboard/integrations/page.js` share the `app/dashboard/layout.js` so instead of rendering the whole page we render below the layout component, this means the layout itself does not get re-rendered, the layout's `getServerSideProps` would not be called, and the Flight response would only hold the result of `app/dashboard/integrations/page.js`, effectively giving you the smallest patch for the UI.
---
Note: the commits in this PR were mostly work in progress to ensure it wasn't lost along the way. The implementation was reworked a bunch of times to where it is now.
Co-authored-by: Jiachi Liu <4800338+huozhi@users.noreply.github.com>
Co-authored-by: JJ Kasper <22380829+ijjk@users.noreply.github.com>
2022-07-06 23:16:47 +02:00
2022-09-21 02:00:19 +02:00
const LayoutRouter =
2022-10-11 19:26:45 +02:00
ComponentMod . LayoutRouter as typeof import ( '../client/components/layout-router' ) . default
2022-09-21 02:00:19 +02:00
const RenderFromTemplateContext =
2022-10-11 19:26:45 +02:00
ComponentMod . RenderFromTemplateContext as typeof import ( '../client/components/render-from-template-context' ) . default
2022-07-08 12:29:41 +02:00
2022-09-21 02:00:19 +02:00
/ * *
* 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 .
* An example of this would be that ` headers ` are available but ` searchParams ` are not because that ' d mean we have to render from the root layout down on all requests .
* /
Implement new client-side router (#37551)
## Client-side router for `app` directory
This PR implements the new router that leverages React 18 concurrent features like Suspense and startTransition.
It also integrates with React Server Components and builds on top of it to allow server-centric routing that only renders the part of the page that has to change.
It's one of the pieces of the implementation of https://nextjs.org/blog/layouts-rfc.
## Details
I'm going to document the differences with the current router here (will be reworked for the upgrade guide)
### Client-side cache
In the current router we have an in-memory cache for getStaticProps data so that if you prefetch and then navigate to a route that has been prefetched it'll be near-instant. For getServerSideProps the behavior is different, any navigation to a page with getServerSideProps fetches the data again.
In the new model the cache is a fundamental piece, it's more granular than at the page level and is set up to ensure consistency across concurrent renders. It can also be invalidated at any level.
#### Push/Replace (also applies to next/link)
The new router still has a `router.push` / `router.replace` method.
There are a few differences in how it works though:
- It only takes `href` as an argument, historically you had to provide `href` (the page path) and `as` (the actual url path) to do dynamic routing. In later versions of Next.js this is no longer required and in the majority of cases `as` was no longer needed. In the new router there's no way to reason about `href` vs `as` because there is no notion of "pages" in the browser.
- Both methods now use `startTransition`, you can wrap these in your own `startTransition` to get `isPending`
- The push/replace support concurrent rendering. When a render is bailed by clicking a different link to navigate to a completely different page that still works and doesn't cause race conditions.
- Support for optimistic loading states when navigating
##### Hard/Soft push/replace
Because of the client-side cache being reworked this now allows us to cover two cases: hard push and soft push.
The main difference between the two is if the cache is reused while navigating. The default for `next/link` is a `hard` push which means that the part of the cache affected by the navigation will be invalidated, e.g. if you already navigated to `/dashboard` and you `router.push('/dashboard')` again it'll get the latest version. This is similar to the existing `getServerSideProps` handling.
In case of a soft push (API to be defined but for testing added `router.softPush('/')`) it'll reuse the existing cache and not invalidate parts that are already filled in. In practice this means it's more like the `getStaticProps` client-side navigation because it does not fetch on navigation except if a part of the page is missing.
#### Back/Forward navigation
Back and Forward navigation ([popstate](https://developer.mozilla.org/en-US/docs/Web/API/Window/popstate_event)) are always handled as a soft navigation, meaning that the cache is reused, this ensures back/forward navigation is near-instant when it's in the client-side cache. This will also allow back/forward navigation to be a high priority update instead of a transition as it is based on user interaction. Note: in this PR it still uses `startTransition` as there's no way to handle the high priority update suspending which happens in case of missing data in the cache. We're working with the React team on a solution for this particular case.
### Layouts
Note: this section assumes you've read [The layouts RFC](https://nextjs.org/blog/layouts-rfc) and [React Server Components RFC](https://reactjs.org/blog/2020/12/21/data-fetching-with-react-server-components.html)
React Server Components rendering leverages the Flight streaming mechanism in React 18, this allows sending a serializable representation of the rendered React tree on the server to the browser, the client-side React can use this serialized representation to render components client-side without the JavaScript being sent to the browser. This is one of the building blocks of Server Components. This allows a bunch of interesting features but for now I'll keep it to how it affects layouts.
When you have a `app/dashboard/layout.js` and `app/dashboard/page.js` the page will render as children of the layout, when you add another page like `app/dashboard/integrations/page.js` that page falls under the dashboard layout as well. When client-side navigating the new router automatically figures out if the page you're navigating to can be a smaller render than the whole page, in this case `app/dashboard/page.js` and `app/dashboard/integrations/page.js` share the `app/dashboard/layout.js` so instead of rendering the whole page we render below the layout component, this means the layout itself does not get re-rendered, the layout's `getServerSideProps` would not be called, and the Flight response would only hold the result of `app/dashboard/integrations/page.js`, effectively giving you the smallest patch for the UI.
---
Note: the commits in this PR were mostly work in progress to ensure it wasn't lost along the way. The implementation was reworked a bunch of times to where it is now.
Co-authored-by: Jiachi Liu <4800338+huozhi@users.noreply.github.com>
Co-authored-by: JJ Kasper <22380829+ijjk@users.noreply.github.com>
2022-07-06 23:16:47 +02:00
2022-09-21 02:00:19 +02:00
const serverContexts : Array < [ string , any ] > = [
[ 'WORKAROUND' , null ] , // TODO-APP: First value has a bug currently where the value is not set on the second request: https://github.com/facebook/react/issues/24849
]
2022-07-26 12:41:51 +02:00
2022-09-21 02:00:19 +02:00
type CreateSegmentPath = ( child : FlightSegmentPath ) = > FlightSegmentPath
Implement new client-side router (#37551)
## Client-side router for `app` directory
This PR implements the new router that leverages React 18 concurrent features like Suspense and startTransition.
It also integrates with React Server Components and builds on top of it to allow server-centric routing that only renders the part of the page that has to change.
It's one of the pieces of the implementation of https://nextjs.org/blog/layouts-rfc.
## Details
I'm going to document the differences with the current router here (will be reworked for the upgrade guide)
### Client-side cache
In the current router we have an in-memory cache for getStaticProps data so that if you prefetch and then navigate to a route that has been prefetched it'll be near-instant. For getServerSideProps the behavior is different, any navigation to a page with getServerSideProps fetches the data again.
In the new model the cache is a fundamental piece, it's more granular than at the page level and is set up to ensure consistency across concurrent renders. It can also be invalidated at any level.
#### Push/Replace (also applies to next/link)
The new router still has a `router.push` / `router.replace` method.
There are a few differences in how it works though:
- It only takes `href` as an argument, historically you had to provide `href` (the page path) and `as` (the actual url path) to do dynamic routing. In later versions of Next.js this is no longer required and in the majority of cases `as` was no longer needed. In the new router there's no way to reason about `href` vs `as` because there is no notion of "pages" in the browser.
- Both methods now use `startTransition`, you can wrap these in your own `startTransition` to get `isPending`
- The push/replace support concurrent rendering. When a render is bailed by clicking a different link to navigate to a completely different page that still works and doesn't cause race conditions.
- Support for optimistic loading states when navigating
##### Hard/Soft push/replace
Because of the client-side cache being reworked this now allows us to cover two cases: hard push and soft push.
The main difference between the two is if the cache is reused while navigating. The default for `next/link` is a `hard` push which means that the part of the cache affected by the navigation will be invalidated, e.g. if you already navigated to `/dashboard` and you `router.push('/dashboard')` again it'll get the latest version. This is similar to the existing `getServerSideProps` handling.
In case of a soft push (API to be defined but for testing added `router.softPush('/')`) it'll reuse the existing cache and not invalidate parts that are already filled in. In practice this means it's more like the `getStaticProps` client-side navigation because it does not fetch on navigation except if a part of the page is missing.
#### Back/Forward navigation
Back and Forward navigation ([popstate](https://developer.mozilla.org/en-US/docs/Web/API/Window/popstate_event)) are always handled as a soft navigation, meaning that the cache is reused, this ensures back/forward navigation is near-instant when it's in the client-side cache. This will also allow back/forward navigation to be a high priority update instead of a transition as it is based on user interaction. Note: in this PR it still uses `startTransition` as there's no way to handle the high priority update suspending which happens in case of missing data in the cache. We're working with the React team on a solution for this particular case.
### Layouts
Note: this section assumes you've read [The layouts RFC](https://nextjs.org/blog/layouts-rfc) and [React Server Components RFC](https://reactjs.org/blog/2020/12/21/data-fetching-with-react-server-components.html)
React Server Components rendering leverages the Flight streaming mechanism in React 18, this allows sending a serializable representation of the rendered React tree on the server to the browser, the client-side React can use this serialized representation to render components client-side without the JavaScript being sent to the browser. This is one of the building blocks of Server Components. This allows a bunch of interesting features but for now I'll keep it to how it affects layouts.
When you have a `app/dashboard/layout.js` and `app/dashboard/page.js` the page will render as children of the layout, when you add another page like `app/dashboard/integrations/page.js` that page falls under the dashboard layout as well. When client-side navigating the new router automatically figures out if the page you're navigating to can be a smaller render than the whole page, in this case `app/dashboard/page.js` and `app/dashboard/integrations/page.js` share the `app/dashboard/layout.js` so instead of rendering the whole page we render below the layout component, this means the layout itself does not get re-rendered, the layout's `getServerSideProps` would not be called, and the Flight response would only hold the result of `app/dashboard/integrations/page.js`, effectively giving you the smallest patch for the UI.
---
Note: the commits in this PR were mostly work in progress to ensure it wasn't lost along the way. The implementation was reworked a bunch of times to where it is now.
Co-authored-by: Jiachi Liu <4800338+huozhi@users.noreply.github.com>
Co-authored-by: JJ Kasper <22380829+ijjk@users.noreply.github.com>
2022-07-06 23:16:47 +02:00
2022-09-21 02:00:19 +02:00
/ * *
* Dynamic parameters . E . g . when you visit ` /dashboard/vercel ` which is rendered by ` /dashboard/[slug] ` the value will be { "slug" : "vercel" } .
* /
const pathParams = ( renderOpts as any ) . params as ParsedUrlQuery
Implement new client-side router (#37551)
## Client-side router for `app` directory
This PR implements the new router that leverages React 18 concurrent features like Suspense and startTransition.
It also integrates with React Server Components and builds on top of it to allow server-centric routing that only renders the part of the page that has to change.
It's one of the pieces of the implementation of https://nextjs.org/blog/layouts-rfc.
## Details
I'm going to document the differences with the current router here (will be reworked for the upgrade guide)
### Client-side cache
In the current router we have an in-memory cache for getStaticProps data so that if you prefetch and then navigate to a route that has been prefetched it'll be near-instant. For getServerSideProps the behavior is different, any navigation to a page with getServerSideProps fetches the data again.
In the new model the cache is a fundamental piece, it's more granular than at the page level and is set up to ensure consistency across concurrent renders. It can also be invalidated at any level.
#### Push/Replace (also applies to next/link)
The new router still has a `router.push` / `router.replace` method.
There are a few differences in how it works though:
- It only takes `href` as an argument, historically you had to provide `href` (the page path) and `as` (the actual url path) to do dynamic routing. In later versions of Next.js this is no longer required and in the majority of cases `as` was no longer needed. In the new router there's no way to reason about `href` vs `as` because there is no notion of "pages" in the browser.
- Both methods now use `startTransition`, you can wrap these in your own `startTransition` to get `isPending`
- The push/replace support concurrent rendering. When a render is bailed by clicking a different link to navigate to a completely different page that still works and doesn't cause race conditions.
- Support for optimistic loading states when navigating
##### Hard/Soft push/replace
Because of the client-side cache being reworked this now allows us to cover two cases: hard push and soft push.
The main difference between the two is if the cache is reused while navigating. The default for `next/link` is a `hard` push which means that the part of the cache affected by the navigation will be invalidated, e.g. if you already navigated to `/dashboard` and you `router.push('/dashboard')` again it'll get the latest version. This is similar to the existing `getServerSideProps` handling.
In case of a soft push (API to be defined but for testing added `router.softPush('/')`) it'll reuse the existing cache and not invalidate parts that are already filled in. In practice this means it's more like the `getStaticProps` client-side navigation because it does not fetch on navigation except if a part of the page is missing.
#### Back/Forward navigation
Back and Forward navigation ([popstate](https://developer.mozilla.org/en-US/docs/Web/API/Window/popstate_event)) are always handled as a soft navigation, meaning that the cache is reused, this ensures back/forward navigation is near-instant when it's in the client-side cache. This will also allow back/forward navigation to be a high priority update instead of a transition as it is based on user interaction. Note: in this PR it still uses `startTransition` as there's no way to handle the high priority update suspending which happens in case of missing data in the cache. We're working with the React team on a solution for this particular case.
### Layouts
Note: this section assumes you've read [The layouts RFC](https://nextjs.org/blog/layouts-rfc) and [React Server Components RFC](https://reactjs.org/blog/2020/12/21/data-fetching-with-react-server-components.html)
React Server Components rendering leverages the Flight streaming mechanism in React 18, this allows sending a serializable representation of the rendered React tree on the server to the browser, the client-side React can use this serialized representation to render components client-side without the JavaScript being sent to the browser. This is one of the building blocks of Server Components. This allows a bunch of interesting features but for now I'll keep it to how it affects layouts.
When you have a `app/dashboard/layout.js` and `app/dashboard/page.js` the page will render as children of the layout, when you add another page like `app/dashboard/integrations/page.js` that page falls under the dashboard layout as well. When client-side navigating the new router automatically figures out if the page you're navigating to can be a smaller render than the whole page, in this case `app/dashboard/page.js` and `app/dashboard/integrations/page.js` share the `app/dashboard/layout.js` so instead of rendering the whole page we render below the layout component, this means the layout itself does not get re-rendered, the layout's `getServerSideProps` would not be called, and the Flight response would only hold the result of `app/dashboard/integrations/page.js`, effectively giving you the smallest patch for the UI.
---
Note: the commits in this PR were mostly work in progress to ensure it wasn't lost along the way. The implementation was reworked a bunch of times to where it is now.
Co-authored-by: Jiachi Liu <4800338+huozhi@users.noreply.github.com>
Co-authored-by: JJ Kasper <22380829+ijjk@users.noreply.github.com>
2022-07-06 23:16:47 +02:00
2022-09-21 02:00:19 +02:00
/ * *
* Parse the dynamic segment and return the associated value .
* /
const getDynamicParamFromSegment = (
// [slug] / [[slug]] / [...slug]
segment : string
) : {
param : string
value : string | string [ ] | null
treeSegment : Segment
type : DynamicParamTypesShort
} | null = > {
const segmentParam = getSegmentParam ( segment )
if ( ! segmentParam ) {
return null
}
Implement new client-side router (#37551)
## Client-side router for `app` directory
This PR implements the new router that leverages React 18 concurrent features like Suspense and startTransition.
It also integrates with React Server Components and builds on top of it to allow server-centric routing that only renders the part of the page that has to change.
It's one of the pieces of the implementation of https://nextjs.org/blog/layouts-rfc.
## Details
I'm going to document the differences with the current router here (will be reworked for the upgrade guide)
### Client-side cache
In the current router we have an in-memory cache for getStaticProps data so that if you prefetch and then navigate to a route that has been prefetched it'll be near-instant. For getServerSideProps the behavior is different, any navigation to a page with getServerSideProps fetches the data again.
In the new model the cache is a fundamental piece, it's more granular than at the page level and is set up to ensure consistency across concurrent renders. It can also be invalidated at any level.
#### Push/Replace (also applies to next/link)
The new router still has a `router.push` / `router.replace` method.
There are a few differences in how it works though:
- It only takes `href` as an argument, historically you had to provide `href` (the page path) and `as` (the actual url path) to do dynamic routing. In later versions of Next.js this is no longer required and in the majority of cases `as` was no longer needed. In the new router there's no way to reason about `href` vs `as` because there is no notion of "pages" in the browser.
- Both methods now use `startTransition`, you can wrap these in your own `startTransition` to get `isPending`
- The push/replace support concurrent rendering. When a render is bailed by clicking a different link to navigate to a completely different page that still works and doesn't cause race conditions.
- Support for optimistic loading states when navigating
##### Hard/Soft push/replace
Because of the client-side cache being reworked this now allows us to cover two cases: hard push and soft push.
The main difference between the two is if the cache is reused while navigating. The default for `next/link` is a `hard` push which means that the part of the cache affected by the navigation will be invalidated, e.g. if you already navigated to `/dashboard` and you `router.push('/dashboard')` again it'll get the latest version. This is similar to the existing `getServerSideProps` handling.
In case of a soft push (API to be defined but for testing added `router.softPush('/')`) it'll reuse the existing cache and not invalidate parts that are already filled in. In practice this means it's more like the `getStaticProps` client-side navigation because it does not fetch on navigation except if a part of the page is missing.
#### Back/Forward navigation
Back and Forward navigation ([popstate](https://developer.mozilla.org/en-US/docs/Web/API/Window/popstate_event)) are always handled as a soft navigation, meaning that the cache is reused, this ensures back/forward navigation is near-instant when it's in the client-side cache. This will also allow back/forward navigation to be a high priority update instead of a transition as it is based on user interaction. Note: in this PR it still uses `startTransition` as there's no way to handle the high priority update suspending which happens in case of missing data in the cache. We're working with the React team on a solution for this particular case.
### Layouts
Note: this section assumes you've read [The layouts RFC](https://nextjs.org/blog/layouts-rfc) and [React Server Components RFC](https://reactjs.org/blog/2020/12/21/data-fetching-with-react-server-components.html)
React Server Components rendering leverages the Flight streaming mechanism in React 18, this allows sending a serializable representation of the rendered React tree on the server to the browser, the client-side React can use this serialized representation to render components client-side without the JavaScript being sent to the browser. This is one of the building blocks of Server Components. This allows a bunch of interesting features but for now I'll keep it to how it affects layouts.
When you have a `app/dashboard/layout.js` and `app/dashboard/page.js` the page will render as children of the layout, when you add another page like `app/dashboard/integrations/page.js` that page falls under the dashboard layout as well. When client-side navigating the new router automatically figures out if the page you're navigating to can be a smaller render than the whole page, in this case `app/dashboard/page.js` and `app/dashboard/integrations/page.js` share the `app/dashboard/layout.js` so instead of rendering the whole page we render below the layout component, this means the layout itself does not get re-rendered, the layout's `getServerSideProps` would not be called, and the Flight response would only hold the result of `app/dashboard/integrations/page.js`, effectively giving you the smallest patch for the UI.
---
Note: the commits in this PR were mostly work in progress to ensure it wasn't lost along the way. The implementation was reworked a bunch of times to where it is now.
Co-authored-by: Jiachi Liu <4800338+huozhi@users.noreply.github.com>
Co-authored-by: JJ Kasper <22380829+ijjk@users.noreply.github.com>
2022-07-06 23:16:47 +02:00
2022-09-21 02:00:19 +02:00
const key = segmentParam . param
const value = pathParams [ key ]
if ( ! value ) {
// Handle case where optional catchall does not have a value, e.g. `/dashboard/[...slug]` when requesting `/dashboard`
if ( segmentParam . type === 'optional-catchall' ) {
const type = getShortDynamicParamType ( segmentParam . type )
return {
param : key ,
value : null ,
type : type ,
// This value always has to be a string.
treeSegment : [ key , '' , type ] ,
}
}
return null
}
2022-07-13 17:52:52 +02:00
2022-09-21 02:00:19 +02:00
const type = getShortDynamicParamType ( segmentParam . type )
Implement new client-side router (#37551)
## Client-side router for `app` directory
This PR implements the new router that leverages React 18 concurrent features like Suspense and startTransition.
It also integrates with React Server Components and builds on top of it to allow server-centric routing that only renders the part of the page that has to change.
It's one of the pieces of the implementation of https://nextjs.org/blog/layouts-rfc.
## Details
I'm going to document the differences with the current router here (will be reworked for the upgrade guide)
### Client-side cache
In the current router we have an in-memory cache for getStaticProps data so that if you prefetch and then navigate to a route that has been prefetched it'll be near-instant. For getServerSideProps the behavior is different, any navigation to a page with getServerSideProps fetches the data again.
In the new model the cache is a fundamental piece, it's more granular than at the page level and is set up to ensure consistency across concurrent renders. It can also be invalidated at any level.
#### Push/Replace (also applies to next/link)
The new router still has a `router.push` / `router.replace` method.
There are a few differences in how it works though:
- It only takes `href` as an argument, historically you had to provide `href` (the page path) and `as` (the actual url path) to do dynamic routing. In later versions of Next.js this is no longer required and in the majority of cases `as` was no longer needed. In the new router there's no way to reason about `href` vs `as` because there is no notion of "pages" in the browser.
- Both methods now use `startTransition`, you can wrap these in your own `startTransition` to get `isPending`
- The push/replace support concurrent rendering. When a render is bailed by clicking a different link to navigate to a completely different page that still works and doesn't cause race conditions.
- Support for optimistic loading states when navigating
##### Hard/Soft push/replace
Because of the client-side cache being reworked this now allows us to cover two cases: hard push and soft push.
The main difference between the two is if the cache is reused while navigating. The default for `next/link` is a `hard` push which means that the part of the cache affected by the navigation will be invalidated, e.g. if you already navigated to `/dashboard` and you `router.push('/dashboard')` again it'll get the latest version. This is similar to the existing `getServerSideProps` handling.
In case of a soft push (API to be defined but for testing added `router.softPush('/')`) it'll reuse the existing cache and not invalidate parts that are already filled in. In practice this means it's more like the `getStaticProps` client-side navigation because it does not fetch on navigation except if a part of the page is missing.
#### Back/Forward navigation
Back and Forward navigation ([popstate](https://developer.mozilla.org/en-US/docs/Web/API/Window/popstate_event)) are always handled as a soft navigation, meaning that the cache is reused, this ensures back/forward navigation is near-instant when it's in the client-side cache. This will also allow back/forward navigation to be a high priority update instead of a transition as it is based on user interaction. Note: in this PR it still uses `startTransition` as there's no way to handle the high priority update suspending which happens in case of missing data in the cache. We're working with the React team on a solution for this particular case.
### Layouts
Note: this section assumes you've read [The layouts RFC](https://nextjs.org/blog/layouts-rfc) and [React Server Components RFC](https://reactjs.org/blog/2020/12/21/data-fetching-with-react-server-components.html)
React Server Components rendering leverages the Flight streaming mechanism in React 18, this allows sending a serializable representation of the rendered React tree on the server to the browser, the client-side React can use this serialized representation to render components client-side without the JavaScript being sent to the browser. This is one of the building blocks of Server Components. This allows a bunch of interesting features but for now I'll keep it to how it affects layouts.
When you have a `app/dashboard/layout.js` and `app/dashboard/page.js` the page will render as children of the layout, when you add another page like `app/dashboard/integrations/page.js` that page falls under the dashboard layout as well. When client-side navigating the new router automatically figures out if the page you're navigating to can be a smaller render than the whole page, in this case `app/dashboard/page.js` and `app/dashboard/integrations/page.js` share the `app/dashboard/layout.js` so instead of rendering the whole page we render below the layout component, this means the layout itself does not get re-rendered, the layout's `getServerSideProps` would not be called, and the Flight response would only hold the result of `app/dashboard/integrations/page.js`, effectively giving you the smallest patch for the UI.
---
Note: the commits in this PR were mostly work in progress to ensure it wasn't lost along the way. The implementation was reworked a bunch of times to where it is now.
Co-authored-by: Jiachi Liu <4800338+huozhi@users.noreply.github.com>
Co-authored-by: JJ Kasper <22380829+ijjk@users.noreply.github.com>
2022-07-06 23:16:47 +02:00
2022-09-21 02:00:19 +02:00
return {
param : key ,
// The value that is passed to user code.
value : value ,
// The value that is rendered in the router tree.
treeSegment : [
key ,
Array . isArray ( value ) ? value . join ( '/' ) : value ,
type ,
] ,
type : type ,
}
}
2022-09-19 20:05:28 +02:00
2022-09-21 02:00:19 +02:00
const createFlightRouterStateFromLoaderTree = ( [
2022-09-09 14:44:12 +02:00
segment ,
parallelRoutes ,
2022-09-21 02:00:19 +02:00
] : LoaderTree ) : FlightRouterState = > {
const dynamicParam = getDynamicParamFromSegment ( segment )
const segmentTree : FlightRouterState = [
dynamicParam ? dynamicParam.treeSegment : segment ,
{ } ,
]
if ( parallelRoutes ) {
segmentTree [ 1 ] = Object . keys ( parallelRoutes ) . reduce (
( existingValue , currentValue ) = > {
existingValue [ currentValue ] = createFlightRouterStateFromLoaderTree (
parallelRoutes [ currentValue ]
)
return existingValue
} ,
{ } as FlightRouterState [ 1 ]
2022-09-09 14:44:12 +02:00
)
2022-09-21 02:00:19 +02:00
}
2022-09-19 20:05:28 +02:00
2022-09-21 02:00:19 +02:00
return segmentTree
2022-09-19 20:05:28 +02:00
}
2022-09-21 02:00:19 +02:00
let defaultRevalidate : false | undefined | number = false
2022-10-12 15:41:19 +02:00
// Collect all server CSS imports used by this specific entry (or entries, for parallel routes).
// Not that we can't rely on the CSS manifest because it tracks CSS imports per module,
// which can be used by multiple entries and cannot be tree-shaked in the module graph.
// More info: https://github.com/vercel/next.js/issues/41018
const serverCSSForEntries = getServerCSSForEntries (
serverCSSManifest ! ,
ComponentMod . pages
)
2022-07-26 12:41:51 +02:00
/ * *
2022-09-21 02:00:19 +02:00
* Use the provided loader tree to create the React Component tree .
2022-07-26 12:41:51 +02:00
* /
2022-09-21 02:00:19 +02:00
const createComponentTree = async ( {
createSegmentPath ,
loaderTree : [
segment ,
parallelRoutes ,
2022-09-22 13:08:45 +02:00
{
layoutOrPagePath ,
layout ,
template ,
error ,
loading ,
page ,
2022-09-27 15:50:21 +02:00
'not-found' : notFound ,
2022-09-22 13:08:45 +02:00
} ,
2022-09-21 02:00:19 +02:00
] ,
parentParams ,
firstItem ,
rootLayoutIncluded ,
} : {
createSegmentPath : CreateSegmentPath
loaderTree : LoaderTree
parentParams : { [ key : string ] : any }
rootLayoutIncluded? : boolean
firstItem? : boolean
} ) : Promise < { Component : React.ComponentType } > = > {
// TODO-APP: enable stylesheet per layout/page
const stylesheets : string [ ] = layoutOrPagePath
? getCssInlinedLinkTags (
serverComponentManifest ,
serverCSSManifest ! ,
2022-10-12 15:41:19 +02:00
layoutOrPagePath ,
serverCSSForEntries
2022-09-21 02:00:19 +02:00
)
: [ ]
2022-10-12 15:41:19 +02:00
2022-10-06 21:24:42 +02:00
const preloadedFontFiles = getPreloadedFontFilesInlineLinkTags (
serverComponentManifest ,
serverCSSManifest ! ,
fontLoaderManifest ,
2022-10-12 15:41:19 +02:00
serverCSSForEntries ,
2022-10-06 21:24:42 +02:00
layoutOrPagePath
)
2022-09-21 02:00:19 +02:00
const Template = template
? await interopDefault ( template ( ) )
: React . Fragment
2022-09-22 13:08:45 +02:00
const NotFound = notFound ? await interopDefault ( notFound ( ) ) : undefined
2022-09-21 02:00:19 +02:00
const ErrorComponent = error ? await interopDefault ( error ( ) ) : undefined
const Loading = loading ? await interopDefault ( loading ( ) ) : undefined
const isLayout = typeof layout !== 'undefined'
const isPage = typeof page !== 'undefined'
const layoutOrPageMod = isLayout
? await layout ( )
: isPage
? await page ( )
: undefined
if ( layoutOrPageMod ? . config ) {
defaultRevalidate = layoutOrPageMod . config . revalidate
2022-09-23 04:44:30 +02:00
if ( isStaticGeneration && defaultRevalidate === 0 ) {
const { DynamicServerError } =
ComponentMod . serverHooks as typeof import ( '../client/components/hooks-server-context' )
throw new DynamicServerError ( ` revalidate: 0 configured ${ segment } ` )
}
2022-09-21 02:00:19 +02:00
}
/ * *
* Checks if the current segment is a root layout .
* /
const rootLayoutAtThisLevel = isLayout && ! rootLayoutIncluded
/ * *
* Checks if the current segment or any level above it has a root layout .
* /
const rootLayoutIncludedAtThisLevelOrAbove =
rootLayoutIncluded || rootLayoutAtThisLevel
// TODO-APP: move these errors to the loader instead?
// we will also need a migration doc here to link to
if ( typeof layoutOrPageMod ? . getServerSideProps === 'function' ) {
throw new Error (
` getServerSideProps is not supported in app/, detected in ${ segment } `
)
}
Implement new client-side router (#37551)
## Client-side router for `app` directory
This PR implements the new router that leverages React 18 concurrent features like Suspense and startTransition.
It also integrates with React Server Components and builds on top of it to allow server-centric routing that only renders the part of the page that has to change.
It's one of the pieces of the implementation of https://nextjs.org/blog/layouts-rfc.
## Details
I'm going to document the differences with the current router here (will be reworked for the upgrade guide)
### Client-side cache
In the current router we have an in-memory cache for getStaticProps data so that if you prefetch and then navigate to a route that has been prefetched it'll be near-instant. For getServerSideProps the behavior is different, any navigation to a page with getServerSideProps fetches the data again.
In the new model the cache is a fundamental piece, it's more granular than at the page level and is set up to ensure consistency across concurrent renders. It can also be invalidated at any level.
#### Push/Replace (also applies to next/link)
The new router still has a `router.push` / `router.replace` method.
There are a few differences in how it works though:
- It only takes `href` as an argument, historically you had to provide `href` (the page path) and `as` (the actual url path) to do dynamic routing. In later versions of Next.js this is no longer required and in the majority of cases `as` was no longer needed. In the new router there's no way to reason about `href` vs `as` because there is no notion of "pages" in the browser.
- Both methods now use `startTransition`, you can wrap these in your own `startTransition` to get `isPending`
- The push/replace support concurrent rendering. When a render is bailed by clicking a different link to navigate to a completely different page that still works and doesn't cause race conditions.
- Support for optimistic loading states when navigating
##### Hard/Soft push/replace
Because of the client-side cache being reworked this now allows us to cover two cases: hard push and soft push.
The main difference between the two is if the cache is reused while navigating. The default for `next/link` is a `hard` push which means that the part of the cache affected by the navigation will be invalidated, e.g. if you already navigated to `/dashboard` and you `router.push('/dashboard')` again it'll get the latest version. This is similar to the existing `getServerSideProps` handling.
In case of a soft push (API to be defined but for testing added `router.softPush('/')`) it'll reuse the existing cache and not invalidate parts that are already filled in. In practice this means it's more like the `getStaticProps` client-side navigation because it does not fetch on navigation except if a part of the page is missing.
#### Back/Forward navigation
Back and Forward navigation ([popstate](https://developer.mozilla.org/en-US/docs/Web/API/Window/popstate_event)) are always handled as a soft navigation, meaning that the cache is reused, this ensures back/forward navigation is near-instant when it's in the client-side cache. This will also allow back/forward navigation to be a high priority update instead of a transition as it is based on user interaction. Note: in this PR it still uses `startTransition` as there's no way to handle the high priority update suspending which happens in case of missing data in the cache. We're working with the React team on a solution for this particular case.
### Layouts
Note: this section assumes you've read [The layouts RFC](https://nextjs.org/blog/layouts-rfc) and [React Server Components RFC](https://reactjs.org/blog/2020/12/21/data-fetching-with-react-server-components.html)
React Server Components rendering leverages the Flight streaming mechanism in React 18, this allows sending a serializable representation of the rendered React tree on the server to the browser, the client-side React can use this serialized representation to render components client-side without the JavaScript being sent to the browser. This is one of the building blocks of Server Components. This allows a bunch of interesting features but for now I'll keep it to how it affects layouts.
When you have a `app/dashboard/layout.js` and `app/dashboard/page.js` the page will render as children of the layout, when you add another page like `app/dashboard/integrations/page.js` that page falls under the dashboard layout as well. When client-side navigating the new router automatically figures out if the page you're navigating to can be a smaller render than the whole page, in this case `app/dashboard/page.js` and `app/dashboard/integrations/page.js` share the `app/dashboard/layout.js` so instead of rendering the whole page we render below the layout component, this means the layout itself does not get re-rendered, the layout's `getServerSideProps` would not be called, and the Flight response would only hold the result of `app/dashboard/integrations/page.js`, effectively giving you the smallest patch for the UI.
---
Note: the commits in this PR were mostly work in progress to ensure it wasn't lost along the way. The implementation was reworked a bunch of times to where it is now.
Co-authored-by: Jiachi Liu <4800338+huozhi@users.noreply.github.com>
Co-authored-by: JJ Kasper <22380829+ijjk@users.noreply.github.com>
2022-07-06 23:16:47 +02:00
2022-09-21 02:00:19 +02:00
if ( typeof layoutOrPageMod ? . getStaticProps === 'function' ) {
throw new Error (
` getStaticProps is not supported in app/, detected in ${ segment } `
)
}
2022-09-19 20:05:28 +02:00
2022-09-21 02:00:19 +02:00
/ * *
* The React Component to render .
* /
const Component = layoutOrPageMod
? interopDefault ( layoutOrPageMod )
: undefined
Implement new client-side router (#37551)
## Client-side router for `app` directory
This PR implements the new router that leverages React 18 concurrent features like Suspense and startTransition.
It also integrates with React Server Components and builds on top of it to allow server-centric routing that only renders the part of the page that has to change.
It's one of the pieces of the implementation of https://nextjs.org/blog/layouts-rfc.
## Details
I'm going to document the differences with the current router here (will be reworked for the upgrade guide)
### Client-side cache
In the current router we have an in-memory cache for getStaticProps data so that if you prefetch and then navigate to a route that has been prefetched it'll be near-instant. For getServerSideProps the behavior is different, any navigation to a page with getServerSideProps fetches the data again.
In the new model the cache is a fundamental piece, it's more granular than at the page level and is set up to ensure consistency across concurrent renders. It can also be invalidated at any level.
#### Push/Replace (also applies to next/link)
The new router still has a `router.push` / `router.replace` method.
There are a few differences in how it works though:
- It only takes `href` as an argument, historically you had to provide `href` (the page path) and `as` (the actual url path) to do dynamic routing. In later versions of Next.js this is no longer required and in the majority of cases `as` was no longer needed. In the new router there's no way to reason about `href` vs `as` because there is no notion of "pages" in the browser.
- Both methods now use `startTransition`, you can wrap these in your own `startTransition` to get `isPending`
- The push/replace support concurrent rendering. When a render is bailed by clicking a different link to navigate to a completely different page that still works and doesn't cause race conditions.
- Support for optimistic loading states when navigating
##### Hard/Soft push/replace
Because of the client-side cache being reworked this now allows us to cover two cases: hard push and soft push.
The main difference between the two is if the cache is reused while navigating. The default for `next/link` is a `hard` push which means that the part of the cache affected by the navigation will be invalidated, e.g. if you already navigated to `/dashboard` and you `router.push('/dashboard')` again it'll get the latest version. This is similar to the existing `getServerSideProps` handling.
In case of a soft push (API to be defined but for testing added `router.softPush('/')`) it'll reuse the existing cache and not invalidate parts that are already filled in. In practice this means it's more like the `getStaticProps` client-side navigation because it does not fetch on navigation except if a part of the page is missing.
#### Back/Forward navigation
Back and Forward navigation ([popstate](https://developer.mozilla.org/en-US/docs/Web/API/Window/popstate_event)) are always handled as a soft navigation, meaning that the cache is reused, this ensures back/forward navigation is near-instant when it's in the client-side cache. This will also allow back/forward navigation to be a high priority update instead of a transition as it is based on user interaction. Note: in this PR it still uses `startTransition` as there's no way to handle the high priority update suspending which happens in case of missing data in the cache. We're working with the React team on a solution for this particular case.
### Layouts
Note: this section assumes you've read [The layouts RFC](https://nextjs.org/blog/layouts-rfc) and [React Server Components RFC](https://reactjs.org/blog/2020/12/21/data-fetching-with-react-server-components.html)
React Server Components rendering leverages the Flight streaming mechanism in React 18, this allows sending a serializable representation of the rendered React tree on the server to the browser, the client-side React can use this serialized representation to render components client-side without the JavaScript being sent to the browser. This is one of the building blocks of Server Components. This allows a bunch of interesting features but for now I'll keep it to how it affects layouts.
When you have a `app/dashboard/layout.js` and `app/dashboard/page.js` the page will render as children of the layout, when you add another page like `app/dashboard/integrations/page.js` that page falls under the dashboard layout as well. When client-side navigating the new router automatically figures out if the page you're navigating to can be a smaller render than the whole page, in this case `app/dashboard/page.js` and `app/dashboard/integrations/page.js` share the `app/dashboard/layout.js` so instead of rendering the whole page we render below the layout component, this means the layout itself does not get re-rendered, the layout's `getServerSideProps` would not be called, and the Flight response would only hold the result of `app/dashboard/integrations/page.js`, effectively giving you the smallest patch for the UI.
---
Note: the commits in this PR were mostly work in progress to ensure it wasn't lost along the way. The implementation was reworked a bunch of times to where it is now.
Co-authored-by: Jiachi Liu <4800338+huozhi@users.noreply.github.com>
Co-authored-by: JJ Kasper <22380829+ijjk@users.noreply.github.com>
2022-07-06 23:16:47 +02:00
2022-09-21 02:00:19 +02:00
// Handle dynamic segment params.
const segmentParam = getDynamicParamFromSegment ( segment )
/ * *
* Create object holding the parent params and current params
* /
const currentParams =
// Handle null case where dynamic param is optional
segmentParam && segmentParam . value !== null
? {
. . . parentParams ,
[ segmentParam . param ] : segmentParam . value ,
}
: // Pass through parent params to children
parentParams
// Resolve the segment param
const actualSegment = segmentParam ? segmentParam.treeSegment : segment
// This happens outside of rendering in order to eagerly kick off data fetching for layouts / the page further down
const parallelRouteMap = await Promise . all (
Object . keys ( parallelRoutes ) . map (
async ( parallelRouteKey ) : Promise < [ string , React . ReactNode ] > = > {
const currentSegmentPath : FlightSegmentPath = firstItem
? [ parallelRouteKey ]
: [ actualSegment , parallelRouteKey ]
const childSegment = parallelRoutes [ parallelRouteKey ] [ 0 ]
const childSegmentParam = getDynamicParamFromSegment ( childSegment )
if ( isPrefetch && Loading ) {
const childProp : ChildProp = {
// Null indicates the tree is not fully rendered
current : null ,
segment : childSegmentParam
? childSegmentParam . treeSegment
: childSegment ,
}
// This is turned back into an object below.
return [
parallelRouteKey ,
< LayoutRouter
parallelRouterKey = { parallelRouteKey }
segmentPath = { createSegmentPath ( currentSegmentPath ) }
loading = { Loading ? < Loading / > : undefined }
2022-09-27 10:46:30 +02:00
hasLoading = { Boolean ( Loading ) }
2022-09-21 02:00:19 +02:00
error = { ErrorComponent }
template = {
< Template >
< RenderFromTemplateContext / >
< / Template >
}
2022-09-22 13:08:45 +02:00
notFound = { NotFound ? < NotFound / > : undefined }
2022-09-21 02:00:19 +02:00
childProp = { childProp }
rootLayoutIncluded = { rootLayoutIncludedAtThisLevelOrAbove }
/ > ,
]
}
// Create the child component
const { Component : ChildComponent } = await createComponentTree ( {
createSegmentPath : ( child ) = > {
return createSegmentPath ( [ . . . currentSegmentPath , . . . child ] )
} ,
loaderTree : parallelRoutes [ parallelRouteKey ] ,
parentParams : currentParams ,
rootLayoutIncluded : rootLayoutIncludedAtThisLevelOrAbove ,
} )
Implement new client-side router (#37551)
## Client-side router for `app` directory
This PR implements the new router that leverages React 18 concurrent features like Suspense and startTransition.
It also integrates with React Server Components and builds on top of it to allow server-centric routing that only renders the part of the page that has to change.
It's one of the pieces of the implementation of https://nextjs.org/blog/layouts-rfc.
## Details
I'm going to document the differences with the current router here (will be reworked for the upgrade guide)
### Client-side cache
In the current router we have an in-memory cache for getStaticProps data so that if you prefetch and then navigate to a route that has been prefetched it'll be near-instant. For getServerSideProps the behavior is different, any navigation to a page with getServerSideProps fetches the data again.
In the new model the cache is a fundamental piece, it's more granular than at the page level and is set up to ensure consistency across concurrent renders. It can also be invalidated at any level.
#### Push/Replace (also applies to next/link)
The new router still has a `router.push` / `router.replace` method.
There are a few differences in how it works though:
- It only takes `href` as an argument, historically you had to provide `href` (the page path) and `as` (the actual url path) to do dynamic routing. In later versions of Next.js this is no longer required and in the majority of cases `as` was no longer needed. In the new router there's no way to reason about `href` vs `as` because there is no notion of "pages" in the browser.
- Both methods now use `startTransition`, you can wrap these in your own `startTransition` to get `isPending`
- The push/replace support concurrent rendering. When a render is bailed by clicking a different link to navigate to a completely different page that still works and doesn't cause race conditions.
- Support for optimistic loading states when navigating
##### Hard/Soft push/replace
Because of the client-side cache being reworked this now allows us to cover two cases: hard push and soft push.
The main difference between the two is if the cache is reused while navigating. The default for `next/link` is a `hard` push which means that the part of the cache affected by the navigation will be invalidated, e.g. if you already navigated to `/dashboard` and you `router.push('/dashboard')` again it'll get the latest version. This is similar to the existing `getServerSideProps` handling.
In case of a soft push (API to be defined but for testing added `router.softPush('/')`) it'll reuse the existing cache and not invalidate parts that are already filled in. In practice this means it's more like the `getStaticProps` client-side navigation because it does not fetch on navigation except if a part of the page is missing.
#### Back/Forward navigation
Back and Forward navigation ([popstate](https://developer.mozilla.org/en-US/docs/Web/API/Window/popstate_event)) are always handled as a soft navigation, meaning that the cache is reused, this ensures back/forward navigation is near-instant when it's in the client-side cache. This will also allow back/forward navigation to be a high priority update instead of a transition as it is based on user interaction. Note: in this PR it still uses `startTransition` as there's no way to handle the high priority update suspending which happens in case of missing data in the cache. We're working with the React team on a solution for this particular case.
### Layouts
Note: this section assumes you've read [The layouts RFC](https://nextjs.org/blog/layouts-rfc) and [React Server Components RFC](https://reactjs.org/blog/2020/12/21/data-fetching-with-react-server-components.html)
React Server Components rendering leverages the Flight streaming mechanism in React 18, this allows sending a serializable representation of the rendered React tree on the server to the browser, the client-side React can use this serialized representation to render components client-side without the JavaScript being sent to the browser. This is one of the building blocks of Server Components. This allows a bunch of interesting features but for now I'll keep it to how it affects layouts.
When you have a `app/dashboard/layout.js` and `app/dashboard/page.js` the page will render as children of the layout, when you add another page like `app/dashboard/integrations/page.js` that page falls under the dashboard layout as well. When client-side navigating the new router automatically figures out if the page you're navigating to can be a smaller render than the whole page, in this case `app/dashboard/page.js` and `app/dashboard/integrations/page.js` share the `app/dashboard/layout.js` so instead of rendering the whole page we render below the layout component, this means the layout itself does not get re-rendered, the layout's `getServerSideProps` would not be called, and the Flight response would only hold the result of `app/dashboard/integrations/page.js`, effectively giving you the smallest patch for the UI.
---
Note: the commits in this PR were mostly work in progress to ensure it wasn't lost along the way. The implementation was reworked a bunch of times to where it is now.
Co-authored-by: Jiachi Liu <4800338+huozhi@users.noreply.github.com>
Co-authored-by: JJ Kasper <22380829+ijjk@users.noreply.github.com>
2022-07-06 23:16:47 +02:00
2022-09-06 19:29:09 +02:00
const childProp : ChildProp = {
2022-09-21 02:00:19 +02:00
current : < ChildComponent / > ,
2022-09-06 19:29:09 +02:00
segment : childSegmentParam
? childSegmentParam . treeSegment
: childSegment ,
}
2022-09-21 02:00:19 +02:00
const segmentPath = createSegmentPath ( currentSegmentPath )
2022-09-06 19:29:09 +02:00
// This is turned back into an object below.
return [
parallelRouteKey ,
< LayoutRouter
parallelRouterKey = { parallelRouteKey }
2022-09-21 02:00:19 +02:00
segmentPath = { segmentPath }
2022-09-09 14:44:12 +02:00
error = { ErrorComponent }
2022-09-21 02:00:19 +02:00
loading = { Loading ? < Loading / > : undefined }
2022-09-27 10:46:30 +02:00
// TODO-APP: Add test for loading returning `undefined`. This currently can't be tested as the `webdriver()` tab will wait for the full page to load before returning.
hasLoading = { Boolean ( Loading ) }
2022-09-09 14:44:12 +02:00
template = {
< Template >
< RenderFromTemplateContext / >
< / Template >
}
2022-09-22 13:08:45 +02:00
notFound = { NotFound ? < NotFound / > : undefined }
2022-09-06 19:29:09 +02:00
childProp = { childProp }
rootLayoutIncluded = { rootLayoutIncludedAtThisLevelOrAbove }
/ > ,
]
}
2022-09-21 02:00:19 +02:00
)
)
2022-09-06 19:29:09 +02:00
2022-09-21 02:00:19 +02:00
// Convert the parallel route map into an object after all promises have been resolved.
const parallelRouteComponents = parallelRouteMap . reduce (
( list , [ parallelRouteKey , Comp ] ) = > {
list [ parallelRouteKey ] = Comp
return list
} ,
{ } as { [ key : string ] : React . ReactNode }
2022-07-21 11:11:01 +02:00
)
Implement new client-side router (#37551)
## Client-side router for `app` directory
This PR implements the new router that leverages React 18 concurrent features like Suspense and startTransition.
It also integrates with React Server Components and builds on top of it to allow server-centric routing that only renders the part of the page that has to change.
It's one of the pieces of the implementation of https://nextjs.org/blog/layouts-rfc.
## Details
I'm going to document the differences with the current router here (will be reworked for the upgrade guide)
### Client-side cache
In the current router we have an in-memory cache for getStaticProps data so that if you prefetch and then navigate to a route that has been prefetched it'll be near-instant. For getServerSideProps the behavior is different, any navigation to a page with getServerSideProps fetches the data again.
In the new model the cache is a fundamental piece, it's more granular than at the page level and is set up to ensure consistency across concurrent renders. It can also be invalidated at any level.
#### Push/Replace (also applies to next/link)
The new router still has a `router.push` / `router.replace` method.
There are a few differences in how it works though:
- It only takes `href` as an argument, historically you had to provide `href` (the page path) and `as` (the actual url path) to do dynamic routing. In later versions of Next.js this is no longer required and in the majority of cases `as` was no longer needed. In the new router there's no way to reason about `href` vs `as` because there is no notion of "pages" in the browser.
- Both methods now use `startTransition`, you can wrap these in your own `startTransition` to get `isPending`
- The push/replace support concurrent rendering. When a render is bailed by clicking a different link to navigate to a completely different page that still works and doesn't cause race conditions.
- Support for optimistic loading states when navigating
##### Hard/Soft push/replace
Because of the client-side cache being reworked this now allows us to cover two cases: hard push and soft push.
The main difference between the two is if the cache is reused while navigating. The default for `next/link` is a `hard` push which means that the part of the cache affected by the navigation will be invalidated, e.g. if you already navigated to `/dashboard` and you `router.push('/dashboard')` again it'll get the latest version. This is similar to the existing `getServerSideProps` handling.
In case of a soft push (API to be defined but for testing added `router.softPush('/')`) it'll reuse the existing cache and not invalidate parts that are already filled in. In practice this means it's more like the `getStaticProps` client-side navigation because it does not fetch on navigation except if a part of the page is missing.
#### Back/Forward navigation
Back and Forward navigation ([popstate](https://developer.mozilla.org/en-US/docs/Web/API/Window/popstate_event)) are always handled as a soft navigation, meaning that the cache is reused, this ensures back/forward navigation is near-instant when it's in the client-side cache. This will also allow back/forward navigation to be a high priority update instead of a transition as it is based on user interaction. Note: in this PR it still uses `startTransition` as there's no way to handle the high priority update suspending which happens in case of missing data in the cache. We're working with the React team on a solution for this particular case.
### Layouts
Note: this section assumes you've read [The layouts RFC](https://nextjs.org/blog/layouts-rfc) and [React Server Components RFC](https://reactjs.org/blog/2020/12/21/data-fetching-with-react-server-components.html)
React Server Components rendering leverages the Flight streaming mechanism in React 18, this allows sending a serializable representation of the rendered React tree on the server to the browser, the client-side React can use this serialized representation to render components client-side without the JavaScript being sent to the browser. This is one of the building blocks of Server Components. This allows a bunch of interesting features but for now I'll keep it to how it affects layouts.
When you have a `app/dashboard/layout.js` and `app/dashboard/page.js` the page will render as children of the layout, when you add another page like `app/dashboard/integrations/page.js` that page falls under the dashboard layout as well. When client-side navigating the new router automatically figures out if the page you're navigating to can be a smaller render than the whole page, in this case `app/dashboard/page.js` and `app/dashboard/integrations/page.js` share the `app/dashboard/layout.js` so instead of rendering the whole page we render below the layout component, this means the layout itself does not get re-rendered, the layout's `getServerSideProps` would not be called, and the Flight response would only hold the result of `app/dashboard/integrations/page.js`, effectively giving you the smallest patch for the UI.
---
Note: the commits in this PR were mostly work in progress to ensure it wasn't lost along the way. The implementation was reworked a bunch of times to where it is now.
Co-authored-by: Jiachi Liu <4800338+huozhi@users.noreply.github.com>
Co-authored-by: JJ Kasper <22380829+ijjk@users.noreply.github.com>
2022-07-06 23:16:47 +02:00
2022-09-21 02:00:19 +02:00
// When the segment does not have a layout or page we still have to add the layout router to ensure the path holds the loading component
if ( ! Component ) {
return {
Component : ( ) = > < > { parallelRouteComponents . children } < / > ,
}
}
2022-05-03 12:37:23 +02:00
Implement new client-side router (#37551)
## Client-side router for `app` directory
This PR implements the new router that leverages React 18 concurrent features like Suspense and startTransition.
It also integrates with React Server Components and builds on top of it to allow server-centric routing that only renders the part of the page that has to change.
It's one of the pieces of the implementation of https://nextjs.org/blog/layouts-rfc.
## Details
I'm going to document the differences with the current router here (will be reworked for the upgrade guide)
### Client-side cache
In the current router we have an in-memory cache for getStaticProps data so that if you prefetch and then navigate to a route that has been prefetched it'll be near-instant. For getServerSideProps the behavior is different, any navigation to a page with getServerSideProps fetches the data again.
In the new model the cache is a fundamental piece, it's more granular than at the page level and is set up to ensure consistency across concurrent renders. It can also be invalidated at any level.
#### Push/Replace (also applies to next/link)
The new router still has a `router.push` / `router.replace` method.
There are a few differences in how it works though:
- It only takes `href` as an argument, historically you had to provide `href` (the page path) and `as` (the actual url path) to do dynamic routing. In later versions of Next.js this is no longer required and in the majority of cases `as` was no longer needed. In the new router there's no way to reason about `href` vs `as` because there is no notion of "pages" in the browser.
- Both methods now use `startTransition`, you can wrap these in your own `startTransition` to get `isPending`
- The push/replace support concurrent rendering. When a render is bailed by clicking a different link to navigate to a completely different page that still works and doesn't cause race conditions.
- Support for optimistic loading states when navigating
##### Hard/Soft push/replace
Because of the client-side cache being reworked this now allows us to cover two cases: hard push and soft push.
The main difference between the two is if the cache is reused while navigating. The default for `next/link` is a `hard` push which means that the part of the cache affected by the navigation will be invalidated, e.g. if you already navigated to `/dashboard` and you `router.push('/dashboard')` again it'll get the latest version. This is similar to the existing `getServerSideProps` handling.
In case of a soft push (API to be defined but for testing added `router.softPush('/')`) it'll reuse the existing cache and not invalidate parts that are already filled in. In practice this means it's more like the `getStaticProps` client-side navigation because it does not fetch on navigation except if a part of the page is missing.
#### Back/Forward navigation
Back and Forward navigation ([popstate](https://developer.mozilla.org/en-US/docs/Web/API/Window/popstate_event)) are always handled as a soft navigation, meaning that the cache is reused, this ensures back/forward navigation is near-instant when it's in the client-side cache. This will also allow back/forward navigation to be a high priority update instead of a transition as it is based on user interaction. Note: in this PR it still uses `startTransition` as there's no way to handle the high priority update suspending which happens in case of missing data in the cache. We're working with the React team on a solution for this particular case.
### Layouts
Note: this section assumes you've read [The layouts RFC](https://nextjs.org/blog/layouts-rfc) and [React Server Components RFC](https://reactjs.org/blog/2020/12/21/data-fetching-with-react-server-components.html)
React Server Components rendering leverages the Flight streaming mechanism in React 18, this allows sending a serializable representation of the rendered React tree on the server to the browser, the client-side React can use this serialized representation to render components client-side without the JavaScript being sent to the browser. This is one of the building blocks of Server Components. This allows a bunch of interesting features but for now I'll keep it to how it affects layouts.
When you have a `app/dashboard/layout.js` and `app/dashboard/page.js` the page will render as children of the layout, when you add another page like `app/dashboard/integrations/page.js` that page falls under the dashboard layout as well. When client-side navigating the new router automatically figures out if the page you're navigating to can be a smaller render than the whole page, in this case `app/dashboard/page.js` and `app/dashboard/integrations/page.js` share the `app/dashboard/layout.js` so instead of rendering the whole page we render below the layout component, this means the layout itself does not get re-rendered, the layout's `getServerSideProps` would not be called, and the Flight response would only hold the result of `app/dashboard/integrations/page.js`, effectively giving you the smallest patch for the UI.
---
Note: the commits in this PR were mostly work in progress to ensure it wasn't lost along the way. The implementation was reworked a bunch of times to where it is now.
Co-authored-by: Jiachi Liu <4800338+huozhi@users.noreply.github.com>
Co-authored-by: JJ Kasper <22380829+ijjk@users.noreply.github.com>
2022-07-06 23:16:47 +02:00
return {
2022-09-21 02:00:19 +02:00
Component : ( ) = > {
let props = { }
return (
< >
2022-10-06 21:24:42 +02:00
{ preloadedFontFiles . map ( ( fontFile ) = > {
const ext = /\.(woff|woff2|eot|ttf|otf)$/ . exec ( fontFile ) ! [ 1 ]
return (
< link
key = { fontFile }
rel = "preload"
href = { ` /_next/ ${ fontFile } ` }
as = "font"
type = { ` font/ ${ ext } ` }
crossOrigin = "anonymous"
/ >
)
} ) }
2022-09-21 02:00:19 +02:00
{ stylesheets
? stylesheets . map ( ( href ) = > (
< link
rel = "stylesheet"
2022-10-13 18:28:29 +02:00
// Add extra cache busting (DEV only) for https://github.com/vercel/next.js/issues/5860
// See also https://bugs.webkit.org/show_bug.cgi?id=187726
href = { ` /_next/ ${ href } ${ dev ? ` ?ts= ${ Date . now ( ) } ` : '' } ` }
2022-09-21 02:00:19 +02:00
// `Precedence` is an opt-in signal for React to handle
// resource loading and deduplication, etc:
// https://github.com/facebook/react/pull/25060
// @ts-ignore
precedence = "high"
key = { href }
/ >
) )
: null }
< Component
{ . . . props }
{ . . . parallelRouteComponents }
// TODO-APP: params and query have to be blocked parallel route names. Might have to add a reserved name list.
// Params are always the current params that apply to the layout
// If you have a `/dashboard/[team]/layout.js` it will provide `team` as a param but not anything further down.
params = { currentParams }
// Query is only provided to page
{ . . . ( isPage ? { searchParams : query } : { } ) }
/ >
< / >
)
} ,
Implement new client-side router (#37551)
## Client-side router for `app` directory
This PR implements the new router that leverages React 18 concurrent features like Suspense and startTransition.
It also integrates with React Server Components and builds on top of it to allow server-centric routing that only renders the part of the page that has to change.
It's one of the pieces of the implementation of https://nextjs.org/blog/layouts-rfc.
## Details
I'm going to document the differences with the current router here (will be reworked for the upgrade guide)
### Client-side cache
In the current router we have an in-memory cache for getStaticProps data so that if you prefetch and then navigate to a route that has been prefetched it'll be near-instant. For getServerSideProps the behavior is different, any navigation to a page with getServerSideProps fetches the data again.
In the new model the cache is a fundamental piece, it's more granular than at the page level and is set up to ensure consistency across concurrent renders. It can also be invalidated at any level.
#### Push/Replace (also applies to next/link)
The new router still has a `router.push` / `router.replace` method.
There are a few differences in how it works though:
- It only takes `href` as an argument, historically you had to provide `href` (the page path) and `as` (the actual url path) to do dynamic routing. In later versions of Next.js this is no longer required and in the majority of cases `as` was no longer needed. In the new router there's no way to reason about `href` vs `as` because there is no notion of "pages" in the browser.
- Both methods now use `startTransition`, you can wrap these in your own `startTransition` to get `isPending`
- The push/replace support concurrent rendering. When a render is bailed by clicking a different link to navigate to a completely different page that still works and doesn't cause race conditions.
- Support for optimistic loading states when navigating
##### Hard/Soft push/replace
Because of the client-side cache being reworked this now allows us to cover two cases: hard push and soft push.
The main difference between the two is if the cache is reused while navigating. The default for `next/link` is a `hard` push which means that the part of the cache affected by the navigation will be invalidated, e.g. if you already navigated to `/dashboard` and you `router.push('/dashboard')` again it'll get the latest version. This is similar to the existing `getServerSideProps` handling.
In case of a soft push (API to be defined but for testing added `router.softPush('/')`) it'll reuse the existing cache and not invalidate parts that are already filled in. In practice this means it's more like the `getStaticProps` client-side navigation because it does not fetch on navigation except if a part of the page is missing.
#### Back/Forward navigation
Back and Forward navigation ([popstate](https://developer.mozilla.org/en-US/docs/Web/API/Window/popstate_event)) are always handled as a soft navigation, meaning that the cache is reused, this ensures back/forward navigation is near-instant when it's in the client-side cache. This will also allow back/forward navigation to be a high priority update instead of a transition as it is based on user interaction. Note: in this PR it still uses `startTransition` as there's no way to handle the high priority update suspending which happens in case of missing data in the cache. We're working with the React team on a solution for this particular case.
### Layouts
Note: this section assumes you've read [The layouts RFC](https://nextjs.org/blog/layouts-rfc) and [React Server Components RFC](https://reactjs.org/blog/2020/12/21/data-fetching-with-react-server-components.html)
React Server Components rendering leverages the Flight streaming mechanism in React 18, this allows sending a serializable representation of the rendered React tree on the server to the browser, the client-side React can use this serialized representation to render components client-side without the JavaScript being sent to the browser. This is one of the building blocks of Server Components. This allows a bunch of interesting features but for now I'll keep it to how it affects layouts.
When you have a `app/dashboard/layout.js` and `app/dashboard/page.js` the page will render as children of the layout, when you add another page like `app/dashboard/integrations/page.js` that page falls under the dashboard layout as well. When client-side navigating the new router automatically figures out if the page you're navigating to can be a smaller render than the whole page, in this case `app/dashboard/page.js` and `app/dashboard/integrations/page.js` share the `app/dashboard/layout.js` so instead of rendering the whole page we render below the layout component, this means the layout itself does not get re-rendered, the layout's `getServerSideProps` would not be called, and the Flight response would only hold the result of `app/dashboard/integrations/page.js`, effectively giving you the smallest patch for the UI.
---
Note: the commits in this PR were mostly work in progress to ensure it wasn't lost along the way. The implementation was reworked a bunch of times to where it is now.
Co-authored-by: Jiachi Liu <4800338+huozhi@users.noreply.github.com>
Co-authored-by: JJ Kasper <22380829+ijjk@users.noreply.github.com>
2022-07-06 23:16:47 +02:00
}
}
2022-10-06 21:43:23 +02:00
const streamToBufferedResult = async (
renderResult : RenderResult
) : Promise < string > = > {
const renderChunks : Buffer [ ] = [ ]
const writable = new Writable ( {
write ( chunk , _encoding , callback ) {
renderChunks . push ( chunk )
callback ( )
} ,
} )
await renderResult . pipe ( writable )
return Buffer . concat ( renderChunks ) . toString ( )
}
Implement new client-side router (#37551)
## Client-side router for `app` directory
This PR implements the new router that leverages React 18 concurrent features like Suspense and startTransition.
It also integrates with React Server Components and builds on top of it to allow server-centric routing that only renders the part of the page that has to change.
It's one of the pieces of the implementation of https://nextjs.org/blog/layouts-rfc.
## Details
I'm going to document the differences with the current router here (will be reworked for the upgrade guide)
### Client-side cache
In the current router we have an in-memory cache for getStaticProps data so that if you prefetch and then navigate to a route that has been prefetched it'll be near-instant. For getServerSideProps the behavior is different, any navigation to a page with getServerSideProps fetches the data again.
In the new model the cache is a fundamental piece, it's more granular than at the page level and is set up to ensure consistency across concurrent renders. It can also be invalidated at any level.
#### Push/Replace (also applies to next/link)
The new router still has a `router.push` / `router.replace` method.
There are a few differences in how it works though:
- It only takes `href` as an argument, historically you had to provide `href` (the page path) and `as` (the actual url path) to do dynamic routing. In later versions of Next.js this is no longer required and in the majority of cases `as` was no longer needed. In the new router there's no way to reason about `href` vs `as` because there is no notion of "pages" in the browser.
- Both methods now use `startTransition`, you can wrap these in your own `startTransition` to get `isPending`
- The push/replace support concurrent rendering. When a render is bailed by clicking a different link to navigate to a completely different page that still works and doesn't cause race conditions.
- Support for optimistic loading states when navigating
##### Hard/Soft push/replace
Because of the client-side cache being reworked this now allows us to cover two cases: hard push and soft push.
The main difference between the two is if the cache is reused while navigating. The default for `next/link` is a `hard` push which means that the part of the cache affected by the navigation will be invalidated, e.g. if you already navigated to `/dashboard` and you `router.push('/dashboard')` again it'll get the latest version. This is similar to the existing `getServerSideProps` handling.
In case of a soft push (API to be defined but for testing added `router.softPush('/')`) it'll reuse the existing cache and not invalidate parts that are already filled in. In practice this means it's more like the `getStaticProps` client-side navigation because it does not fetch on navigation except if a part of the page is missing.
#### Back/Forward navigation
Back and Forward navigation ([popstate](https://developer.mozilla.org/en-US/docs/Web/API/Window/popstate_event)) are always handled as a soft navigation, meaning that the cache is reused, this ensures back/forward navigation is near-instant when it's in the client-side cache. This will also allow back/forward navigation to be a high priority update instead of a transition as it is based on user interaction. Note: in this PR it still uses `startTransition` as there's no way to handle the high priority update suspending which happens in case of missing data in the cache. We're working with the React team on a solution for this particular case.
### Layouts
Note: this section assumes you've read [The layouts RFC](https://nextjs.org/blog/layouts-rfc) and [React Server Components RFC](https://reactjs.org/blog/2020/12/21/data-fetching-with-react-server-components.html)
React Server Components rendering leverages the Flight streaming mechanism in React 18, this allows sending a serializable representation of the rendered React tree on the server to the browser, the client-side React can use this serialized representation to render components client-side without the JavaScript being sent to the browser. This is one of the building blocks of Server Components. This allows a bunch of interesting features but for now I'll keep it to how it affects layouts.
When you have a `app/dashboard/layout.js` and `app/dashboard/page.js` the page will render as children of the layout, when you add another page like `app/dashboard/integrations/page.js` that page falls under the dashboard layout as well. When client-side navigating the new router automatically figures out if the page you're navigating to can be a smaller render than the whole page, in this case `app/dashboard/page.js` and `app/dashboard/integrations/page.js` share the `app/dashboard/layout.js` so instead of rendering the whole page we render below the layout component, this means the layout itself does not get re-rendered, the layout's `getServerSideProps` would not be called, and the Flight response would only hold the result of `app/dashboard/integrations/page.js`, effectively giving you the smallest patch for the UI.
---
Note: the commits in this PR were mostly work in progress to ensure it wasn't lost along the way. The implementation was reworked a bunch of times to where it is now.
Co-authored-by: Jiachi Liu <4800338+huozhi@users.noreply.github.com>
Co-authored-by: JJ Kasper <22380829+ijjk@users.noreply.github.com>
2022-07-06 23:16:47 +02:00
2022-09-21 02:00:19 +02:00
// Handle Flight render request. This is only used when client-side navigating. E.g. when you `router.push('/dashboard')` or `router.reload()`.
2022-10-06 21:43:23 +02:00
const generateFlight = async ( ) : Promise < RenderResult > = > {
2022-09-21 02:00:19 +02:00
// TODO-APP: throw on invalid flightRouterState
2022-07-26 12:41:51 +02:00
/ * *
2022-09-21 02:00:19 +02:00
* Use router state to decide at what common layout to render the page .
* This can either be the common layout between two pages or a specific place to start rendering from using the "refetch" marker in the tree .
2022-07-26 12:41:51 +02:00
* /
2022-10-05 15:45:46 +02:00
const walkTreeWithFlightRouterState = async ( {
createSegmentPath ,
loaderTreeToFilter ,
parentParams ,
isFirst ,
flightRouterState ,
parentRendered ,
} : {
createSegmentPath : CreateSegmentPath
loaderTreeToFilter : LoaderTree
parentParams : { [ key : string ] : string | string [ ] }
isFirst : boolean
flightRouterState? : FlightRouterState
2022-09-21 02:00:19 +02:00
parentRendered? : boolean
2022-10-05 15:45:46 +02:00
} ) : Promise < FlightDataPath > = > {
2022-09-21 02:00:19 +02:00
const [ segment , parallelRoutes ] = loaderTreeToFilter
const parallelRoutesKeys = Object . keys ( parallelRoutes )
// Because this function walks to a deeper point in the tree to start rendering we have to track the dynamic parameters up to the point where rendering starts
const segmentParam = getDynamicParamFromSegment ( segment )
const currentParams =
// Handle null case where dynamic param is optional
segmentParam && segmentParam . value !== null
? {
. . . parentParams ,
[ segmentParam . param ] : segmentParam . value ,
}
: parentParams
const actualSegment : Segment = segmentParam
? segmentParam . treeSegment
: segment
/ * *
* Decide if the current segment is where rendering has to start .
* /
const renderComponentsOnThisLevel =
// No further router state available
! flightRouterState ||
// Segment in router state does not match current segment
! matchSegment ( actualSegment , flightRouterState [ 0 ] ) ||
// Last item in the tree
parallelRoutesKeys . length === 0 ||
// Explicit refresh
flightRouterState [ 3 ] === 'refetch'
if ( ! parentRendered && renderComponentsOnThisLevel ) {
return [
actualSegment ,
// Create router state using the slice of the loaderTree
createFlightRouterStateFromLoaderTree ( loaderTreeToFilter ) ,
// Check if one level down from the common layout has a loading component. If it doesn't only provide the router state as part of the Flight data.
isPrefetch && ! Boolean ( loaderTreeToFilter [ 2 ] . loading )
? null
: // Create component tree using the slice of the loaderTree
React . createElement (
(
await createComponentTree (
// This ensures flightRouterPath is valid and filters down the tree
{
2022-10-05 15:45:46 +02:00
createSegmentPath : ( child ) = > {
return createSegmentPath ( child )
} ,
2022-09-21 02:00:19 +02:00
loaderTree : loaderTreeToFilter ,
parentParams : currentParams ,
2022-10-05 15:45:46 +02:00
firstItem : isFirst ,
2022-09-21 02:00:19 +02:00
}
)
) . Component
) ,
]
}
Implement new client-side router (#37551)
## Client-side router for `app` directory
This PR implements the new router that leverages React 18 concurrent features like Suspense and startTransition.
It also integrates with React Server Components and builds on top of it to allow server-centric routing that only renders the part of the page that has to change.
It's one of the pieces of the implementation of https://nextjs.org/blog/layouts-rfc.
## Details
I'm going to document the differences with the current router here (will be reworked for the upgrade guide)
### Client-side cache
In the current router we have an in-memory cache for getStaticProps data so that if you prefetch and then navigate to a route that has been prefetched it'll be near-instant. For getServerSideProps the behavior is different, any navigation to a page with getServerSideProps fetches the data again.
In the new model the cache is a fundamental piece, it's more granular than at the page level and is set up to ensure consistency across concurrent renders. It can also be invalidated at any level.
#### Push/Replace (also applies to next/link)
The new router still has a `router.push` / `router.replace` method.
There are a few differences in how it works though:
- It only takes `href` as an argument, historically you had to provide `href` (the page path) and `as` (the actual url path) to do dynamic routing. In later versions of Next.js this is no longer required and in the majority of cases `as` was no longer needed. In the new router there's no way to reason about `href` vs `as` because there is no notion of "pages" in the browser.
- Both methods now use `startTransition`, you can wrap these in your own `startTransition` to get `isPending`
- The push/replace support concurrent rendering. When a render is bailed by clicking a different link to navigate to a completely different page that still works and doesn't cause race conditions.
- Support for optimistic loading states when navigating
##### Hard/Soft push/replace
Because of the client-side cache being reworked this now allows us to cover two cases: hard push and soft push.
The main difference between the two is if the cache is reused while navigating. The default for `next/link` is a `hard` push which means that the part of the cache affected by the navigation will be invalidated, e.g. if you already navigated to `/dashboard` and you `router.push('/dashboard')` again it'll get the latest version. This is similar to the existing `getServerSideProps` handling.
In case of a soft push (API to be defined but for testing added `router.softPush('/')`) it'll reuse the existing cache and not invalidate parts that are already filled in. In practice this means it's more like the `getStaticProps` client-side navigation because it does not fetch on navigation except if a part of the page is missing.
#### Back/Forward navigation
Back and Forward navigation ([popstate](https://developer.mozilla.org/en-US/docs/Web/API/Window/popstate_event)) are always handled as a soft navigation, meaning that the cache is reused, this ensures back/forward navigation is near-instant when it's in the client-side cache. This will also allow back/forward navigation to be a high priority update instead of a transition as it is based on user interaction. Note: in this PR it still uses `startTransition` as there's no way to handle the high priority update suspending which happens in case of missing data in the cache. We're working with the React team on a solution for this particular case.
### Layouts
Note: this section assumes you've read [The layouts RFC](https://nextjs.org/blog/layouts-rfc) and [React Server Components RFC](https://reactjs.org/blog/2020/12/21/data-fetching-with-react-server-components.html)
React Server Components rendering leverages the Flight streaming mechanism in React 18, this allows sending a serializable representation of the rendered React tree on the server to the browser, the client-side React can use this serialized representation to render components client-side without the JavaScript being sent to the browser. This is one of the building blocks of Server Components. This allows a bunch of interesting features but for now I'll keep it to how it affects layouts.
When you have a `app/dashboard/layout.js` and `app/dashboard/page.js` the page will render as children of the layout, when you add another page like `app/dashboard/integrations/page.js` that page falls under the dashboard layout as well. When client-side navigating the new router automatically figures out if the page you're navigating to can be a smaller render than the whole page, in this case `app/dashboard/page.js` and `app/dashboard/integrations/page.js` share the `app/dashboard/layout.js` so instead of rendering the whole page we render below the layout component, this means the layout itself does not get re-rendered, the layout's `getServerSideProps` would not be called, and the Flight response would only hold the result of `app/dashboard/integrations/page.js`, effectively giving you the smallest patch for the UI.
---
Note: the commits in this PR were mostly work in progress to ensure it wasn't lost along the way. The implementation was reworked a bunch of times to where it is now.
Co-authored-by: Jiachi Liu <4800338+huozhi@users.noreply.github.com>
Co-authored-by: JJ Kasper <22380829+ijjk@users.noreply.github.com>
2022-07-06 23:16:47 +02:00
2022-09-21 02:00:19 +02:00
// Walk through all parallel routes.
for ( const parallelRouteKey of parallelRoutesKeys ) {
const parallelRoute = parallelRoutes [ parallelRouteKey ]
2022-10-05 15:45:46 +02:00
const currentSegmentPath : FlightSegmentPath = isFirst
? [ parallelRouteKey ]
: [ actualSegment , parallelRouteKey ]
const path = await walkTreeWithFlightRouterState ( {
createSegmentPath : ( child ) = > {
return createSegmentPath ( [ . . . currentSegmentPath , . . . child ] )
} ,
loaderTreeToFilter : parallelRoute ,
parentParams : currentParams ,
flightRouterState :
flightRouterState && flightRouterState [ 1 ] [ parallelRouteKey ] ,
parentRendered : parentRendered || renderComponentsOnThisLevel ,
isFirst : false ,
} )
Implement new client-side router (#37551)
## Client-side router for `app` directory
This PR implements the new router that leverages React 18 concurrent features like Suspense and startTransition.
It also integrates with React Server Components and builds on top of it to allow server-centric routing that only renders the part of the page that has to change.
It's one of the pieces of the implementation of https://nextjs.org/blog/layouts-rfc.
## Details
I'm going to document the differences with the current router here (will be reworked for the upgrade guide)
### Client-side cache
In the current router we have an in-memory cache for getStaticProps data so that if you prefetch and then navigate to a route that has been prefetched it'll be near-instant. For getServerSideProps the behavior is different, any navigation to a page with getServerSideProps fetches the data again.
In the new model the cache is a fundamental piece, it's more granular than at the page level and is set up to ensure consistency across concurrent renders. It can also be invalidated at any level.
#### Push/Replace (also applies to next/link)
The new router still has a `router.push` / `router.replace` method.
There are a few differences in how it works though:
- It only takes `href` as an argument, historically you had to provide `href` (the page path) and `as` (the actual url path) to do dynamic routing. In later versions of Next.js this is no longer required and in the majority of cases `as` was no longer needed. In the new router there's no way to reason about `href` vs `as` because there is no notion of "pages" in the browser.
- Both methods now use `startTransition`, you can wrap these in your own `startTransition` to get `isPending`
- The push/replace support concurrent rendering. When a render is bailed by clicking a different link to navigate to a completely different page that still works and doesn't cause race conditions.
- Support for optimistic loading states when navigating
##### Hard/Soft push/replace
Because of the client-side cache being reworked this now allows us to cover two cases: hard push and soft push.
The main difference between the two is if the cache is reused while navigating. The default for `next/link` is a `hard` push which means that the part of the cache affected by the navigation will be invalidated, e.g. if you already navigated to `/dashboard` and you `router.push('/dashboard')` again it'll get the latest version. This is similar to the existing `getServerSideProps` handling.
In case of a soft push (API to be defined but for testing added `router.softPush('/')`) it'll reuse the existing cache and not invalidate parts that are already filled in. In practice this means it's more like the `getStaticProps` client-side navigation because it does not fetch on navigation except if a part of the page is missing.
#### Back/Forward navigation
Back and Forward navigation ([popstate](https://developer.mozilla.org/en-US/docs/Web/API/Window/popstate_event)) are always handled as a soft navigation, meaning that the cache is reused, this ensures back/forward navigation is near-instant when it's in the client-side cache. This will also allow back/forward navigation to be a high priority update instead of a transition as it is based on user interaction. Note: in this PR it still uses `startTransition` as there's no way to handle the high priority update suspending which happens in case of missing data in the cache. We're working with the React team on a solution for this particular case.
### Layouts
Note: this section assumes you've read [The layouts RFC](https://nextjs.org/blog/layouts-rfc) and [React Server Components RFC](https://reactjs.org/blog/2020/12/21/data-fetching-with-react-server-components.html)
React Server Components rendering leverages the Flight streaming mechanism in React 18, this allows sending a serializable representation of the rendered React tree on the server to the browser, the client-side React can use this serialized representation to render components client-side without the JavaScript being sent to the browser. This is one of the building blocks of Server Components. This allows a bunch of interesting features but for now I'll keep it to how it affects layouts.
When you have a `app/dashboard/layout.js` and `app/dashboard/page.js` the page will render as children of the layout, when you add another page like `app/dashboard/integrations/page.js` that page falls under the dashboard layout as well. When client-side navigating the new router automatically figures out if the page you're navigating to can be a smaller render than the whole page, in this case `app/dashboard/page.js` and `app/dashboard/integrations/page.js` share the `app/dashboard/layout.js` so instead of rendering the whole page we render below the layout component, this means the layout itself does not get re-rendered, the layout's `getServerSideProps` would not be called, and the Flight response would only hold the result of `app/dashboard/integrations/page.js`, effectively giving you the smallest patch for the UI.
---
Note: the commits in this PR were mostly work in progress to ensure it wasn't lost along the way. The implementation was reworked a bunch of times to where it is now.
Co-authored-by: Jiachi Liu <4800338+huozhi@users.noreply.github.com>
Co-authored-by: JJ Kasper <22380829+ijjk@users.noreply.github.com>
2022-07-06 23:16:47 +02:00
2022-09-21 02:00:19 +02:00
if ( typeof path [ path . length - 1 ] !== 'string' ) {
return [ actualSegment , parallelRouteKey , . . . path ]
}
2022-05-10 18:57:14 +02:00
}
2022-09-21 02:00:19 +02:00
return [ actualSegment ]
2022-05-10 18:57:14 +02:00
}
2022-09-21 02:00:19 +02:00
// Flight data that is going to be passed to the browser.
// Currently a single item array but in the future multiple patches might be combined in a single request.
const flightData : FlightData = [
// TODO-APP: change walk to output without ''
(
2022-10-05 15:45:46 +02:00
await walkTreeWithFlightRouterState ( {
createSegmentPath : ( child ) = > child ,
loaderTreeToFilter : loaderTree ,
parentParams : { } ,
flightRouterState : providedFlightRouterState ,
isFirst : true ,
} )
2022-09-21 02:00:19 +02:00
) . slice ( 1 ) ,
]
2022-06-02 12:02:05 +02:00
2022-09-24 02:51:12 +02:00
// For app dir, use the bundled version of Fizz renderer (renderToReadableStream)
// which contains the subset React.
2022-09-21 20:45:33 +02:00
const readable = ComponentMod . renderToReadableStream (
2022-09-21 02:00:19 +02:00
flightData ,
serverComponentManifest ,
{
context : serverContexts ,
onError : flightDataRendererErrorHandler ,
}
) . pipeThrough ( createBufferedTransformStream ( ) )
2022-06-02 12:02:05 +02:00
2022-09-21 02:00:19 +02:00
return new FlightRenderResult ( readable )
2022-09-19 20:05:28 +02:00
}
2022-05-03 12:37:23 +02:00
2022-10-06 21:43:23 +02:00
if ( isFlight && ! isStaticGeneration ) {
return generateFlight ( )
}
2022-09-21 02:00:19 +02:00
// Below this line is handling for rendering to HTML.
2022-07-12 22:54:42 +02:00
2022-09-21 02:00:19 +02:00
// Create full component tree from root to leaf.
const { Component : ComponentTree } = await createComponentTree ( {
createSegmentPath : ( child ) = > child ,
loaderTree : loaderTree ,
parentParams : { } ,
firstItem : true ,
} )
Implement new client-side router (#37551)
## Client-side router for `app` directory
This PR implements the new router that leverages React 18 concurrent features like Suspense and startTransition.
It also integrates with React Server Components and builds on top of it to allow server-centric routing that only renders the part of the page that has to change.
It's one of the pieces of the implementation of https://nextjs.org/blog/layouts-rfc.
## Details
I'm going to document the differences with the current router here (will be reworked for the upgrade guide)
### Client-side cache
In the current router we have an in-memory cache for getStaticProps data so that if you prefetch and then navigate to a route that has been prefetched it'll be near-instant. For getServerSideProps the behavior is different, any navigation to a page with getServerSideProps fetches the data again.
In the new model the cache is a fundamental piece, it's more granular than at the page level and is set up to ensure consistency across concurrent renders. It can also be invalidated at any level.
#### Push/Replace (also applies to next/link)
The new router still has a `router.push` / `router.replace` method.
There are a few differences in how it works though:
- It only takes `href` as an argument, historically you had to provide `href` (the page path) and `as` (the actual url path) to do dynamic routing. In later versions of Next.js this is no longer required and in the majority of cases `as` was no longer needed. In the new router there's no way to reason about `href` vs `as` because there is no notion of "pages" in the browser.
- Both methods now use `startTransition`, you can wrap these in your own `startTransition` to get `isPending`
- The push/replace support concurrent rendering. When a render is bailed by clicking a different link to navigate to a completely different page that still works and doesn't cause race conditions.
- Support for optimistic loading states when navigating
##### Hard/Soft push/replace
Because of the client-side cache being reworked this now allows us to cover two cases: hard push and soft push.
The main difference between the two is if the cache is reused while navigating. The default for `next/link` is a `hard` push which means that the part of the cache affected by the navigation will be invalidated, e.g. if you already navigated to `/dashboard` and you `router.push('/dashboard')` again it'll get the latest version. This is similar to the existing `getServerSideProps` handling.
In case of a soft push (API to be defined but for testing added `router.softPush('/')`) it'll reuse the existing cache and not invalidate parts that are already filled in. In practice this means it's more like the `getStaticProps` client-side navigation because it does not fetch on navigation except if a part of the page is missing.
#### Back/Forward navigation
Back and Forward navigation ([popstate](https://developer.mozilla.org/en-US/docs/Web/API/Window/popstate_event)) are always handled as a soft navigation, meaning that the cache is reused, this ensures back/forward navigation is near-instant when it's in the client-side cache. This will also allow back/forward navigation to be a high priority update instead of a transition as it is based on user interaction. Note: in this PR it still uses `startTransition` as there's no way to handle the high priority update suspending which happens in case of missing data in the cache. We're working with the React team on a solution for this particular case.
### Layouts
Note: this section assumes you've read [The layouts RFC](https://nextjs.org/blog/layouts-rfc) and [React Server Components RFC](https://reactjs.org/blog/2020/12/21/data-fetching-with-react-server-components.html)
React Server Components rendering leverages the Flight streaming mechanism in React 18, this allows sending a serializable representation of the rendered React tree on the server to the browser, the client-side React can use this serialized representation to render components client-side without the JavaScript being sent to the browser. This is one of the building blocks of Server Components. This allows a bunch of interesting features but for now I'll keep it to how it affects layouts.
When you have a `app/dashboard/layout.js` and `app/dashboard/page.js` the page will render as children of the layout, when you add another page like `app/dashboard/integrations/page.js` that page falls under the dashboard layout as well. When client-side navigating the new router automatically figures out if the page you're navigating to can be a smaller render than the whole page, in this case `app/dashboard/page.js` and `app/dashboard/integrations/page.js` share the `app/dashboard/layout.js` so instead of rendering the whole page we render below the layout component, this means the layout itself does not get re-rendered, the layout's `getServerSideProps` would not be called, and the Flight response would only hold the result of `app/dashboard/integrations/page.js`, effectively giving you the smallest patch for the UI.
---
Note: the commits in this PR were mostly work in progress to ensure it wasn't lost along the way. The implementation was reworked a bunch of times to where it is now.
Co-authored-by: Jiachi Liu <4800338+huozhi@users.noreply.github.com>
Co-authored-by: JJ Kasper <22380829+ijjk@users.noreply.github.com>
2022-07-06 23:16:47 +02:00
2022-09-21 02:00:19 +02:00
// AppRouter is provided by next-app-loader
const AppRouter =
2022-10-11 19:26:45 +02:00
ComponentMod . AppRouter as typeof import ( '../client/components/app-router' ) . default
Implement new client-side router (#37551)
## Client-side router for `app` directory
This PR implements the new router that leverages React 18 concurrent features like Suspense and startTransition.
It also integrates with React Server Components and builds on top of it to allow server-centric routing that only renders the part of the page that has to change.
It's one of the pieces of the implementation of https://nextjs.org/blog/layouts-rfc.
## Details
I'm going to document the differences with the current router here (will be reworked for the upgrade guide)
### Client-side cache
In the current router we have an in-memory cache for getStaticProps data so that if you prefetch and then navigate to a route that has been prefetched it'll be near-instant. For getServerSideProps the behavior is different, any navigation to a page with getServerSideProps fetches the data again.
In the new model the cache is a fundamental piece, it's more granular than at the page level and is set up to ensure consistency across concurrent renders. It can also be invalidated at any level.
#### Push/Replace (also applies to next/link)
The new router still has a `router.push` / `router.replace` method.
There are a few differences in how it works though:
- It only takes `href` as an argument, historically you had to provide `href` (the page path) and `as` (the actual url path) to do dynamic routing. In later versions of Next.js this is no longer required and in the majority of cases `as` was no longer needed. In the new router there's no way to reason about `href` vs `as` because there is no notion of "pages" in the browser.
- Both methods now use `startTransition`, you can wrap these in your own `startTransition` to get `isPending`
- The push/replace support concurrent rendering. When a render is bailed by clicking a different link to navigate to a completely different page that still works and doesn't cause race conditions.
- Support for optimistic loading states when navigating
##### Hard/Soft push/replace
Because of the client-side cache being reworked this now allows us to cover two cases: hard push and soft push.
The main difference between the two is if the cache is reused while navigating. The default for `next/link` is a `hard` push which means that the part of the cache affected by the navigation will be invalidated, e.g. if you already navigated to `/dashboard` and you `router.push('/dashboard')` again it'll get the latest version. This is similar to the existing `getServerSideProps` handling.
In case of a soft push (API to be defined but for testing added `router.softPush('/')`) it'll reuse the existing cache and not invalidate parts that are already filled in. In practice this means it's more like the `getStaticProps` client-side navigation because it does not fetch on navigation except if a part of the page is missing.
#### Back/Forward navigation
Back and Forward navigation ([popstate](https://developer.mozilla.org/en-US/docs/Web/API/Window/popstate_event)) are always handled as a soft navigation, meaning that the cache is reused, this ensures back/forward navigation is near-instant when it's in the client-side cache. This will also allow back/forward navigation to be a high priority update instead of a transition as it is based on user interaction. Note: in this PR it still uses `startTransition` as there's no way to handle the high priority update suspending which happens in case of missing data in the cache. We're working with the React team on a solution for this particular case.
### Layouts
Note: this section assumes you've read [The layouts RFC](https://nextjs.org/blog/layouts-rfc) and [React Server Components RFC](https://reactjs.org/blog/2020/12/21/data-fetching-with-react-server-components.html)
React Server Components rendering leverages the Flight streaming mechanism in React 18, this allows sending a serializable representation of the rendered React tree on the server to the browser, the client-side React can use this serialized representation to render components client-side without the JavaScript being sent to the browser. This is one of the building blocks of Server Components. This allows a bunch of interesting features but for now I'll keep it to how it affects layouts.
When you have a `app/dashboard/layout.js` and `app/dashboard/page.js` the page will render as children of the layout, when you add another page like `app/dashboard/integrations/page.js` that page falls under the dashboard layout as well. When client-side navigating the new router automatically figures out if the page you're navigating to can be a smaller render than the whole page, in this case `app/dashboard/page.js` and `app/dashboard/integrations/page.js` share the `app/dashboard/layout.js` so instead of rendering the whole page we render below the layout component, this means the layout itself does not get re-rendered, the layout's `getServerSideProps` would not be called, and the Flight response would only hold the result of `app/dashboard/integrations/page.js`, effectively giving you the smallest patch for the UI.
---
Note: the commits in this PR were mostly work in progress to ensure it wasn't lost along the way. The implementation was reworked a bunch of times to where it is now.
Co-authored-by: Jiachi Liu <4800338+huozhi@users.noreply.github.com>
Co-authored-by: JJ Kasper <22380829+ijjk@users.noreply.github.com>
2022-07-06 23:16:47 +02:00
2022-09-21 02:00:19 +02:00
let serverComponentsInlinedTransformStream : TransformStream <
Uint8Array ,
Uint8Array
> = new TransformStream ( )
2022-05-03 12:37:23 +02:00
2022-09-21 02:00:19 +02:00
// TODO-APP: validate req.url as it gets passed to render.
const initialCanonicalUrl = req . url !
2022-05-03 12:37:23 +02:00
2022-09-21 02:00:19 +02:00
// Get the nonce from the incomming request if it has one.
const csp = req . headers [ 'content-security-policy' ]
let nonce : string | undefined
if ( csp && typeof csp === 'string' ) {
nonce = getScriptNonceFromHeader ( csp )
}
2022-09-09 00:17:15 +02:00
2022-09-21 02:00:19 +02:00
const serverComponentsRenderOpts = {
transformStream : serverComponentsInlinedTransformStream ,
serverComponentManifest ,
serverContexts ,
rscChunks : [ ] ,
}
2022-09-19 20:05:28 +02:00
2022-09-21 02:00:19 +02:00
/ * *
* A new React Component that renders the provided React Component
* using Flight which can then be rendered to HTML .
* /
const ServerComponentsRenderer = createServerComponentRenderer (
( ) = > {
const initialTree = createFlightRouterStateFromLoaderTree ( loaderTree )
2022-07-26 12:41:51 +02:00
2022-09-21 02:00:19 +02:00
return (
< AppRouter
2022-10-07 15:25:22 +02:00
assetPrefix = { renderOpts . assetPrefix || '' }
2022-09-21 02:00:19 +02:00
initialCanonicalUrl = { initialCanonicalUrl }
initialTree = { initialTree }
>
< ComponentTree / >
< / AppRouter >
)
2022-08-03 18:21:20 +02:00
} ,
2022-09-21 02:00:19 +02:00
ComponentMod ,
serverComponentsRenderOpts ,
2022-09-23 04:44:30 +02:00
serverComponentsErrorHandler ,
2022-09-21 02:00:19 +02:00
nonce
2022-08-03 18:21:20 +02:00
)
2022-05-03 12:37:23 +02:00
2022-10-03 15:43:35 +02:00
const serverInsertedHTMLCallbacks : Set < ( ) = > React . ReactNode > = new Set ( )
function InsertedHTML ( { children } : { children : JSX.Element } ) {
// Reset addInsertedHtmlCallback on each render
serverInsertedHTMLCallbacks . clear ( )
const addInsertedHtml = React . useCallback (
2022-09-21 02:00:19 +02:00
( handler : ( ) = > React . ReactNode ) = > {
2022-10-03 15:43:35 +02:00
serverInsertedHTMLCallbacks . add ( handler )
2022-09-21 02:00:19 +02:00
} ,
[ ]
)
2022-05-03 12:37:23 +02:00
2022-09-21 02:00:19 +02:00
return (
2022-10-09 17:08:51 +02:00
< HeadManagerContext.Provider
value = { {
appDir : true ,
nonce ,
} }
>
< ServerInsertedHTMLContext.Provider value = { addInsertedHtml } >
{ children }
< / ServerInsertedHTMLContext.Provider >
< / HeadManagerContext.Provider >
2022-09-21 02:00:19 +02:00
)
}
2022-05-03 12:37:23 +02:00
2022-09-21 02:00:19 +02:00
const bodyResult = async ( ) = > {
2022-10-09 17:08:51 +02:00
const polyfills = buildManifest . polyfillFiles
. filter (
( polyfill ) = >
polyfill . endsWith ( '.js' ) && ! polyfill . endsWith ( '.module.js' )
)
. map ( ( polyfill ) = > ( {
src : ` ${ renderOpts . assetPrefix || '' } /_next/ ${ polyfill } ` ,
integrity : subresourceIntegrityManifest?. [ polyfill ] ,
} ) )
2022-09-21 02:00:19 +02:00
const content = (
2022-10-03 15:43:35 +02:00
< InsertedHTML >
2022-09-21 02:00:19 +02:00
< ServerComponentsRenderer / >
2022-10-03 15:43:35 +02:00
< / InsertedHTML >
2022-08-03 18:21:20 +02:00
)
2022-09-21 02:00:19 +02:00
2022-10-09 17:08:51 +02:00
let polyfillsFlushed = false
2022-10-03 15:43:35 +02:00
const getServerInsertedHTML = ( ) : Promise < string > = > {
2022-09-29 10:56:28 +02:00
const flushed = renderToString (
2022-10-03 15:43:35 +02:00
< >
{ Array . from ( serverInsertedHTMLCallbacks ) . map ( ( callback ) = >
callback ( )
) }
2022-10-09 17:08:51 +02:00
{ polyfillsFlushed
? null
: polyfills ? . map ( ( polyfill ) = > {
return (
< script
key = { polyfill . src }
2022-10-12 23:08:36 +02:00
src = { polyfill . src }
integrity = { polyfill . integrity }
2022-10-09 17:08:51 +02:00
noModule = { true }
nonce = { nonce }
/ >
)
} ) }
2022-10-03 15:43:35 +02:00
< / >
2022-09-21 02:00:19 +02:00
)
2022-10-09 17:08:51 +02:00
polyfillsFlushed = true
2022-09-21 02:00:19 +02:00
return flushed
}
try {
const renderStream = await renderToInitialStream ( {
ReactDOMServer ,
element : content ,
streamOptions : {
onError : htmlRendererErrorHandler ,
nonce ,
// Include hydration scripts in the HTML
2022-09-30 01:22:21 +02:00
bootstrapScripts : [
. . . ( subresourceIntegrityManifest
? buildManifest . rootMainFiles . map ( ( src ) = > ( {
src : ` ${ renderOpts . assetPrefix || '' } /_next/ ` + src ,
integrity : subresourceIntegrityManifest [ src ] ,
} ) )
: buildManifest . rootMainFiles . map (
( src ) = > ` ${ renderOpts . assetPrefix || '' } /_next/ ` + src
) ) ,
] ,
2022-09-21 02:00:19 +02:00
} ,
} )
return await continueFromInitialStream ( renderStream , {
dataStream : serverComponentsInlinedTransformStream?.readable ,
2022-10-06 21:43:23 +02:00
generateStaticHTML : isStaticGeneration ,
2022-10-03 15:43:35 +02:00
getServerInsertedHTML ,
serverInsertedHTMLToHead : true ,
2022-10-03 21:27:16 +02:00
dev ,
2022-09-21 02:00:19 +02:00
} )
} catch ( err : any ) {
// TODO-APP: show error overlay in development. `element` should probably be wrapped in AppRouter for this case.
const renderStream = await renderToInitialStream ( {
ReactDOMServer ,
element : (
< html id = "__next_error__" >
< head > < / head >
< body > < / body >
< / html >
) ,
streamOptions : {
nonce ,
// Include hydration scripts in the HTML
bootstrapScripts : subresourceIntegrityManifest
? buildManifest . rootMainFiles . map ( ( src ) = > ( {
src : ` ${ renderOpts . assetPrefix || '' } /_next/ ` + src ,
integrity : subresourceIntegrityManifest [ src ] ,
} ) )
: buildManifest . rootMainFiles . map (
( src ) = > ` ${ renderOpts . assetPrefix || '' } /_next/ ` + src
) ,
} ,
} )
return await continueFromInitialStream ( renderStream , {
dataStream : serverComponentsInlinedTransformStream?.readable ,
2022-10-06 21:43:23 +02:00
generateStaticHTML : isStaticGeneration ,
2022-10-03 15:43:35 +02:00
getServerInsertedHTML ,
serverInsertedHTMLToHead : true ,
2022-10-03 21:27:16 +02:00
dev ,
2022-09-21 02:00:19 +02:00
} )
}
}
2022-10-06 21:43:23 +02:00
const renderResult = new RenderResult ( await bodyResult ( ) )
2022-09-21 02:00:19 +02:00
2022-10-06 21:43:23 +02:00
if ( isStaticGeneration ) {
const htmlResult = await streamToBufferedResult ( renderResult )
2022-09-23 04:44:30 +02:00
// if we encountered any unexpected errors during build
// we fail the prerendering phase and the build
if ( capturedErrors . length > 0 ) {
throw capturedErrors [ 0 ]
}
2022-10-06 21:43:23 +02:00
// const before = Buffer.concat(
// serverComponentsRenderOpts.rscChunks
// ).toString()
// TODO-APP: derive this from same pass to prevent additional
// render during static generation
const filteredFlightData = await streamToBufferedResult (
await generateFlight ( )
)
2022-09-23 04:44:30 +02:00
2022-10-06 21:43:23 +02:00
; ( renderOpts as any ) . pageData = filteredFlightData
2022-09-21 02:00:19 +02:00
; ( renderOpts as any ) . revalidate =
typeof staticGenerationStore ? . revalidate === 'undefined'
? defaultRevalidate
: staticGenerationStore ? . revalidate
2022-10-06 21:43:23 +02:00
return new RenderResult ( htmlResult )
2022-08-03 18:21:20 +02:00
}
2022-10-06 21:43:23 +02:00
return renderResult
2022-05-03 12:37:23 +02:00
}
2022-09-21 02:00:19 +02:00
const initialStaticGenerationStore = {
isStaticGeneration ,
inUse : true ,
pathname ,
2022-09-19 20:05:28 +02:00
}
2022-09-20 15:28:07 +02:00
2022-09-25 11:45:00 +02:00
const tryGetPreviewData =
process . env . NEXT_RUNTIME === 'edge'
? ( ) = > false
: require ( './api-utils/node' ) . tryGetPreviewData
// Reads of this are cached on the `req` object, so this should resolve
// instantly. There's no need to pass this data down from a previous
// invoke, where we'd have to consider server & serverless.
const previewData = tryGetPreviewData (
req ,
res ,
( renderOpts as any ) . previewProps
)
2022-10-13 17:19:10 +02:00
let cachedHeadersInstance : ReadonlyHeaders | undefined
let cachedCookiesInstance : ReadonlyNextCookies | undefined
2022-09-25 11:45:00 +02:00
const requestStore = {
2022-10-13 17:19:10 +02:00
get headers() {
if ( ! cachedHeadersInstance ) {
cachedHeadersInstance = new ReadonlyHeaders (
headersWithoutFlight ( req . headers )
)
}
return cachedHeadersInstance
} ,
get cookies() {
if ( ! cachedCookiesInstance ) {
cachedCookiesInstance = new ReadonlyNextCookies ( {
headers : {
get : ( key ) = > {
if ( key !== 'cookie' ) {
throw new Error ( 'Only cookie header is supported' )
}
return req . headers . cookie
} ,
} ,
} )
}
return cachedCookiesInstance
} ,
2022-09-25 11:45:00 +02:00
previewData ,
}
function handleRequestStoreRun < T > ( fn : ( ) = > T ) : Promise < T > {
if ( 'getStore' in requestAsyncStorage ) {
return new Promise ( ( resolve , reject ) = > {
requestAsyncStorage . run ( requestStore , ( ) = > {
return Promise . resolve ( fn ( ) ) . then ( resolve ) . catch ( reject )
} )
} )
} else {
Object . assign ( requestAsyncStorage , requestStore )
return Promise . resolve ( fn ( ) )
}
}
function handleStaticGenerationStoreRun < T > ( fn : ( ) = > T ) : Promise < T > {
if ( 'getStore' in staticGenerationAsyncStorage ) {
return new Promise ( ( resolve , reject ) = > {
2022-09-21 02:00:19 +02:00
staticGenerationAsyncStorage . run ( initialStaticGenerationStore , ( ) = > {
2022-09-25 11:45:00 +02:00
return Promise . resolve ( fn ( ) ) . then ( resolve ) . catch ( reject )
2022-09-21 02:00:19 +02:00
} )
2022-09-25 11:45:00 +02:00
} )
} else {
Object . assign ( staticGenerationAsyncStorage , initialStaticGenerationStore )
return Promise . resolve ( fn ( ) ) . finally ( ( ) = > {
staticGenerationAsyncStorage . inUse = false
} )
}
2022-09-20 15:28:07 +02:00
}
2022-09-25 11:45:00 +02:00
return handleRequestStoreRun ( ( ) = >
handleStaticGenerationStoreRun ( ( ) = > wrappedRender ( ) )
)
2022-05-03 12:37:23 +02:00
}