Skip to content

Implement workaround for hydration error on React strict mode #3637

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions packages/@react-aria/ssr/src/SSRProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -69,8 +69,26 @@ let canUseDOM = Boolean(
window.document.createElement
);

// Access `React.useId` using `toString` to supress bundler warning (workaround for https://github.com/webpack/webpack/issues/14814)
const useId: () => string | undefined = (React as any)['useId'.toString()];

/** @private */
export function useSSRSafeId(defaultId?: string): string {
// Use React.useId if available (i.e. if running on React 18). Otherwise, fallback to useIdForLegacyReact.
// It is safe to wrap hooks with if statement here because useId is invariant on runtime.
if (typeof useId === 'function') {
// eslint-disable-next-line react-hooks/rules-of-hooks
const id = useId();
return defaultId || id;
} else {
// eslint-disable-next-line react-hooks/rules-of-hooks
return useIdForLegacyReact(defaultId);
}
}

// Returns unique ID that is consistent across the server and client.
// Known limitation: on strict mode, generated IDs doesn't match between the server and client.
function useIdForLegacyReact(defaultId?: string): string {
let ctx = useContext(SSRContext);

// If we are rendering in a non-DOM environment, and there's no SSRProvider,
Expand Down
11 changes: 11 additions & 0 deletions packages/@react-aria/ssr/test/SSRProvider.ssr.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import {testSSR} from '@react-spectrum/test-utils';

describe('useSSRSafeId', function () {
it('should render without errors', async function () {
await testSSR(__filename, `
import {useSSRSafeId} from '../';
const Test = () => <div id={useSSRSafeId()} />;
<Test />
`);
});
});
10 changes: 10 additions & 0 deletions packages/@react-aria/ssr/test/SSRProvider.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,11 @@ function Test() {

describe('SSRProvider', function () {
it('it should generate consistent unique ids', function () {
if (typeof React.useId === 'function') {
// We cannot test against IDs generated by React.useId.
return;
}

let tree = render(
<SSRProvider>
<Test />
Expand All @@ -34,6 +39,11 @@ describe('SSRProvider', function () {
});

it('it should generate consistent unique ids with nested SSR providers', function () {
if (typeof React.useId === 'function') {
// We cannot test against IDs generated by React.useId.
return;
}

let tree = render(
<SSRProvider>
<Test />
Expand Down
18 changes: 14 additions & 4 deletions packages/dev/docs/pages/react-aria/ssr.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -39,16 +39,26 @@ import {SSRProvider} from '@react-aria/ssr';

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.

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

## Automatic ID Generation

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.
### React 18

If you are using React 18, React Aria uses [React.useId](https://reactjs.org/docs/hooks-reference.html#useid) to generate unique IDs that are stable across the client and server.

If your React app have multiple roots, you need to configure `identifierPrefix` option on `hydrateRoot` and `ReactDOMServer` to prevent collisions. For more information, please see the documentation of [React.useId](https://reactjs.org/docs/hooks-reference.html#useid).

### React 16 and 17

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, and React StrictMode cannot be supported.

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
SSR support.
You may 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 SSR support.

## SSR specific rendering

You can also use the [useIsSSR](useIsSSR.html) hook in your own components to determine whether they are running in an SSR context. This hook returns `true` both during server rendering and hydration, but updates immediately to `false` after hydration. You can use this to delay browser-specific code like media queries and feature detection until after the client has hydrated.
You can use the [useIsSSR](useIsSSR.html) hook in your own components to determine whether they are running in an SSR context. This hook returns `true` both during server rendering and hydration, but updates immediately to `false` after hydration. You can use this to delay browser-specific code like media queries and feature detection until after the client has hydrated.

```tsx
import {useIsSSR} from '@react-aria/ssr';
Expand Down
5 changes: 4 additions & 1 deletion packages/dev/docs/pages/react-spectrum/ssr.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,10 @@ import {SSRProvider, Provider, defaultTheme} from '@adobe/react-spectrum';

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.

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.
Note that `SSRProvider` doesn't support [strict mode](https://reactjs.org/docs/strict-mode.html) on React 16 and 17.
When strict mode is enabled on React 16 or 17, it generates different IDs between server and client. That results in hydration errors.

When using SSR, only a single copy of React Spectrum can be on the page at a time. If you are using React 18, you may configure `identifierPrefix` option on `hydrateRoot` and `ReactDOMServer` to bypass this restriction. For more information, please see the documentation of [React.useId](https://reactjs.org/docs/hooks-reference.html#useid).

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.

Expand Down