From 8f116e3199e2db57f8c68c54f0d9c3e5c237ad80 Mon Sep 17 00:00:00 2001 From: Ciffelia Date: Fri, 16 Sep 2022 15:40:34 +0900 Subject: [PATCH 1/3] Implement workaround for hydration error on React strict mode (#2231, #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). --- packages/@react-aria/ssr/src/SSRProvider.tsx | 18 ++++++++++++++++++ .../ssr/test/SSRProvider.ssr.test.js | 11 +++++++++++ .../@react-aria/ssr/test/SSRProvider.test.js | 10 ++++++++++ packages/dev/docs/pages/react-aria/ssr.mdx | 7 ++++++- packages/dev/docs/pages/react-spectrum/ssr.mdx | 3 +++ 5 files changed, 48 insertions(+), 1 deletion(-) create mode 100644 packages/@react-aria/ssr/test/SSRProvider.ssr.test.js diff --git a/packages/@react-aria/ssr/src/SSRProvider.tsx b/packages/@react-aria/ssr/src/SSRProvider.tsx index ea466a37335..bfe0aec31eb 100644 --- a/packages/@react-aria/ssr/src/SSRProvider.tsx +++ b/packages/@react-aria/ssr/src/SSRProvider.tsx @@ -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, diff --git a/packages/@react-aria/ssr/test/SSRProvider.ssr.test.js b/packages/@react-aria/ssr/test/SSRProvider.ssr.test.js new file mode 100644 index 00000000000..4274725327c --- /dev/null +++ b/packages/@react-aria/ssr/test/SSRProvider.ssr.test.js @@ -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 = () =>
; + + `); + }); +}); diff --git a/packages/@react-aria/ssr/test/SSRProvider.test.js b/packages/@react-aria/ssr/test/SSRProvider.test.js index 85538d02853..7d7cf8d7ccc 100644 --- a/packages/@react-aria/ssr/test/SSRProvider.test.js +++ b/packages/@react-aria/ssr/test/SSRProvider.test.js @@ -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( @@ -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( diff --git a/packages/dev/docs/pages/react-aria/ssr.mdx b/packages/dev/docs/pages/react-aria/ssr.mdx index 2c464f50279..5184ec30e56 100644 --- a/packages/dev/docs/pages/react-aria/ssr.mdx +++ b/packages/dev/docs/pages/react-aria/ssr.mdx @@ -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. + +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. 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. diff --git a/packages/dev/docs/pages/react-spectrum/ssr.mdx b/packages/dev/docs/pages/react-spectrum/ssr.mdx index 7df26a60773..e6eb9d46761 100644 --- a/packages/dev/docs/pages/react-spectrum/ssr.mdx +++ b/packages/dev/docs/pages/react-spectrum/ssr.mdx @@ -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. From 43e54819769025c58abc965a5381cccf9d3cd217 Mon Sep 17 00:00:00 2001 From: ciffelia Date: Thu, 13 Oct 2022 15:51:02 +0900 Subject: [PATCH 2/3] Update docs Co-authored-by: Robert Snow --- packages/dev/docs/pages/react-aria/ssr.mdx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/dev/docs/pages/react-aria/ssr.mdx b/packages/dev/docs/pages/react-aria/ssr.mdx index 5184ec30e56..87c1afc2898 100644 --- a/packages/dev/docs/pages/react-aria/ssr.mdx +++ b/packages/dev/docs/pages/react-aria/ssr.mdx @@ -44,9 +44,9 @@ When strict mode is enabled on React 16 or 17, it generates different IDs betwee ## Automatic ID Generation -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. +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 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. +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. From 925383e21a56ebd405960fd4e1ac023c25b36e43 Mon Sep 17 00:00:00 2001 From: Ciffelia Date: Thu, 13 Oct 2022 16:19:43 +0900 Subject: [PATCH 3/3] Update docs - remove implementation details - mention `identifierPrefix` --- packages/dev/docs/pages/react-aria/ssr.mdx | 11 ++++++++--- packages/dev/docs/pages/react-spectrum/ssr.mdx | 2 +- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/packages/dev/docs/pages/react-aria/ssr.mdx b/packages/dev/docs/pages/react-aria/ssr.mdx index 87c1afc2898..71f10aa05d8 100644 --- a/packages/dev/docs/pages/react-aria/ssr.mdx +++ b/packages/dev/docs/pages/react-aria/ssr.mdx @@ -44,16 +44,21 @@ When strict mode is enabled on React 16 or 17, it generates different IDs betwee ## Automatic ID Generation +### 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'; diff --git a/packages/dev/docs/pages/react-spectrum/ssr.mdx b/packages/dev/docs/pages/react-spectrum/ssr.mdx index e6eb9d46761..e1336833600 100644 --- a/packages/dev/docs/pages/react-spectrum/ssr.mdx +++ b/packages/dev/docs/pages/react-spectrum/ssr.mdx @@ -45,7 +45,7 @@ Wrapping your application in an `SSRProvider` ensures that the HTML generated on 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. +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.