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..71f10aa05d8 100644 --- a/packages/dev/docs/pages/react-aria/ssr.mdx +++ b/packages/dev/docs/pages/react-aria/ssr.mdx @@ -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'; diff --git a/packages/dev/docs/pages/react-spectrum/ssr.mdx b/packages/dev/docs/pages/react-spectrum/ssr.mdx index 7df26a60773..e1336833600 100644 --- a/packages/dev/docs/pages/react-spectrum/ssr.mdx +++ b/packages/dev/docs/pages/react-spectrum/ssr.mdx @@ -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.