From aca900f8d83da8de8849a2e07dab6602e171a8e7 Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Wed, 1 Dec 2021 20:18:17 -0500 Subject: [PATCH] Implement identifierPrefix option for useId When an `identifierPrefix` option is given, React will add it to the beginning of ids generated by `useId`. The main use case is to avoid conflicts when there are multiple React roots on a single page. The server API already supported an `identifierPrefix` option. It's not only used by `useId`, but also for React-generated ids that are used to stitch together chunks of HTML, among other things. I added a corresponding option to the client. You must pass the same prefix option to both the server and client. Eventually we may make this automatic by sending the prefix from the server as part of the HTML stream. --- packages/react-art/src/ReactART.js | 10 ++- .../src/__tests__/ReactDOMUseId-test.js | 64 ++++++++++++++++++- .../react-dom/src/client/ReactDOMLegacy.js | 1 + packages/react-dom/src/client/ReactDOMRoot.js | 53 ++++++++++----- .../src/server/ReactDOMServerFormatConfig.js | 21 ++++++ .../ReactDOMServerLegacyFormatConfig.js | 3 + .../react-native-renderer/src/ReactFabric.js | 1 + .../src/ReactNativeRenderer.js | 10 ++- .../server/ReactNativeServerFormatConfig.js | 8 +++ .../src/createReactNoop.js | 4 ++ .../src/ReactFiberHooks.new.js | 12 +++- .../src/ReactFiberHooks.old.js | 12 +++- .../src/ReactFiberReconciler.new.js | 2 + .../src/ReactFiberReconciler.old.js | 2 + .../src/ReactFiberRoot.new.js | 12 +++- .../src/ReactFiberRoot.old.js | 12 +++- .../src/ReactInternalTypes.js | 7 ++ .../ReactFiberHostContext-test.internal.js | 4 ++ packages/react-server/src/ReactFizzHooks.js | 19 +++--- .../src/ReactTestRenderer.js | 1 + 20 files changed, 222 insertions(+), 36 deletions(-) diff --git a/packages/react-art/src/ReactART.js b/packages/react-art/src/ReactART.js index a29f9cd23714d2..9d1b6a16c20384 100644 --- a/packages/react-art/src/ReactART.js +++ b/packages/react-art/src/ReactART.js @@ -66,7 +66,15 @@ class Surface extends React.Component { this._surface = Mode.Surface(+width, +height, this._tagRef); - this._mountNode = createContainer(this._surface, LegacyRoot, false, null); + this._mountNode = createContainer( + this._surface, + LegacyRoot, + false, + null, + false, + false, + '', + ); updateContainer(this.props.children, this._mountNode, this); } diff --git a/packages/react-dom/src/__tests__/ReactDOMUseId-test.js b/packages/react-dom/src/__tests__/ReactDOMUseId-test.js index dd7bbaa8b41fac..036c138d5e97a5 100644 --- a/packages/react-dom/src/__tests__/ReactDOMUseId-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMUseId-test.js @@ -94,7 +94,7 @@ describe('useId', () => { function normalizeTreeIdForTesting(id) { const [serverClientPrefix, base32, hookIndex] = id.split(':'); - if (serverClientPrefix === 'r') { + if (serverClientPrefix.endsWith('r')) { // Client ids aren't stable. For testing purposes, strip out the counter. return ( 'CLIENT_GENERATED_ID' + @@ -569,4 +569,66 @@ describe('useId', () => { // Should have hydrated successfully expect(span.current).toBe(dehydratedSpan); }); + + test('identifierPrefix option', async () => { + function Child() { + const id = useId(); + return
{id}
; + } + + function App({showMore}) { + return ( + <> + + + {showMore && } + + ); + } + + await serverAct(async () => { + const {pipe} = ReactDOMFizzServer.renderToPipeableStream(, { + identifierPrefix: 'custom-prefix-', + }); + pipe(writable); + }); + let root; + await clientAct(async () => { + root = ReactDOM.hydrateRoot(container, , { + identifierPrefix: 'custom-prefix-', + }); + }); + expect(container).toMatchInlineSnapshot(` +
+
+ custom-prefix-R:1 +
+
+ custom-prefix-R:2 +
+
+ `); + + // Mount a new, client-only id + await clientAct(async () => { + root.render(); + }); + expect(container).toMatchInlineSnapshot(` +
+
+ custom-prefix-R:1 +
+
+ custom-prefix-R:2 +
+
+ custom-prefix-r:0 +
+
+ `); + }); }); diff --git a/packages/react-dom/src/client/ReactDOMLegacy.js b/packages/react-dom/src/client/ReactDOMLegacy.js index ac12f18bee5095..e3def8459e754b 100644 --- a/packages/react-dom/src/client/ReactDOMLegacy.js +++ b/packages/react-dom/src/client/ReactDOMLegacy.js @@ -121,6 +121,7 @@ function legacyCreateRootFromDOMContainer( null, // hydrationCallbacks false, // isStrictMode false, // concurrentUpdatesByDefaultOverride, + '', // identiferPrefix ); markContainerAsRoot(root.current, container); diff --git a/packages/react-dom/src/client/ReactDOMRoot.js b/packages/react-dom/src/client/ReactDOMRoot.js index d8794eec5bb1b8..3c49a15fc14dcd 100644 --- a/packages/react-dom/src/client/ReactDOMRoot.js +++ b/packages/react-dom/src/client/ReactDOMRoot.js @@ -32,6 +32,7 @@ export type CreateRootOptions = { // END OF TODO unstable_strictMode?: boolean, unstable_concurrentUpdatesByDefault?: boolean, + identifierPrefix?: string, ... }; @@ -43,6 +44,7 @@ export type HydrateRootOptions = { // Options for all roots unstable_strictMode?: boolean, unstable_concurrentUpdatesByDefault?: boolean, + identifierPrefix?: string, ... }; @@ -158,13 +160,22 @@ export function createRoot( null; // END TODO - const isStrictMode = options != null && options.unstable_strictMode === true; - let concurrentUpdatesByDefaultOverride = null; - if (allowConcurrentByDefault) { - concurrentUpdatesByDefaultOverride = - options != null && options.unstable_concurrentUpdatesByDefault != null - ? options.unstable_concurrentUpdatesByDefault - : null; + let isStrictMode = false; + let concurrentUpdatesByDefaultOverride = false; + let identifierPrefix = ''; + if (options !== null && options !== undefined) { + if (options.unstable_strictMode === true) { + isStrictMode = true; + } + if ( + allowConcurrentByDefault && + options.unstable_concurrentUpdatesByDefault === true + ) { + concurrentUpdatesByDefaultOverride = true; + } + if (options.identifierPrefix !== undefined) { + identifierPrefix = options.identifierPrefix; + } } const root = createContainer( @@ -174,6 +185,7 @@ export function createRoot( hydrationCallbacks, isStrictMode, concurrentUpdatesByDefaultOverride, + identifierPrefix, ); markContainerAsRoot(root.current, container); @@ -217,15 +229,25 @@ export function hydrateRoot( // For now we reuse the whole bag of options since they contain // the hydration callbacks. const hydrationCallbacks = options != null ? options : null; + // TODO: Delete this option const mutableSources = (options != null && options.hydratedSources) || null; - const isStrictMode = options != null && options.unstable_strictMode === true; - - let concurrentUpdatesByDefaultOverride = null; - if (allowConcurrentByDefault) { - concurrentUpdatesByDefaultOverride = - options != null && options.unstable_concurrentUpdatesByDefault != null - ? options.unstable_concurrentUpdatesByDefault - : null; + + let isStrictMode = false; + let concurrentUpdatesByDefaultOverride = false; + let identifierPrefix = ''; + if (options !== null && options !== undefined) { + if (options.unstable_strictMode === true) { + isStrictMode = true; + } + if ( + allowConcurrentByDefault && + options.unstable_concurrentUpdatesByDefault === true + ) { + concurrentUpdatesByDefaultOverride = true; + } + if (options.identifierPrefix !== undefined) { + identifierPrefix = options.identifierPrefix; + } } const root = createContainer( @@ -235,6 +257,7 @@ export function hydrateRoot( hydrationCallbacks, isStrictMode, concurrentUpdatesByDefaultOverride, + identifierPrefix, ); markContainerAsRoot(root.current, container); // This can't be a comment node since hydration doesn't work on comment nodes anyway. diff --git a/packages/react-dom/src/server/ReactDOMServerFormatConfig.js b/packages/react-dom/src/server/ReactDOMServerFormatConfig.js index 65a10974b0196d..c4f8d89a8c9073 100644 --- a/packages/react-dom/src/server/ReactDOMServerFormatConfig.js +++ b/packages/react-dom/src/server/ReactDOMServerFormatConfig.js @@ -64,6 +64,7 @@ export type ResponseState = { placeholderPrefix: PrecomputedChunk, segmentPrefix: PrecomputedChunk, boundaryPrefix: string, + idPrefix: string, nextSuspenseID: number, sentCompleteSegmentFunction: boolean, sentCompleteBoundaryFunction: boolean, @@ -125,6 +126,7 @@ export function createResponseState( placeholderPrefix: stringToPrecomputedChunk(idPrefix + 'P:'), segmentPrefix: stringToPrecomputedChunk(idPrefix + 'S:'), boundaryPrefix: idPrefix + 'B:', + idPrefix: idPrefix + 'R:', nextSuspenseID: 0, sentCompleteSegmentFunction: false, sentCompleteBoundaryFunction: false, @@ -229,6 +231,25 @@ export function assignSuspenseBoundaryID( ); } +export function makeId( + responseState: ResponseState, + treeId: string, + localId: number, +): string { + const idPrefix = responseState.idPrefix; + + let id = idPrefix + treeId; + + // Unless this is the first id at this level, append a number at the end + // that represents the position of this useId hook among all the useId + // hooks for this fiber. + if (localId > 0) { + id += ':' + localId.toString(32); + } + + return id; +} + function encodeHTMLTextNode(text: string): string { return escapeTextForBrowser(text); } diff --git a/packages/react-dom/src/server/ReactDOMServerLegacyFormatConfig.js b/packages/react-dom/src/server/ReactDOMServerLegacyFormatConfig.js index 5b0798b737129b..c3d09f481fb62a 100644 --- a/packages/react-dom/src/server/ReactDOMServerLegacyFormatConfig.js +++ b/packages/react-dom/src/server/ReactDOMServerLegacyFormatConfig.js @@ -34,6 +34,7 @@ export type ResponseState = { placeholderPrefix: PrecomputedChunk, segmentPrefix: PrecomputedChunk, boundaryPrefix: string, + idPrefix: string, nextSuspenseID: number, sentCompleteSegmentFunction: boolean, sentCompleteBoundaryFunction: boolean, @@ -54,6 +55,7 @@ export function createResponseState( placeholderPrefix: responseState.placeholderPrefix, segmentPrefix: responseState.segmentPrefix, boundaryPrefix: responseState.boundaryPrefix, + idPrefix: responseState.idPrefix, nextSuspenseID: responseState.nextSuspenseID, sentCompleteSegmentFunction: responseState.sentCompleteSegmentFunction, sentCompleteBoundaryFunction: responseState.sentCompleteBoundaryFunction, @@ -79,6 +81,7 @@ export { getChildFormatContext, UNINITIALIZED_SUSPENSE_BOUNDARY_ID, assignSuspenseBoundaryID, + makeId, pushStartInstance, pushEndInstance, pushStartCompletedSuspenseBoundary, diff --git a/packages/react-native-renderer/src/ReactFabric.js b/packages/react-native-renderer/src/ReactFabric.js index 744c1c178397f9..bf7754d6099c23 100644 --- a/packages/react-native-renderer/src/ReactFabric.js +++ b/packages/react-native-renderer/src/ReactFabric.js @@ -213,6 +213,7 @@ function render( null, false, null, + '', ); roots.set(containerTag, root); } diff --git a/packages/react-native-renderer/src/ReactNativeRenderer.js b/packages/react-native-renderer/src/ReactNativeRenderer.js index a60683b47b2ee4..fb539d89968119 100644 --- a/packages/react-native-renderer/src/ReactNativeRenderer.js +++ b/packages/react-native-renderer/src/ReactNativeRenderer.js @@ -202,7 +202,15 @@ function render( if (!root) { // TODO (bvaughn): If we decide to keep the wrapper component, // We could create a wrapper for containerTag as well to reduce special casing. - root = createContainer(containerTag, LegacyRoot, false, null, false, null); + root = createContainer( + containerTag, + LegacyRoot, + false, + null, + false, + null, + '', + ); roots.set(containerTag, root); } updateContainer(element, root, null, callback); diff --git a/packages/react-native-renderer/src/server/ReactNativeServerFormatConfig.js b/packages/react-native-renderer/src/server/ReactNativeServerFormatConfig.js index 5b12d810bbe0fa..20084731b53330 100644 --- a/packages/react-native-renderer/src/server/ReactNativeServerFormatConfig.js +++ b/packages/react-native-renderer/src/server/ReactNativeServerFormatConfig.js @@ -107,6 +107,14 @@ export function assignSuspenseBoundaryID( return responseState.nextSuspenseID++; } +export function makeId( + responseState: ResponseState, + treeId: string, + localId: number, +): string { + throw new Error('Not implemented'); +} + const RAW_TEXT = stringToPrecomputedChunk('RCTRawText'); export function pushTextInstance( diff --git a/packages/react-noop-renderer/src/createReactNoop.js b/packages/react-noop-renderer/src/createReactNoop.js index c5021d968939df..ef76b6610617f2 100644 --- a/packages/react-noop-renderer/src/createReactNoop.js +++ b/packages/react-noop-renderer/src/createReactNoop.js @@ -973,6 +973,8 @@ function createReactNoop(reconciler: Function, useMutation: boolean) { false, null, null, + false, + '', ); return { _Scheduler: Scheduler, @@ -1000,6 +1002,8 @@ function createReactNoop(reconciler: Function, useMutation: boolean) { false, null, null, + false, + '', ); return { _Scheduler: Scheduler, diff --git a/packages/react-reconciler/src/ReactFiberHooks.new.js b/packages/react-reconciler/src/ReactFiberHooks.new.js index 2ea341864642ca..2c26859bafe704 100644 --- a/packages/react-reconciler/src/ReactFiberHooks.new.js +++ b/packages/react-reconciler/src/ReactFiberHooks.new.js @@ -2035,12 +2035,20 @@ export function getIsUpdatingOpaqueValueInRenderPhaseInDEV(): boolean | void { function mountId(): string { const hook = mountWorkInProgressHook(); + const root = ((getWorkInProgressRoot(): any): FiberRoot); + // TODO: In Fizz, id generation is specific to each server config. Maybe we + // should do this in Fiber, too? Deferring this decision for now because + // there's no other place to store the prefix except for an internal field on + // the public createRoot object, which the fiber tree does not currently have + // a reference to. + const identifierPrefix = root.identifierPrefix; + let id; if (getIsHydrating()) { const treeId = getTreeId(); // Use a captial R prefix for server-generated ids. - id = 'R:' + treeId; + id = identifierPrefix + 'R:' + treeId; // Unless this is the first id at this level, append a number at the end // that represents the position of this useId hook among all the useId @@ -2052,7 +2060,7 @@ function mountId(): string { } else { // Use a lowercase r prefix for client-generated ids. const globalClientId = globalClientIdCounter++; - id = 'r:' + globalClientId.toString(32); + id = identifierPrefix + 'r:' + globalClientId.toString(32); } hook.memoizedState = id; diff --git a/packages/react-reconciler/src/ReactFiberHooks.old.js b/packages/react-reconciler/src/ReactFiberHooks.old.js index ff6a9f652fb4db..077073bbeef3c3 100644 --- a/packages/react-reconciler/src/ReactFiberHooks.old.js +++ b/packages/react-reconciler/src/ReactFiberHooks.old.js @@ -2035,12 +2035,20 @@ export function getIsUpdatingOpaqueValueInRenderPhaseInDEV(): boolean | void { function mountId(): string { const hook = mountWorkInProgressHook(); + const root = ((getWorkInProgressRoot(): any): FiberRoot); + // TODO: In Fizz, id generation is specific to each server config. Maybe we + // should do this in Fiber, too? Deferring this decision for now because + // there's no other place to store the prefix except for an internal field on + // the public createRoot object, which the fiber tree does not currently have + // a reference to. + const identifierPrefix = root.identifierPrefix; + let id; if (getIsHydrating()) { const treeId = getTreeId(); // Use a captial R prefix for server-generated ids. - id = 'R:' + treeId; + id = identifierPrefix + 'R:' + treeId; // Unless this is the first id at this level, append a number at the end // that represents the position of this useId hook among all the useId @@ -2052,7 +2060,7 @@ function mountId(): string { } else { // Use a lowercase r prefix for client-generated ids. const globalClientId = globalClientIdCounter++; - id = 'r:' + globalClientId.toString(32); + id = identifierPrefix + 'r:' + globalClientId.toString(32); } hook.memoizedState = id; diff --git a/packages/react-reconciler/src/ReactFiberReconciler.new.js b/packages/react-reconciler/src/ReactFiberReconciler.new.js index 6c42165a303476..1b8b9502de3ee9 100644 --- a/packages/react-reconciler/src/ReactFiberReconciler.new.js +++ b/packages/react-reconciler/src/ReactFiberReconciler.new.js @@ -241,6 +241,7 @@ export function createContainer( hydrationCallbacks: null | SuspenseHydrationCallbacks, isStrictMode: boolean, concurrentUpdatesByDefaultOverride: null | boolean, + identifierPrefix: string, ): OpaqueRoot { return createFiberRoot( containerInfo, @@ -249,6 +250,7 @@ export function createContainer( hydrationCallbacks, isStrictMode, concurrentUpdatesByDefaultOverride, + identifierPrefix, ); } diff --git a/packages/react-reconciler/src/ReactFiberReconciler.old.js b/packages/react-reconciler/src/ReactFiberReconciler.old.js index 7693cc7fe4006b..8649ff69898410 100644 --- a/packages/react-reconciler/src/ReactFiberReconciler.old.js +++ b/packages/react-reconciler/src/ReactFiberReconciler.old.js @@ -241,6 +241,7 @@ export function createContainer( hydrationCallbacks: null | SuspenseHydrationCallbacks, isStrictMode: boolean, concurrentUpdatesByDefaultOverride: null | boolean, + identifierPrefix: string, ): OpaqueRoot { return createFiberRoot( containerInfo, @@ -249,6 +250,7 @@ export function createContainer( hydrationCallbacks, isStrictMode, concurrentUpdatesByDefaultOverride, + identifierPrefix, ); } diff --git a/packages/react-reconciler/src/ReactFiberRoot.new.js b/packages/react-reconciler/src/ReactFiberRoot.new.js index 803adee1e22dd8..9e9feb45d9b037 100644 --- a/packages/react-reconciler/src/ReactFiberRoot.new.js +++ b/packages/react-reconciler/src/ReactFiberRoot.new.js @@ -30,7 +30,7 @@ import {initializeUpdateQueue} from './ReactUpdateQueue.new'; import {LegacyRoot, ConcurrentRoot} from './ReactRootTags'; import {createCache, retainCache} from './ReactFiberCacheComponent.new'; -function FiberRootNode(containerInfo, tag, hydrate) { +function FiberRootNode(containerInfo, tag, hydrate, identifierPrefix) { this.tag = tag; this.containerInfo = containerInfo; this.pendingChildren = null; @@ -56,6 +56,8 @@ function FiberRootNode(containerInfo, tag, hydrate) { this.entangledLanes = NoLanes; this.entanglements = createLaneMap(NoLanes); + this.identifierPrefix = identifierPrefix; + if (enableCache) { this.pooledCache = null; this.pooledCacheLanes = NoLanes; @@ -101,8 +103,14 @@ export function createFiberRoot( hydrationCallbacks: null | SuspenseHydrationCallbacks, isStrictMode: boolean, concurrentUpdatesByDefaultOverride: null | boolean, + identifierPrefix: string, ): FiberRoot { - const root: FiberRoot = (new FiberRootNode(containerInfo, tag, hydrate): any); + const root: FiberRoot = (new FiberRootNode( + containerInfo, + tag, + hydrate, + identifierPrefix, + ): any); if (enableSuspenseCallback) { root.hydrationCallbacks = hydrationCallbacks; } diff --git a/packages/react-reconciler/src/ReactFiberRoot.old.js b/packages/react-reconciler/src/ReactFiberRoot.old.js index 504dac966ef229..d8d061297854fb 100644 --- a/packages/react-reconciler/src/ReactFiberRoot.old.js +++ b/packages/react-reconciler/src/ReactFiberRoot.old.js @@ -30,7 +30,7 @@ import {initializeUpdateQueue} from './ReactUpdateQueue.old'; import {LegacyRoot, ConcurrentRoot} from './ReactRootTags'; import {createCache, retainCache} from './ReactFiberCacheComponent.old'; -function FiberRootNode(containerInfo, tag, hydrate) { +function FiberRootNode(containerInfo, tag, hydrate, identifierPrefix) { this.tag = tag; this.containerInfo = containerInfo; this.pendingChildren = null; @@ -56,6 +56,8 @@ function FiberRootNode(containerInfo, tag, hydrate) { this.entangledLanes = NoLanes; this.entanglements = createLaneMap(NoLanes); + this.identifierPrefix = identifierPrefix; + if (enableCache) { this.pooledCache = null; this.pooledCacheLanes = NoLanes; @@ -101,8 +103,14 @@ export function createFiberRoot( hydrationCallbacks: null | SuspenseHydrationCallbacks, isStrictMode: boolean, concurrentUpdatesByDefaultOverride: null | boolean, + identifierPrefix: string, ): FiberRoot { - const root: FiberRoot = (new FiberRootNode(containerInfo, tag, hydrate): any); + const root: FiberRoot = (new FiberRootNode( + containerInfo, + tag, + hydrate, + identifierPrefix, + ): any); if (enableSuspenseCallback) { root.hydrationCallbacks = hydrationCallbacks; } diff --git a/packages/react-reconciler/src/ReactInternalTypes.js b/packages/react-reconciler/src/ReactInternalTypes.js index db964103836da9..13965720b7cd30 100644 --- a/packages/react-reconciler/src/ReactInternalTypes.js +++ b/packages/react-reconciler/src/ReactInternalTypes.js @@ -239,6 +239,13 @@ type BaseFiberRootProperties = {| pooledCache: Cache | null, pooledCacheLanes: Lanes, + + // TODO: In Fizz, id generation is specific to each server config. Maybe we + // should do this in Fiber, too? Deferring this decision for now because + // there's no other place to store the prefix except for an internal field on + // the public createRoot object, which the fiber tree does not currently have + // a reference to. + identifierPrefix: string, |}; // The following attributes are only used by DevTools and are only present in DEV builds. diff --git a/packages/react-reconciler/src/__tests__/ReactFiberHostContext-test.internal.js b/packages/react-reconciler/src/__tests__/ReactFiberHostContext-test.internal.js index ee4bd306481b0a..4bf292df79f7af 100644 --- a/packages/react-reconciler/src/__tests__/ReactFiberHostContext-test.internal.js +++ b/packages/react-reconciler/src/__tests__/ReactFiberHostContext-test.internal.js @@ -74,6 +74,8 @@ describe('ReactFiberHostContext', () => { ConcurrentRoot, false, null, + false, + '', ); act(() => { Renderer.updateContainer( @@ -135,6 +137,8 @@ describe('ReactFiberHostContext', () => { ConcurrentRoot, false, null, + false, + '', ); act(() => { Renderer.updateContainer( diff --git a/packages/react-server/src/ReactFizzHooks.js b/packages/react-server/src/ReactFizzHooks.js index 926a1969bb48a3..5997f1b02b902b 100644 --- a/packages/react-server/src/ReactFizzHooks.js +++ b/packages/react-server/src/ReactFizzHooks.js @@ -22,6 +22,8 @@ import type {Task} from './ReactFizzServer'; import {readContext as readContextImpl} from './ReactFizzNewContext'; import {getTreeId} from './ReactFizzTreeContext'; +import {makeId} from './ReactServerFormatConfig'; + import {enableCache} from 'shared/ReactFeatureFlags'; import is from 'shared/objectIs'; @@ -512,18 +514,15 @@ function useId(): string { const task: Task = (currentlyRenderingTask: any); const treeId = getTreeId(task.treeContext); - // Use a captial R prefix for server-generated ids. - let id = 'R:' + treeId; - - // Unless this is the first id at this level, append a number at the end - // that represents the position of this useId hook among all the useId - // hooks for this fiber. - const localId = localIdCounter++; - if (localId > 0) { - id += ':' + localId.toString(32); + const responseState = currentResponseState; + if (responseState === null) { + throw new Error( + 'Invalid hook call. Hooks can only be called inside of the body of a function component.', + ); } - return id; + const localId = localIdCounter++; + return makeId(responseState, treeId, localId); } function unsupportedRefresh() { diff --git a/packages/react-test-renderer/src/ReactTestRenderer.js b/packages/react-test-renderer/src/ReactTestRenderer.js index 2e49968e85ded2..de6e4beffec5f0 100644 --- a/packages/react-test-renderer/src/ReactTestRenderer.js +++ b/packages/react-test-renderer/src/ReactTestRenderer.js @@ -471,6 +471,7 @@ function create(element: React$Element, options: TestRendererOptions) { null, isStrictMode, concurrentUpdatesByDefault, + '', ); if (root == null) {