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) {