diff --git a/packages/next/pages/_document.tsx b/packages/next/pages/_document.tsx index a1c855bef1d70..a69b919b16709 100644 --- a/packages/next/pages/_document.tsx +++ b/packages/next/pages/_document.tsx @@ -1,8 +1,5 @@ import React, { Component, ReactElement, ReactNode, useContext } from 'react' -import { - BODY_RENDER_TARGET, - OPTIMIZED_FONT_PROVIDERS, -} from '../shared/lib/constants' +import { OPTIMIZED_FONT_PROVIDERS } from '../shared/lib/constants' import { DocumentContext, DocumentInitialProps, @@ -763,13 +760,18 @@ export class Head extends Component< } } -export function Main() { - const { inAmpMode, docComponentsRendered } = useContext(HtmlContext) - +export function Main({ + children, +}: { + children?: (content: JSX.Element) => JSX.Element +}) { + const { inAmpMode, docComponentsRendered, useMainContent } = + useContext(HtmlContext) + const content = useMainContent(children) docComponentsRendered.Main = true - if (inAmpMode) return <>{BODY_RENDER_TARGET} - return
{BODY_RENDER_TARGET}
+ if (inAmpMode) return content + return
{content}
} export class NextScript extends Component { diff --git a/packages/next/server/render.tsx b/packages/next/server/render.tsx index 53fa578287d73..fd44dbc070cef 100644 --- a/packages/next/server/render.tsx +++ b/packages/next/server/render.tsx @@ -20,7 +20,6 @@ import { GetServerSideProps, GetStaticProps, PreviewData } from '../types' import { isInAmpMode } from '../shared/lib/amp' import { AmpStateContext } from '../shared/lib/amp-context' import { - BODY_RENDER_TARGET, SERVER_PROPS_ID, STATIC_PROPS_ID, STATIC_STATUS_PAGES, @@ -935,6 +934,20 @@ export async function renderToHTML( } } + const appWrappers: Array<(content: JSX.Element) => JSX.Element> = [] + const getWrappedApp = (app: JSX.Element) => { + // Prevent wrappers from reading/writing props by rendering inside an + // opaque component. Wrappers should use context instead. + const InnerApp = () => app + return ( + + {appWrappers.reduce((innerContent, fn) => { + return fn(innerContent) + }, )} + + ) + } + /** * Rules of Static & Dynamic HTML: * @@ -976,13 +989,13 @@ export async function renderToHTML( enhanceComponents(options, App, Component) const html = ReactDOMServer.renderToString( - + getWrappedApp( - + ) ) return { html, head } } @@ -1002,33 +1015,51 @@ export async function renderToHTML( } return { - bodyResult: piperFromArray([docProps.html]), + bodyResult: () => piperFromArray([docProps.html]), documentElement: (htmlProps: HtmlProps) => ( ), + useMainContent: (fn?: (content: JSX.Element) => JSX.Element) => { + if (fn) { + throw new Error( + 'The `children` property is not supported by non-functional custom Document components' + ) + } + // @ts-ignore + return + }, head: docProps.head, headTags: await headTags(documentCtx), styles: docProps.styles, } } else { - const content = - ctx.err && ErrorDebug ? ( - - ) : ( - - - - ) + const bodyResult = async () => { + const content = + ctx.err && ErrorDebug ? ( + + ) : ( + getWrappedApp( + + ) + ) - const bodyResult = concurrentFeatures - ? process.browser - ? await renderToReadableStream(content) - : await renderToNodeStream(content, generateStaticHTML) - : piperFromArray([ReactDOMServer.renderToString(content)]) + return concurrentFeatures + ? process.browser + ? await renderToReadableStream(content) + : await renderToNodeStream(content, generateStaticHTML) + : piperFromArray([ReactDOMServer.renderToString(content)]) + } return { bodyResult, documentElement: () => (Document as any)(), + useMainContent: (fn?: (content: JSX.Element) => JSX.Element) => { + if (fn) { + appWrappers.push(fn) + } + // @ts-ignore + return + }, head, headTags: [], styles: jsxStyleRegistry.styles(), @@ -1056,8 +1087,8 @@ export async function renderToHTML( } const hybridAmp = ampState.hybrid - const docComponentsRendered: DocumentProps['docComponentsRendered'] = {} + const { assetPrefix, buildId, @@ -1123,6 +1154,7 @@ export async function renderToHTML( head: documentResult.head, headTags: documentResult.headTags, styles: documentResult.styles, + useMainContent: documentResult.useMainContent, useMaybeDeferContent, } @@ -1181,20 +1213,20 @@ export async function renderToHTML( } } - const renderTargetIdx = documentHTML.indexOf(BODY_RENDER_TARGET) + const [renderTargetPrefix, renderTargetSuffix] = documentHTML.split( + /<\/next-js-internal-body-render-target>/ + ) const prefix: Array = [] prefix.push('') - prefix.push(documentHTML.substring(0, renderTargetIdx)) + prefix.push(renderTargetPrefix) if (inAmpMode) { prefix.push('') } let pipers: Array = [ piperFromArray(prefix), - documentResult.bodyResult, - piperFromArray([ - documentHTML.substring(renderTargetIdx + BODY_RENDER_TARGET.length), - ]), + await documentResult.bodyResult(), + piperFromArray([renderTargetSuffix]), ] const postProcessors: Array<((html: string) => Promise) | null> = ( diff --git a/packages/next/shared/lib/constants.ts b/packages/next/shared/lib/constants.ts index e181c24d03ecf..ba3a5f7e93c9e 100644 --- a/packages/next/shared/lib/constants.ts +++ b/packages/next/shared/lib/constants.ts @@ -23,7 +23,6 @@ export const BLOCKED_PAGES = ['/_document', '/_app', '/_error'] export const CLIENT_PUBLIC_FILES_PATH = 'public' export const CLIENT_STATIC_FILES_PATH = 'static' export const CLIENT_STATIC_FILES_RUNTIME = 'runtime' -export const BODY_RENDER_TARGET = '__NEXT_BODY_RENDER_TARGET__' export const STRING_LITERAL_DROP_BUNDLE = '__NEXT_DROP_CLIENT_FILE__' // server/middleware-flight-manifest.js diff --git a/packages/next/shared/lib/utils.ts b/packages/next/shared/lib/utils.ts index 3582203816499..a74499fdba9b8 100644 --- a/packages/next/shared/lib/utils.ts +++ b/packages/next/shared/lib/utils.ts @@ -221,6 +221,7 @@ export type HtmlProps = { styles?: React.ReactElement[] | React.ReactFragment head?: Array useMaybeDeferContent: MaybeDeferContentHook + useMainContent: (fn?: (content: JSX.Element) => JSX.Element) => JSX.Element } /** diff --git a/test/integration/document-functional-render-prop/lib/context.js b/test/integration/document-functional-render-prop/lib/context.js new file mode 100644 index 0000000000000..229e13653eb1c --- /dev/null +++ b/test/integration/document-functional-render-prop/lib/context.js @@ -0,0 +1,3 @@ +import { createContext } from 'react' + +export default createContext(null) diff --git a/test/integration/document-functional-render-prop/pages/_document.js b/test/integration/document-functional-render-prop/pages/_document.js new file mode 100644 index 0000000000000..6f7199c4e6d00 --- /dev/null +++ b/test/integration/document-functional-render-prop/pages/_document.js @@ -0,0 +1,20 @@ +import { Html, Head, Main, NextScript } from 'next/document' +import Context from '../lib/context' + +export default function Document() { + return ( + + + +
+ {(children) => ( + + {children} + + )} +
+ + + + ) +} diff --git a/test/integration/document-functional-render-prop/pages/index.js b/test/integration/document-functional-render-prop/pages/index.js new file mode 100644 index 0000000000000..d00fe7fc70a6b --- /dev/null +++ b/test/integration/document-functional-render-prop/pages/index.js @@ -0,0 +1,7 @@ +import { useContext } from 'react' +import Context from '../lib/context' + +export default function MainRenderProp() { + const value = useContext(Context) + return {value} +} diff --git a/test/integration/document-functional-render-prop/tests/index.test.js b/test/integration/document-functional-render-prop/tests/index.test.js new file mode 100644 index 0000000000000..d7cb027d41f7e --- /dev/null +++ b/test/integration/document-functional-render-prop/tests/index.test.js @@ -0,0 +1,24 @@ +/* eslint-env jest */ + +import { join } from 'path' +import { findPort, launchApp, killApp, renderViaHTTP } from 'next-test-utils' + +const appDir = join(__dirname, '..') +let appPort +let app + +describe('Functional Custom Document', () => { + describe('development mode', () => { + beforeAll(async () => { + appPort = await findPort() + app = await launchApp(appDir, appPort) + }) + + afterAll(() => killApp(app)) + + it('supports render props', async () => { + const html = await renderViaHTTP(appPort, '/') + expect(html).toMatch(/from render prop<\/span>/) + }) + }) +})