From 1134a116a46584c449fdb0459187854e660e4254 Mon Sep 17 00:00:00 2001 From: Luna Ruan Date: Fri, 10 Apr 2020 20:11:24 -0700 Subject: [PATCH] add new IDs for each each server renderer instance and prefixes to distinguish between each server render --- packages/react-art/src/ReactARTHostConfig.js | 4 - .../ReactDOMServerIntegrationHooks-test.js | 156 ++++++++++++++++++ .../src/client/ReactDOMHostConfig.js | 5 - .../src/server/ReactDOMNodeStreamRenderer.js | 17 +- .../src/server/ReactDOMStringRenderer.js | 9 +- .../src/server/ReactPartialRenderer.js | 28 +++- .../src/server/ReactPartialRendererHooks.js | 23 +-- .../src/ReactFabricHostConfig.js | 4 - .../src/ReactNativeHostConfig.js | 4 - .../src/ReactInternalTypes.js | 3 +- .../src/forks/ReactFiberHostConfig.custom.js | 1 - .../src/ReactTestHostConfig.js | 5 - 12 files changed, 208 insertions(+), 51 deletions(-) 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 &&