diff --git a/packages/next/server/render.tsx b/packages/next/server/render.tsx index fd44dbc070cefce..eed32ff812a8a6a 100644 --- a/packages/next/server/render.tsx +++ b/packages/next/server/render.tsx @@ -513,9 +513,9 @@ export async function renderToHTML( defaultLocale: renderOpts.defaultLocale, AppTree: (props: any) => { return ( - + - + ) }, defaultGetInitialProps: async ( @@ -576,6 +576,41 @@ export async function renderToHTML( ) + // The `useId` API uses the path indexes to generate an ID for each node. + // To guarantee the match of hydration, we need to ensure that the structure + // of wrapper nodes is isomorphic in server and client. + // TODO: With `enhanceApp` and `enhanceComponents` options, this approach may + // not be useful. + // https://github.com/facebook/react/pull/22644 + const Noop = () => null + const AppContainerWithIsomorphicFiberStructure = ({ + children, + }: { + children: JSX.Element + }) => { + return ( + <> + {/* */} + + + <> + {/* */} + {dev ? ( + <> + {children} + + + ) : ( + children + )} + {/* */} + + + + + ) + } + props = await loadGetInitialProps(App, { AppTree: ctx.AppTree, Component, @@ -940,11 +975,11 @@ export async function renderToHTML( // opaque component. Wrappers should use context instead. const InnerApp = () => app return ( - + {appWrappers.reduce((innerContent, fn) => { return fn(innerContent) }, )} - + ) } diff --git a/test/integration/react-18/app/pages/use-id.js b/test/integration/react-18/app/pages/use-id.js new file mode 100644 index 000000000000000..1b5f6fef28fca0f --- /dev/null +++ b/test/integration/react-18/app/pages/use-id.js @@ -0,0 +1,5 @@ +import { useId } from 'react' + +export default function Page() { + return
{useId()}
+} diff --git a/test/integration/react-18/test/basics.js b/test/integration/react-18/test/basics.js index bb3c4ad99ced4f7..d54a9b02e4dd436 100644 --- a/test/integration/react-18/test/basics.js +++ b/test/integration/react-18/test/basics.js @@ -28,4 +28,15 @@ export default (context) => { expect(content).toBe('rab') expect(nextData.dynamicIds).toBeUndefined() }) + + it('useId() values should match on hydration', async () => { + const html = await renderViaHTTP(context.appPort, '/use-id') + const $ = cheerio.load(html) + const ssrId = $('#id').text() + + const browser = await webdriver(context.appPort, '/use-id') + const csrId = await browser.eval('document.getElementById("id").innerText') + + expect(ssrId).toEqual(csrId) + }) }