diff --git a/packages/react-art/src/ReactARTHostConfig.js b/packages/react-art/src/ReactARTHostConfig.js index e0e883c4b21b5..5ea20502ef574 100644 --- a/packages/react-art/src/ReactARTHostConfig.js +++ b/packages/react-art/src/ReactARTHostConfig.js @@ -488,7 +488,7 @@ export function makeClientIdInDEV(warnOnAccessInDEV: () => void): OpaqueIDType { throw new Error('Not yet implemented'); } -export function makeServerId(): OpaqueIDType { +export function makeServerId(prefix: ?string, serverId: number): OpaqueIDType { throw new Error('Not yet implemented'); } diff --git a/packages/react-dom/src/__tests__/ReactDOMServerIntegrationHooks-test.internal.js b/packages/react-dom/src/__tests__/ReactDOMServerIntegrationHooks-test.internal.js index a910b011f0507..a1929446e4e69 100644 --- a/packages/react-dom/src/__tests__/ReactDOMServerIntegrationHooks-test.internal.js +++ b/packages/react-dom/src/__tests__/ReactDOMServerIntegrationHooks-test.internal.js @@ -1008,6 +1008,91 @@ describe('ReactDOMServerHooks', () => { ); }); + it('useOpaqueIdentifier prefix works for server renderer and does not clash', async () => { + function ChildTwo({id}) { + return
Child Three
; + } + function App() { + const id = useOpaqueIdentifier(); + const idTwo = useOpaqueIdentifier(); + + return ( +
+
Chid One
+ +
Child Three
+
Child Four
+
+ ); + } + + const containerOne = document.createElement('div'); + document.body.append(containerOne); + + containerOne.innerHTML = ReactDOMServer.renderToString(, { + prefix: 'one', + }); + + const containerTwo = document.createElement('div'); + document.body.append(containerTwo); + + containerTwo.innerHTML = ReactDOMServer.renderToString(, { + prefix: 'two', + }); + + expect(document.body.children.length).toEqual(2); + const childOne = document.body.children[0]; + const childTwo = document.body.children[1]; + + expect( + childOne.children[0].children[0].getAttribute('aria-labelledby'), + ).toEqual(childOne.children[0].children[1].getAttribute('id')); + expect( + childOne.children[0].children[2].getAttribute('aria-labelledby'), + ).toEqual(childOne.children[0].children[3].getAttribute('id')); + + expect( + childOne.children[0].children[0].getAttribute('aria-labelledby'), + ).not.toEqual( + childOne.children[0].children[2].getAttribute('aria-labelledby'), + ); + + expect( + childOne.children[0].children[0] + .getAttribute('aria-labelledby') + .startsWith('one'), + ).toBe(true); + expect( + childOne.children[0].children[2] + .getAttribute('aria-labelledby') + .includes('one'), + ).toBe(true); + + expect( + childTwo.children[0].children[0].getAttribute('aria-labelledby'), + ).toEqual(childTwo.children[0].children[1].getAttribute('id')); + expect( + childTwo.children[0].children[2].getAttribute('aria-labelledby'), + ).toEqual(childTwo.children[0].children[3].getAttribute('id')); + + expect( + childTwo.children[0].children[0].getAttribute('aria-labelledby'), + ).not.toEqual( + childTwo.children[0].children[2].getAttribute('aria-labelledby'), + ); + + expect( + childTwo.children[0].children[0] + .getAttribute('aria-labelledby') + .startsWith('two'), + ).toBe(true); + expect( + childTwo.children[0].children[2] + .getAttribute('aria-labelledby') + .startsWith('two'), + ).toBe(true); + }); + it('useOpaqueIdentifier: IDs match when, after hydration, a new component that uses the ID is rendered', async () => { let _setShowDiv; function App() { diff --git a/packages/react-dom/src/client/ReactDOMHostConfig.js b/packages/react-dom/src/client/ReactDOMHostConfig.js index b8b0050de7a59..d9c5b6b391ddd 100644 --- a/packages/react-dom/src/client/ReactDOMHostConfig.js +++ b/packages/react-dom/src/client/ReactDOMHostConfig.js @@ -1174,9 +1174,8 @@ export function makeClientIdInDEV(warnOnAccessInDEV: () => void): OpaqueIDType { }; } -let serverId: number = 0; -export function makeServerId(): OpaqueIDType { - return 'R:' + (serverId++).toString(36); +export function makeServerId(prefix: ?string, serverId: number): OpaqueIDType { + return (prefix || '') + 'R:' + serverId.toString(36); } export function isOpaqueHydratingObject(value: mixed): boolean { diff --git a/packages/react-dom/src/server/ReactDOMNodeStreamRenderer.js b/packages/react-dom/src/server/ReactDOMNodeStreamRenderer.js index 22ccd73853990..0e76fc1b59256 100644 --- a/packages/react-dom/src/server/ReactDOMNodeStreamRenderer.js +++ b/packages/react-dom/src/server/ReactDOMNodeStreamRenderer.js @@ -4,6 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ +import type {PartialRendererOptions} from './ReactPartialRenderer'; import {Readable} from 'stream'; @@ -36,8 +37,11 @@ class ReactMarkupReadableStream extends Readable { * server. * See https://reactjs.org/docs/react-dom-server.html#rendertonodestream */ -export function renderToNodeStream(element) { - return new ReactMarkupReadableStream(element, false); +export function renderToNodeStream( + element, + options: PartialRendererOptions | void, +) { + return new ReactMarkupReadableStream(element, false, options); } /** @@ -45,6 +49,9 @@ export function renderToNodeStream(element) { * such as data-react-id that React uses internally. * See https://reactjs.org/docs/react-dom-server.html#rendertostaticnodestream */ -export function renderToStaticNodeStream(element) { - return new ReactMarkupReadableStream(element, true); +export function renderToStaticNodeStream( + element, + options: PartialRendererOptions | void, +) { + return new ReactMarkupReadableStream(element, true, options); } diff --git a/packages/react-dom/src/server/ReactDOMStringRenderer.js b/packages/react-dom/src/server/ReactDOMStringRenderer.js index 1afc65acd6d5c..d530fb628e356 100644 --- a/packages/react-dom/src/server/ReactDOMStringRenderer.js +++ b/packages/react-dom/src/server/ReactDOMStringRenderer.js @@ -5,6 +5,7 @@ * LICENSE file in the root directory of this source tree. */ +import type {PartialRendererOptions} from './ReactPartialRenderer'; import ReactPartialRenderer from './ReactPartialRenderer'; /** @@ -12,8 +13,11 @@ import ReactPartialRenderer from './ReactPartialRenderer'; * server. * See https://reactjs.org/docs/react-dom-server.html#rendertostring */ -export function renderToString(element) { - const renderer = new ReactPartialRenderer(element, false); +export function renderToString( + element, + options?: void | PartialRendererOptions, +) { + const renderer = new ReactPartialRenderer(element, false, options); try { const markup = renderer.read(Infinity); return markup; @@ -27,10 +31,13 @@ export function renderToString(element) { * such as data-react-id that React uses internally. * See https://reactjs.org/docs/react-dom-server.html#rendertostaticmarkup */ -export function renderToStaticMarkup(element) { - const renderer = new ReactPartialRenderer(element, true); +export function renderToStaticMarkup( + element, + options?: void | PartialRendererOptions, +) { + const renderer = new ReactPartialRenderer(element, true, options); try { - const markup = renderer.read(Infinity); + const markup = renderer.read(Infinity, options); return markup; } finally { renderer.destroy(); diff --git a/packages/react-dom/src/server/ReactPartialRenderer.js b/packages/react-dom/src/server/ReactPartialRenderer.js index c1ff6d2bd35c4..0a12606e517d3 100644 --- a/packages/react-dom/src/server/ReactPartialRenderer.js +++ b/packages/react-dom/src/server/ReactPartialRenderer.js @@ -61,6 +61,10 @@ import { Dispatcher, currentThreadID, setCurrentThreadID, + currentUniqueID, + setCurrentUniqueID, + uniqueIDPrefix, + setCurrentUniqueIDPrefix, } from './ReactPartialRendererHooks'; import { Namespaces, @@ -78,6 +82,10 @@ import {validateProperties as validateARIAProperties} from '../shared/ReactDOMIn import {validateProperties as validateInputProperties} from '../shared/ReactDOMNullInputValuePropHook'; import {validateProperties as validateUnknownProperties} from '../shared/ReactDOMUnknownPropertyHook'; +export type PartialRendererOptions = { + prefix?: string, +}; + // Based on reading the React.Children implementation. TODO: type this somewhere? type ReactNode = string | number | ReactElement; type FlatReactChildren = Array; @@ -725,7 +733,14 @@ class ReactDOMServerRenderer { contextValueStack: Array; contextProviderStack: ?Array>; // DEV-only - constructor(children: mixed, makeStaticMarkup: boolean) { + uniqueID: number; + prefix: string; + + constructor( + children: mixed, + makeStaticMarkup: boolean, + options?: PartialRendererOptions | void, + ) { const flatChildren = flattenTopLevelChildren(children); const topFrame: Frame = { @@ -753,6 +768,11 @@ class ReactDOMServerRenderer { this.contextIndex = -1; this.contextStack = []; this.contextValueStack = []; + + // useOpaqueIdentifier ID + this.uniqueID = 0; + this.prefix = (options && options.prefix) || ''; + if (__DEV__) { this.contextProviderStack = []; } @@ -838,6 +858,10 @@ class ReactDOMServerRenderer { const prevThreadID = currentThreadID; setCurrentThreadID(this.threadID); + const prevUniqueID = currentUniqueID; + setCurrentUniqueID(this.uniqueID); + const prevUniquePrefix = uniqueIDPrefix; + setCurrentUniqueIDPrefix(this.prefix); const prevDispatcher = ReactCurrentDispatcher.current; ReactCurrentDispatcher.current = Dispatcher; try { @@ -935,6 +959,8 @@ class ReactDOMServerRenderer { } finally { ReactCurrentDispatcher.current = prevDispatcher; setCurrentThreadID(prevThreadID); + setCurrentUniqueID(prevUniqueID); + setCurrentUniqueIDPrefix(prevUniquePrefix); } } diff --git a/packages/react-dom/src/server/ReactPartialRendererHooks.js b/packages/react-dom/src/server/ReactPartialRendererHooks.js index fe5c11c4a9994..7ca8d770211f3 100644 --- a/packages/react-dom/src/server/ReactPartialRendererHooks.js +++ b/packages/react-dom/src/server/ReactPartialRendererHooks.js @@ -495,8 +495,19 @@ function useTransition( return [startTransition, false]; } +export let currentUniqueID: number = 0; +export let uniqueIDPrefix: string = ''; + +export function setCurrentUniqueIDPrefix(prefix: string) { + uniqueIDPrefix = prefix; +} + +export function setCurrentUniqueID(id: number) { + currentUniqueID = id; +} + function useOpaqueIdentifier(): OpaqueIDType { - return makeServerId(); + return makeServerId(uniqueIDPrefix, currentUniqueID++); } function useEvent(event: any): ReactDOMListenerMap { diff --git a/packages/react-native-renderer/src/ReactFabricHostConfig.js b/packages/react-native-renderer/src/ReactFabricHostConfig.js index 91aa98398114e..fbf830ee93f0d 100644 --- a/packages/react-native-renderer/src/ReactFabricHostConfig.js +++ b/packages/react-native-renderer/src/ReactFabricHostConfig.js @@ -505,7 +505,7 @@ export function makeClientIdInDEV(warnOnAccessInDEV: () => void): OpaqueIDType { throw new Error('Not yet implemented'); } -export function makeServerId(): OpaqueIDType { +export function makeServerId(prefix: ?string, serverId: number): OpaqueIDType { throw new Error('Not yet implemented'); } diff --git a/packages/react-native-renderer/src/ReactNativeHostConfig.js b/packages/react-native-renderer/src/ReactNativeHostConfig.js index a6ed4de72c392..bf81b81c6dc6f 100644 --- a/packages/react-native-renderer/src/ReactNativeHostConfig.js +++ b/packages/react-native-renderer/src/ReactNativeHostConfig.js @@ -554,7 +554,7 @@ export function makeClientIdInDEV(warnOnAccessInDEV: () => void): OpaqueIDType { throw new Error('Not yet implemented'); } -export function makeServerId(): OpaqueIDType { +export function makeServerId(prefix: ?string, serverId: number): OpaqueIDType { throw new Error('Not yet implemented'); } diff --git a/packages/react-test-renderer/src/ReactTestHostConfig.js b/packages/react-test-renderer/src/ReactTestHostConfig.js index c3eda48d2a4ea..3754a9339de59 100644 --- a/packages/react-test-renderer/src/ReactTestHostConfig.js +++ b/packages/react-test-renderer/src/ReactTestHostConfig.js @@ -407,9 +407,8 @@ export function makeClientIdInDEV(warnOnAccessInDEV: () => void): OpaqueIDType { }; } -let serverId: number = 0; -export function makeServerId(): OpaqueIDType { - return 's_' + (serverId++).toString(36); +export function makeServerId(prefix: ?string, serverId: number): OpaqueIDType { + return (prefix || '') + 's_' + serverId.toString(36); } export function isOpaqueHydratingObject(value: mixed): boolean {