Skip to content

Commit

Permalink
Fix useId mismatches on hydration (#31102)
Browse files Browse the repository at this point in the history
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`
  • Loading branch information
shuding committed Nov 8, 2021
1 parent 87c4d17 commit 960298b
Show file tree
Hide file tree
Showing 3 changed files with 55 additions and 4 deletions.
43 changes: 39 additions & 4 deletions packages/next/server/render.tsx
Expand Up @@ -513,9 +513,9 @@ export async function renderToHTML(
defaultLocale: renderOpts.defaultLocale,
AppTree: (props: any) => {
return (
<AppContainer>
<AppContainerWithIsomorphicFiberStructure>
<App {...props} Component={Component} router={router} />
</AppContainer>
</AppContainerWithIsomorphicFiberStructure>
)
},
defaultGetInitialProps: async (
Expand Down Expand Up @@ -576,6 +576,41 @@ export async function renderToHTML(
</RouterContext.Provider>
)

// 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 (
<>
{/* <Head/> */}
<Noop />
<AppContainer>
<>
{/* <ReactDevOverlay/> */}
{dev ? (
<>
{children}
<Noop />
</>
) : (
children
)}
{/* <RouteAnnouncer/> */}
<Noop />
</>
</AppContainer>
</>
)
}

props = await loadGetInitialProps(App, {
AppTree: ctx.AppTree,
Component,
Expand Down Expand Up @@ -940,11 +975,11 @@ export async function renderToHTML(
// opaque component. Wrappers should use context instead.
const InnerApp = () => app
return (
<AppContainer>
<AppContainerWithIsomorphicFiberStructure>
{appWrappers.reduce((innerContent, fn) => {
return fn(innerContent)
}, <InnerApp />)}
</AppContainer>
</AppContainerWithIsomorphicFiberStructure>
)
}

Expand Down
5 changes: 5 additions & 0 deletions test/integration/react-18/app/pages/use-id.js
@@ -0,0 +1,5 @@
import { useId } from 'react'

export default function Page() {
return <div id="id">{useId()}</div>
}
11 changes: 11 additions & 0 deletions test/integration/react-18/test/basics.js
Expand Up @@ -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)
})
}

0 comments on commit 960298b

Please sign in to comment.