diff --git a/packages/react-art/src/ReactARTHostConfig.js b/packages/react-art/src/ReactARTHostConfig.js
index 7086dcccb5f0c..b2568e3d4c8a0 100644
--- a/packages/react-art/src/ReactARTHostConfig.js
+++ b/packages/react-art/src/ReactARTHostConfig.js
@@ -489,10 +489,6 @@ export function makeClientIdInDEV(warnOnAccessInDEV: () => void): OpaqueIDType {
throw new Error('Not yet implemented');
}
-export function makeServerId(): OpaqueIDType {
- throw new Error('Not yet implemented');
-}
-
export function beforeActiveInstanceBlur() {
// noop
}
diff --git a/packages/react-dom/src/__tests__/ReactDOMServerIntegrationHooks-test.js b/packages/react-dom/src/__tests__/ReactDOMServerIntegrationHooks-test.js
index afd366aaa2036..6c96f0a169e3c 100644
--- a/packages/react-dom/src/__tests__/ReactDOMServerIntegrationHooks-test.js
+++ b/packages/react-dom/src/__tests__/ReactDOMServerIntegrationHooks-test.js
@@ -1004,6 +1004,162 @@ describe('ReactDOMServerHooks', () => {
);
});
+ it('useOpaqueIdentifier identifierPrefix 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(, {
+ identifierPrefix: 'one',
+ });
+
+ const containerTwo = document.createElement('div');
+ document.body.append(containerTwo);
+
+ containerTwo.innerHTML = ReactDOMServer.renderToString(, {
+ identifierPrefix: '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 identifierPrefix works for multiple reads on a streaming server renderer', async () => {
+ function ChildTwo() {
+ const id = useOpaqueIdentifier();
+
+ return Child Two
;
+ }
+
+ function App() {
+ const id = useOpaqueIdentifier();
+
+ return (
+ <>
+ Child One
+
+ Aria One
+ >
+ );
+ }
+
+ const container = document.createElement('div');
+ document.body.append(container);
+
+ const streamOne = ReactDOMServer.renderToNodeStream(, {
+ identifierPrefix: 'one',
+ }).setEncoding('utf8');
+ const streamTwo = ReactDOMServer.renderToNodeStream(, {
+ identifierPrefix: 'two',
+ }).setEncoding('utf8');
+
+ const containerOne = document.createElement('div');
+ const containerTwo = document.createElement('div');
+
+ streamOne._read(10);
+ streamTwo._read(10);
+
+ containerOne.innerHTML = streamOne.read();
+ containerTwo.innerHTML = streamTwo.read();
+
+ expect(containerOne.children[0].getAttribute('id')).not.toEqual(
+ containerOne.children[1].getAttribute('id'),
+ );
+ expect(containerTwo.children[0].getAttribute('id')).not.toEqual(
+ containerTwo.children[1].getAttribute('id'),
+ );
+ expect(containerOne.children[0].getAttribute('id')).not.toEqual(
+ containerTwo.children[0].getAttribute('id'),
+ );
+ expect(
+ containerOne.children[0].getAttribute('id').includes('one'),
+ ).toBe(true);
+ expect(
+ containerOne.children[1].getAttribute('id').includes('one'),
+ ).toBe(true);
+ expect(
+ containerTwo.children[0].getAttribute('id').includes('two'),
+ ).toBe(true);
+ expect(
+ containerTwo.children[1].getAttribute('id').includes('two'),
+ ).toBe(true);
+
+ expect(containerOne.children[1].getAttribute('id')).not.toEqual(
+ containerTwo.children[1].getAttribute('id'),
+ );
+ expect(containerOne.children[0].getAttribute('id')).toEqual(
+ containerOne.children[2].getAttribute('aria-labelledby'),
+ );
+ expect(containerTwo.children[0].getAttribute('id')).toEqual(
+ containerTwo.children[2].getAttribute('aria-labelledby'),
+ );
+ });
+
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 dd7ac44686a34..68e339d1ca51f 100644
--- a/packages/react-dom/src/client/ReactDOMHostConfig.js
+++ b/packages/react-dom/src/client/ReactDOMHostConfig.js
@@ -1078,11 +1078,6 @@ export function makeClientIdInDEV(warnOnAccessInDEV: () => void): OpaqueIDType {
};
}
-let serverId: number = 0;
-export function makeServerId(): OpaqueIDType {
- return 'R:' + (serverId++).toString(36);
-}
-
export function isOpaqueHydratingObject(value: mixed): boolean {
return (
value !== null &&
diff --git a/packages/react-dom/src/server/ReactDOMNodeStreamRenderer.js b/packages/react-dom/src/server/ReactDOMNodeStreamRenderer.js
index 22ccd73853990..6b032556db4d3 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 {ServerOptions} from './ReactPartialRenderer';
import {Readable} from 'stream';
@@ -11,11 +12,15 @@ import ReactPartialRenderer from './ReactPartialRenderer';
// This is a Readable Node.js stream which wraps the ReactDOMPartialRenderer.
class ReactMarkupReadableStream extends Readable {
- constructor(element, makeStaticMarkup) {
+ constructor(element, makeStaticMarkup, options) {
// Calls the stream.Readable(options) constructor. Consider exposing built-in
// features like highWaterMark in the future.
super({});
- this.partialRenderer = new ReactPartialRenderer(element, makeStaticMarkup);
+ this.partialRenderer = new ReactPartialRenderer(
+ element,
+ makeStaticMarkup,
+ options,
+ );
}
_destroy(err, callback) {
@@ -36,8 +41,8 @@ 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?: ServerOptions) {
+ return new ReactMarkupReadableStream(element, false, options);
}
/**
@@ -45,6 +50,6 @@ 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?: ServerOptions) {
+ 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..f2dbb68648aa7 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 {ServerOptions} from './ReactPartialRenderer';
import ReactPartialRenderer from './ReactPartialRenderer';
/**
@@ -12,8 +13,8 @@ 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?: ServerOptions) {
+ const renderer = new ReactPartialRenderer(element, false, options);
try {
const markup = renderer.read(Infinity);
return markup;
@@ -27,8 +28,8 @@ 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?: ServerOptions) {
+ const renderer = new ReactPartialRenderer(element, true, options);
try {
const markup = renderer.read(Infinity);
return markup;
diff --git a/packages/react-dom/src/server/ReactPartialRenderer.js b/packages/react-dom/src/server/ReactPartialRenderer.js
index 5292796eb3df4..4615bd8e280db 100644
--- a/packages/react-dom/src/server/ReactPartialRenderer.js
+++ b/packages/react-dom/src/server/ReactPartialRenderer.js
@@ -60,8 +60,8 @@ import {
prepareToUseHooks,
finishHooks,
Dispatcher,
- currentThreadID,
- setCurrentThreadID,
+ currentPartialRenderer,
+ setCurrentPartialRenderer,
} from './ReactPartialRendererHooks';
import {
Namespaces,
@@ -79,6 +79,10 @@ import {validateProperties as validateARIAProperties} from '../shared/ReactDOMIn
import {validateProperties as validateInputProperties} from '../shared/ReactDOMNullInputValuePropHook';
import {validateProperties as validateUnknownProperties} from '../shared/ReactDOMUnknownPropertyHook';
+export type ServerOptions = {
+ identifierPrefix?: string,
+};
+
// Based on reading the React.Children implementation. TODO: type this somewhere?
type ReactNode = string | number | ReactElement;
type FlatReactChildren = Array;
@@ -726,7 +730,14 @@ class ReactDOMServerRenderer {
contextValueStack: Array;
contextProviderStack: ?Array>; // DEV-only
- constructor(children: mixed, makeStaticMarkup: boolean) {
+ uniqueID: number;
+ identifierPrefix: string;
+
+ constructor(
+ children: mixed,
+ makeStaticMarkup: boolean,
+ options?: ServerOptions,
+ ) {
const flatChildren = flattenTopLevelChildren(children);
const topFrame: Frame = {
@@ -754,6 +765,11 @@ class ReactDOMServerRenderer {
this.contextIndex = -1;
this.contextStack = [];
this.contextValueStack = [];
+
+ // useOpaqueIdentifier ID
+ this.uniqueID = 0;
+ this.identifierPrefix = (options && options.identifierPrefix) || '';
+
if (__DEV__) {
this.contextProviderStack = [];
}
@@ -837,8 +853,8 @@ class ReactDOMServerRenderer {
return null;
}
- const prevThreadID = currentThreadID;
- setCurrentThreadID(this.threadID);
+ const prevPartialRenderer = currentPartialRenderer;
+ setCurrentPartialRenderer(this);
const prevDispatcher = ReactCurrentDispatcher.current;
ReactCurrentDispatcher.current = Dispatcher;
try {
@@ -935,7 +951,7 @@ class ReactDOMServerRenderer {
return out[0];
} finally {
ReactCurrentDispatcher.current = prevDispatcher;
- setCurrentThreadID(prevThreadID);
+ setCurrentPartialRenderer(prevPartialRenderer);
}
}
diff --git a/packages/react-dom/src/server/ReactPartialRendererHooks.js b/packages/react-dom/src/server/ReactPartialRendererHooks.js
index 23e5af5b0c7da..c9153a0af54b5 100644
--- a/packages/react-dom/src/server/ReactPartialRendererHooks.js
+++ b/packages/react-dom/src/server/ReactPartialRendererHooks.js
@@ -8,8 +8,6 @@
*/
import type {Dispatcher as DispatcherType} from 'react-reconciler/src/ReactInternalTypes';
-import type {ThreadID} from './ReactThreadIDAllocator';
-import type {OpaqueIDType} from 'react-reconciler/src/ReactFiberHostConfig';
import type {
MutableSource,
@@ -19,9 +17,9 @@ import type {
ReactEventResponderListener,
} from 'shared/ReactTypes';
import type {SuspenseConfig} from 'react-reconciler/src/ReactFiberSuspenseConfig';
+import type PartialRenderer from './ReactPartialRenderer';
import {validateContextBounds} from './ReactPartialRendererContext';
-import {makeServerId} from '../client/ReactDOMHostConfig';
import invariant from 'shared/invariant';
import is from 'shared/objectIs';
@@ -49,6 +47,8 @@ type TimeoutConfig = {|
timeoutMs: number,
|};
+type OpaqueIDType = string;
+
let currentlyRenderingComponent: Object | null = null;
let firstWorkInProgressHook: Hook | null = null;
let workInProgressHook: Hook | null = null;
@@ -226,7 +226,7 @@ function readContext(
context: ReactContext,
observedBits: void | number | boolean,
): T {
- const threadID = currentThreadID;
+ const threadID = currentPartialRenderer.threadID;
validateContextBounds(context, threadID);
if (__DEV__) {
if (isInHookUserCodeInDev) {
@@ -249,7 +249,7 @@ function useContext(
currentHookNameInDev = 'useContext';
}
resolveCurrentlyRenderingComponent();
- const threadID = currentThreadID;
+ const threadID = currentPartialRenderer.threadID;
validateContextBounds(context, threadID);
return context[threadID];
}
@@ -495,15 +495,18 @@ function useTransition(
}
function useOpaqueIdentifier(): OpaqueIDType {
- return makeServerId();
+ return (
+ (currentPartialRenderer.identifierPrefix || '') +
+ 'R:' +
+ (currentPartialRenderer.uniqueID++).toString(36)
+ );
}
function noop(): void {}
-export let currentThreadID: ThreadID = 0;
-
-export function setCurrentThreadID(threadID: ThreadID) {
- currentThreadID = threadID;
+export let currentPartialRenderer: PartialRenderer = (null: any);
+export function setCurrentPartialRenderer(renderer: PartialRenderer) {
+ currentPartialRenderer = renderer;
}
export const Dispatcher: DispatcherType = {
diff --git a/packages/react-native-renderer/src/ReactFabricHostConfig.js b/packages/react-native-renderer/src/ReactFabricHostConfig.js
index fa3b25103ffab..bb7b2ea8888b4 100644
--- a/packages/react-native-renderer/src/ReactFabricHostConfig.js
+++ b/packages/react-native-renderer/src/ReactFabricHostConfig.js
@@ -503,10 +503,6 @@ export function makeClientIdInDEV(warnOnAccessInDEV: () => void): OpaqueIDType {
throw new Error('Not yet implemented');
}
-export function makeServerId(): OpaqueIDType {
- throw new Error('Not yet implemented');
-}
-
export function beforeActiveInstanceBlur() {
// noop
}
diff --git a/packages/react-native-renderer/src/ReactNativeHostConfig.js b/packages/react-native-renderer/src/ReactNativeHostConfig.js
index fd9b08cf4b0a9..ff1f64b933f80 100644
--- a/packages/react-native-renderer/src/ReactNativeHostConfig.js
+++ b/packages/react-native-renderer/src/ReactNativeHostConfig.js
@@ -551,10 +551,6 @@ export function makeClientIdInDEV(warnOnAccessInDEV: () => void): OpaqueIDType {
throw new Error('Not yet implemented');
}
-export function makeServerId(): OpaqueIDType {
- throw new Error('Not yet implemented');
-}
-
export function beforeActiveInstanceBlur() {
// noop
}
diff --git a/packages/react-reconciler/src/ReactInternalTypes.js b/packages/react-reconciler/src/ReactInternalTypes.js
index edca38409314f..4b4f5ab64464a 100644
--- a/packages/react-reconciler/src/ReactInternalTypes.js
+++ b/packages/react-reconciler/src/ReactInternalTypes.js
@@ -29,7 +29,6 @@ import type {RootTag} from './ReactRootTags';
import type {TimeoutHandle, NoTimeout} from './ReactFiberHostConfig';
import type {Wakeable} from 'shared/ReactTypes';
import type {Interaction} from 'scheduler/src/Tracing';
-import type {OpaqueIDType} from 'react-reconciler/src/ReactFiberHostConfig';
import type {SuspenseConfig, TimeoutConfig} from './ReactFiberSuspenseConfig';
export type ReactPriorityLevel = 99 | 98 | 97 | 96 | 95 | 90;
@@ -364,5 +363,5 @@ export type Dispatcher = {|
getSnapshot: MutableSourceGetSnapshotFn,
subscribe: MutableSourceSubscribeFn,
): Snapshot,
- useOpaqueIdentifier(): OpaqueIDType | void,
+ useOpaqueIdentifier(): any,
|};
diff --git a/packages/react-reconciler/src/forks/ReactFiberHostConfig.custom.js b/packages/react-reconciler/src/forks/ReactFiberHostConfig.custom.js
index 6bc1c38dafce9..6f3d625b91274 100644
--- a/packages/react-reconciler/src/forks/ReactFiberHostConfig.custom.js
+++ b/packages/react-reconciler/src/forks/ReactFiberHostConfig.custom.js
@@ -80,7 +80,6 @@ export const makeOpaqueHydratingObject =
$$$hostConfig.makeOpaqueHydratingObject;
export const makeClientId = $$$hostConfig.makeClientId;
export const makeClientIdInDEV = $$$hostConfig.makeClientIdInDEV;
-export const makeServerId = $$$hostConfig.makeServerId;
export const beforeActiveInstanceBlur = $$$hostConfig.beforeActiveInstanceBlur;
export const afterActiveInstanceBlur = $$$hostConfig.afterActiveInstanceBlur;
export const preparePortalMount = $$$hostConfig.preparePortalMount;
diff --git a/packages/react-test-renderer/src/ReactTestHostConfig.js b/packages/react-test-renderer/src/ReactTestHostConfig.js
index 296bdcf56db08..ebc004e5295a6 100644
--- a/packages/react-test-renderer/src/ReactTestHostConfig.js
+++ b/packages/react-test-renderer/src/ReactTestHostConfig.js
@@ -405,11 +405,6 @@ export function makeClientIdInDEV(warnOnAccessInDEV: () => void): OpaqueIDType {
};
}
-let serverId: number = 0;
-export function makeServerId(): OpaqueIDType {
- return 's_' + (serverId++).toString(36);
-}
-
export function isOpaqueHydratingObject(value: mixed): boolean {
return (
value !== null &&