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 {