From 960298b3447f8476a43f29d2badb125b697ffff0 Mon Sep 17 00:00:00 2001
From: Shu Ding
Date: Tue, 9 Nov 2021 00:32:06 +0100
Subject: [PATCH] Fix `useId` mismatches on hydration (#31102)
Fixes #30876.
## Bug
- [x] Related issues linked using `fixes #number`
- [x] Integration tests added
- [ ] Errors have helpful link attached, see `contributing.md`
## Feature
- [ ] Implements an existing feature request or RFC. Make sure the feature request has been accepted for implementation before opening a PR.
- [ ] Related issues linked using `fixes #number`
- [ ] Integration tests added
- [ ] Documentation added
- [ ] Telemetry added. In case of a feature if it's used or not.
- [ ] Errors have helpful link attached, see `contributing.md`
## Documentation / Examples
- [ ] Make sure the linting passes by running `yarn lint`
---
packages/next/server/render.tsx | 43 +++++++++++++++++--
test/integration/react-18/app/pages/use-id.js | 5 +++
test/integration/react-18/test/basics.js | 11 +++++
3 files changed, 55 insertions(+), 4 deletions(-)
create mode 100644 test/integration/react-18/app/pages/use-id.js
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)
+ })
}