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 a29f9cd23714d..9d1b6a16c2038 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 dd7bbaa8b41fa..036c138d5e97a 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 ac12f18bee509..e3def8459e754 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 d8794eec5bb1b..3c49a15fc14dc 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 65a10974b0196..c4f8d89a8c907 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 5b0798b737129..c3d09f481fb62 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 744c1c178397f..bf7754d6099c2 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 a60683b47b2ee..fb539d8996811 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 5b12d810bbe0f..20084731b5333 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 c5021d968939d..ef76b6610617f 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 2ea341864642c..2c26859bafe70 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 ff6a9f652fb4d..077073bbeef3c 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 6c42165a30347..1b8b9502de3ee 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 7693cc7fe4006..8649ff6989841 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 803adee1e22dd..9e9feb45d9b03 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 504dac966ef22..d8d061297854f 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 db964103836da..13965720b7cd3 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 ee4bd306481b0..4bf292df79f7a 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 926a1969bb48a..5997f1b02b902 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 2e49968e85ded..de6e4beffec5f 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) {