Skip to content
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

Implement workaround for hydration error on React strict mode #3637

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from 1 commit
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
7 changes: 6 additions & 1 deletion packages/dev/docs/pages/react-aria/ssr.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,14 @@ 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.
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.
ciffelia marked this conversation as resolved.
Show resolved Hide resolved

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.
ciffelia marked this conversation as resolved.
Show resolved Hide resolved

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.
Expand Down
3 changes: 3 additions & 0 deletions packages/dev/docs/pages/react-spectrum/ssr.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@ 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.

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. 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.

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