Skip to content

Commit 8f116e3

Browse files
committed
Implement workaround for hydration error on React strict mode (adobe#2231, adobe#779)
This commit implements a workaround for hydration error during SSR running in strict mode. The workaround uses React.useId, which is introduced in React 18. On React 16 and 17, nothing changes with this commit (i.e. hydration error still occurs).
1 parent ed2c240 commit 8f116e3

File tree

5 files changed

+48
-1
lines changed

5 files changed

+48
-1
lines changed

packages/@react-aria/ssr/src/SSRProvider.tsx

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,8 +69,26 @@ let canUseDOM = Boolean(
6969
window.document.createElement
7070
);
7171

72+
// Access `React.useId` using `toString` to supress bundler warning (workaround for https://github.com/webpack/webpack/issues/14814)
73+
const useId: () => string | undefined = (React as any)['useId'.toString()];
74+
7275
/** @private */
7376
export function useSSRSafeId(defaultId?: string): string {
77+
// Use React.useId if available (i.e. if running on React 18). Otherwise, fallback to useIdForLegacyReact.
78+
// It is safe to wrap hooks with if statement here because useId is invariant on runtime.
79+
if (typeof useId === 'function') {
80+
// eslint-disable-next-line react-hooks/rules-of-hooks
81+
const id = useId();
82+
return defaultId || id;
83+
} else {
84+
// eslint-disable-next-line react-hooks/rules-of-hooks
85+
return useIdForLegacyReact(defaultId);
86+
}
87+
}
88+
89+
// Returns unique ID that is consistent across the server and client.
90+
// Known limitation: on strict mode, generated IDs doesn't match between the server and client.
91+
function useIdForLegacyReact(defaultId?: string): string {
7492
let ctx = useContext(SSRContext);
7593

7694
// If we are rendering in a non-DOM environment, and there's no SSRProvider,
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import {testSSR} from '@react-spectrum/test-utils';
2+
3+
describe('useSSRSafeId', function () {
4+
it('should render without errors', async function () {
5+
await testSSR(__filename, `
6+
import {useSSRSafeId} from '../';
7+
const Test = () => <div id={useSSRSafeId()} />;
8+
<Test />
9+
`);
10+
});
11+
});

packages/@react-aria/ssr/test/SSRProvider.test.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,11 @@ function Test() {
2121

2222
describe('SSRProvider', function () {
2323
it('it should generate consistent unique ids', function () {
24+
if (typeof React.useId === 'function') {
25+
// We cannot test against IDs generated by React.useId.
26+
return;
27+
}
28+
2429
let tree = render(
2530
<SSRProvider>
2631
<Test />
@@ -34,6 +39,11 @@ describe('SSRProvider', function () {
3439
});
3540

3641
it('it should generate consistent unique ids with nested SSR providers', function () {
42+
if (typeof React.useId === 'function') {
43+
// We cannot test against IDs generated by React.useId.
44+
return;
45+
}
46+
3747
let tree = render(
3848
<SSRProvider>
3949
<Test />

packages/dev/docs/pages/react-aria/ssr.mdx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,9 +39,14 @@ import {SSRProvider} from '@react-aria/ssr';
3939

4040
Wrapping your application in an `SSRProvider` helps ensure that the HTML generated on the server matches the DOM structure hydrated on the client. Specifically, it affects React Aria’s automatic id generation, and you can also use this information to influence rendering in your own components.
4141

42+
Note that SSRProvider doesn't support [strict mode](https://reactjs.org/docs/strict-mode.html) on React 16 and 17.
43+
When strict mode is enabled on React 16 or 17, it generates different IDs between server and client. That results in hydration errors.
44+
4245
## Automatic ID Generation
4346

44-
When using SSR, only a single copy of React Aria can be on the page at a time. This is in contrast to client-side rendering, where multiple copies from different parts of an app can coexist. Internally, many components rely on auto-generated ids to link related elements via ARIA attributes. These ids typically use a randomly generated seed plus an incrementing counter to ensure uniqueness even when multiple instances of React Aria are on the page. With SSR, we need to ensure that these ids are consistent between the server and client. This means the counter resets on every request, and we use a consistent seed. Due to this, multiple copies of React Aria cannot be supported because the auto-generated ids would conflict.
47+
If you are using React 18, React Aria uses [React.useId](https://reactjs.org/docs/hooks-reference.html#useid) to generate unique IDs that is stable across the client and server.
48+
49+
If you are using React 16 or 17, React Aria tries to generate consistent IDs by itself. In this case, when using SSR, only a single copy of React Aria can be on the page at a time. This is in contrast to client-side rendering, where multiple copies from different parts of an app can coexist. Internally, many components rely on auto-generated ids to link related elements via ARIA attributes. These ids typically use a randomly generated seed plus an incrementing counter to ensure uniqueness even when multiple instances of React Aria are on the page. With SSR, we need to ensure that these ids are consistent between the server and client. This means the counter resets on every request, and we use a consistent seed. Due to this, multiple copies of React Aria cannot be supported because the auto-generated ids would conflict.
4550

4651
If you use React Aria’s [useId](useId.html) hook in your own components, `SSRProvider` will ensure the ids are consistent when server rendered. No additional changes in each component are required to enable
4752
SSR support.

packages/dev/docs/pages/react-spectrum/ssr.mdx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,9 @@ import {SSRProvider, Provider, defaultTheme} from '@adobe/react-spectrum';
4242

4343
Wrapping your application in an `SSRProvider` ensures that the HTML generated on the server matches the DOM structure hydrated on the client. Specifically, it affects four things: id generation for accessibility, media queries, feature detection, and automatic locale selection.
4444

45+
Note that `SSRProvider` doesn't support [strict mode](https://reactjs.org/docs/strict-mode.html) on React 16 and 17.
46+
When strict mode is enabled on React 16 or 17, it generates different IDs between server and client. That results in hydration errors.
47+
4548
When using SSR, only a single copy of React Spectrum can be on the page at a time. This is in contrast to client-side rendering, where multiple copies from different parts of an app can coexist. Internally, many components rely on auto-generated ids to link related elements via ARIA attributes. When server side rendering, these ids need to be consistent so they match between the server and client, and this would not be possible with multiple copies of React Spectrum.
4649

4750
Media queries and DOM feature detection cannot be performed on the server because they depend on specific browser parameters that aren’t sent as part of the request. In cases where these affect the rendering of a particular component, this check is delayed until just after hydration is completed. This ensures that the rendering is consistent between the server and hydrated DOM, but updated immediately after the page becomes interactive.

0 commit comments

Comments
 (0)