+ `);
+ });
+
+ test('indirections', async () => {
+ function App() {
+ // There are no forks in this tree, but the parent and the child should
+ // have different ids.
+ return (
+
+
+ `);
+ });
+
+ test('empty (null) children', async () => {
+ // We don't treat empty children different from non-empty ones, which means
+ // they get allocated a slot when generating ids. There's no inherent reason
+ // to do this; Fiber happens to allocate a fiber for null children that
+ // appear in a list, which is not ideal for performance. For the purposes
+ // of id generation, though, what matters is that Fizz and Fiber
+ // are consistent.
+ function App() {
+ return (
+ <>
+ {null}
+
+ {null}
+
+ >
+ );
+ }
+
+ await serverAct(async () => {
+ const {pipe} = ReactDOMFizzServer.renderToPipeableStream();
+ pipe(writable);
+ });
+ await clientAct(async () => {
+ ReactDOM.hydrateRoot(container, );
+ });
+ expect(container).toMatchInlineSnapshot(`
+
+
+
+
+ `);
+ });
+
+ test('large ids', async () => {
+ // The component in this test outputs a recursive tree of nodes with ids,
+ // where the underlying binary representation is an alternating series of 1s
+ // and 0s. In other words, they are all of the form 101010101.
+ //
+ // Because we use base 32 encoding, the resulting id should consist of
+ // alternating 'a' (01010) and 'l' (10101) characters, except for the the
+ // 'R:' prefix, and the first character after that, which may not correspond
+ // to a complete set of 5 bits.
+ //
+ // Example: R:clalalalalalalala...
+ //
+ // We can use this pattern to test large ids that exceed the bitwise
+ // safe range (32 bits). The algorithm should theoretically support ids
+ // of any size.
+
+ function Child({children}) {
+ const id = useId();
+ return
{children}
;
+ }
+
+ function App() {
+ let tree = ;
+ for (let i = 0; i < 50; i++) {
+ tree = (
+ <>
+
+ {tree}
+ >
+ );
+ }
+ return tree;
+ }
+
+ await serverAct(async () => {
+ const {pipe} = ReactDOMFizzServer.renderToPipeableStream();
+ pipe(writable);
+ });
+ await clientAct(async () => {
+ ReactDOM.hydrateRoot(container, );
+ });
+ const divs = container.querySelectorAll('div');
+
+ // Confirm that every id matches the expected pattern
+ for (let i = 0; i < divs.length; i++) {
+ // Example: R:clalalalalalalala...
+ expect(divs[i].id).toMatch(/^R:.(((al)*a?)((la)*l?))*$/);
+ }
+ });
+
+ test('multiple ids in a single component', async () => {
+ function App() {
+ const id1 = useId();
+ const id2 = useId();
+ const id3 = useId();
+ return `${id1}, ${id2}, ${id3}`;
+ }
+
+ await serverAct(async () => {
+ const {pipe} = ReactDOMFizzServer.renderToPipeableStream();
+ pipe(writable);
+ });
+ await clientAct(async () => {
+ ReactDOM.hydrateRoot(container, );
+ });
+ // We append a suffix to the end of the id to distinguish them
+ expect(container).toMatchInlineSnapshot(`
+
+ `);
+ });
+
+ test('inserting/deleting siblings outside a dehydrated Suspense boundary', async () => {
+ const span = React.createRef(null);
+ function App({swap}) {
+ // Note: Using a dynamic array so these are treated as insertions and
+ // deletions instead of updates, because Fiber currently allocates a node
+ // even for empty children.
+ const children = [
+ ,
+ swap ? : ,
+ ,
+ ];
+ return (
+ <>
+ {children}
+
+
+
+
+ >
+ );
+ }
+
+ await serverAct(async () => {
+ const {pipe} = ReactDOMFizzServer.renderToPipeableStream();
+ pipe(writable);
+ });
+ const dehydratedSpan = container.getElementsByTagName('span')[0];
+ await clientAct(async () => {
+ const root = ReactDOM.hydrateRoot(container, );
+ expect(Scheduler).toFlushUntilNextPaint([]);
+ expect(container).toMatchInlineSnapshot(`
+
+
+
+
+
+
+
+
+
+ `);
+
+ // The inner boundary hasn't hydrated yet
+ expect(span.current).toBe(null);
+
+ // Swap B for C
+ root.render();
+ });
+ // The swap should not have caused a mismatch.
+ expect(container).toMatchInlineSnapshot(`
+
+
+
+
+
+
+
+
+
+ `);
+ // Should have hydrated successfully
+ expect(span.current).toBe(dehydratedSpan);
+ });
+
+ test('inserting/deleting siblings inside a dehydrated Suspense boundary', async () => {
+ const span = React.createRef(null);
+ function App({swap}) {
+ // Note: Using a dynamic array so these are treated as insertions and
+ // deletions instead of updates, because Fiber currently allocates a node
+ // even for empty children.
+ const children = [
+ ,
+ swap ? : ,
+ ,
+ ];
+ return (
+
+ {children}
+
+
+ );
+ }
+
+ await serverAct(async () => {
+ const {pipe} = ReactDOMFizzServer.renderToPipeableStream();
+ pipe(writable);
+ });
+ const dehydratedSpan = container.getElementsByTagName('span')[0];
+ await clientAct(async () => {
+ const root = ReactDOM.hydrateRoot(container, );
+ expect(Scheduler).toFlushUntilNextPaint([]);
+ expect(container).toMatchInlineSnapshot(`
+
+
+
+
+
+
+
+
+ `);
+
+ // The inner boundary hasn't hydrated yet
+ expect(span.current).toBe(null);
+
+ // Swap B for C
+ root.render();
+ });
+ // The swap should not have caused a mismatch.
+ expect(container).toMatchInlineSnapshot(`
+
+
+
+
+
+
+
+
+ `);
+ // Should have hydrated successfully
+ expect(span.current).toBe(dehydratedSpan);
+ });
+});
diff --git a/packages/react-dom/src/server/ReactPartialRendererHooks.js b/packages/react-dom/src/server/ReactPartialRendererHooks.js
index 168fd78f6103..26f2dd00ee0c 100644
--- a/packages/react-dom/src/server/ReactPartialRendererHooks.js
+++ b/packages/react-dom/src/server/ReactPartialRendererHooks.js
@@ -519,6 +519,10 @@ function useOpaqueIdentifier(): OpaqueIDType {
);
}
+function useId(): OpaqueIDType {
+ throw new Error('Not implemented.');
+}
+
function useCacheRefresh(): (?() => T, ?T) => void {
throw new Error('Not implemented.');
}
@@ -549,6 +553,7 @@ export const Dispatcher: DispatcherType = {
useDeferredValue,
useTransition,
useOpaqueIdentifier,
+ useId,
// Subscriptions are not setup in a server environment.
useMutableSource,
useSyncExternalStore,
diff --git a/packages/react-reconciler/src/ReactChildFiber.new.js b/packages/react-reconciler/src/ReactChildFiber.new.js
index 9071edc24f7a..658b1f0e7b79 100644
--- a/packages/react-reconciler/src/ReactChildFiber.new.js
+++ b/packages/react-reconciler/src/ReactChildFiber.new.js
@@ -13,7 +13,7 @@ import type {Fiber} from './ReactInternalTypes';
import type {Lanes} from './ReactFiberLane.new';
import getComponentNameFromFiber from 'react-reconciler/src/getComponentNameFromFiber';
-import {Placement, ChildDeletion} from './ReactFiberFlags';
+import {Placement, ChildDeletion, Forked} from './ReactFiberFlags';
import {
getIteratorFn,
REACT_ELEMENT_TYPE,
@@ -40,6 +40,8 @@ import {
import {emptyRefsObject} from './ReactFiberClassComponent.new';
import {isCompatibleFamilyForHotReloading} from './ReactFiberHotReloading.new';
import {StrictLegacyMode} from './ReactTypeOfMode';
+import {getIsHydrating} from './ReactFiberHydrationContext.new';
+import {pushTreeFork} from './ReactFiberTreeContext.new';
let didWarnAboutMaps;
let didWarnAboutGenerators;
@@ -334,7 +336,9 @@ function ChildReconciler(shouldTrackSideEffects) {
): number {
newFiber.index = newIndex;
if (!shouldTrackSideEffects) {
- // Noop.
+ // During hydration, the useId algorithm needs to know which fibers are
+ // part of a list of children (arrays, iterators).
+ newFiber.flags |= Forked;
return lastPlacedIndex;
}
const current = newFiber.alternate;
@@ -823,6 +827,10 @@ function ChildReconciler(shouldTrackSideEffects) {
if (newIdx === newChildren.length) {
// We've reached the end of the new children. We can delete the rest.
deleteRemainingChildren(returnFiber, oldFiber);
+ if (getIsHydrating()) {
+ const numberOfForks = newIdx;
+ pushTreeFork(returnFiber, numberOfForks);
+ }
return resultingFirstChild;
}
@@ -843,6 +851,10 @@ function ChildReconciler(shouldTrackSideEffects) {
}
previousNewFiber = newFiber;
}
+ if (getIsHydrating()) {
+ const numberOfForks = newIdx;
+ pushTreeFork(returnFiber, numberOfForks);
+ }
return resultingFirstChild;
}
@@ -886,6 +898,10 @@ function ChildReconciler(shouldTrackSideEffects) {
existingChildren.forEach(child => deleteChild(returnFiber, child));
}
+ if (getIsHydrating()) {
+ const numberOfForks = newIdx;
+ pushTreeFork(returnFiber, numberOfForks);
+ }
return resultingFirstChild;
}
@@ -1013,6 +1029,10 @@ function ChildReconciler(shouldTrackSideEffects) {
if (step.done) {
// We've reached the end of the new children. We can delete the rest.
deleteRemainingChildren(returnFiber, oldFiber);
+ if (getIsHydrating()) {
+ const numberOfForks = newIdx;
+ pushTreeFork(returnFiber, numberOfForks);
+ }
return resultingFirstChild;
}
@@ -1033,6 +1053,10 @@ function ChildReconciler(shouldTrackSideEffects) {
}
previousNewFiber = newFiber;
}
+ if (getIsHydrating()) {
+ const numberOfForks = newIdx;
+ pushTreeFork(returnFiber, numberOfForks);
+ }
return resultingFirstChild;
}
@@ -1076,6 +1100,10 @@ function ChildReconciler(shouldTrackSideEffects) {
existingChildren.forEach(child => deleteChild(returnFiber, child));
}
+ if (getIsHydrating()) {
+ const numberOfForks = newIdx;
+ pushTreeFork(returnFiber, numberOfForks);
+ }
return resultingFirstChild;
}
diff --git a/packages/react-reconciler/src/ReactChildFiber.old.js b/packages/react-reconciler/src/ReactChildFiber.old.js
index 0128ca8f36f3..0ef3b301e95a 100644
--- a/packages/react-reconciler/src/ReactChildFiber.old.js
+++ b/packages/react-reconciler/src/ReactChildFiber.old.js
@@ -13,7 +13,7 @@ import type {Fiber} from './ReactInternalTypes';
import type {Lanes} from './ReactFiberLane.old';
import getComponentNameFromFiber from 'react-reconciler/src/getComponentNameFromFiber';
-import {Placement, ChildDeletion} from './ReactFiberFlags';
+import {Placement, ChildDeletion, Forked} from './ReactFiberFlags';
import {
getIteratorFn,
REACT_ELEMENT_TYPE,
@@ -40,6 +40,8 @@ import {
import {emptyRefsObject} from './ReactFiberClassComponent.old';
import {isCompatibleFamilyForHotReloading} from './ReactFiberHotReloading.old';
import {StrictLegacyMode} from './ReactTypeOfMode';
+import {getIsHydrating} from './ReactFiberHydrationContext.old';
+import {pushTreeFork} from './ReactFiberTreeContext.old';
let didWarnAboutMaps;
let didWarnAboutGenerators;
@@ -334,7 +336,9 @@ function ChildReconciler(shouldTrackSideEffects) {
): number {
newFiber.index = newIndex;
if (!shouldTrackSideEffects) {
- // Noop.
+ // During hydration, the useId algorithm needs to know which fibers are
+ // part of a list of children (arrays, iterators).
+ newFiber.flags |= Forked;
return lastPlacedIndex;
}
const current = newFiber.alternate;
@@ -823,6 +827,10 @@ function ChildReconciler(shouldTrackSideEffects) {
if (newIdx === newChildren.length) {
// We've reached the end of the new children. We can delete the rest.
deleteRemainingChildren(returnFiber, oldFiber);
+ if (getIsHydrating()) {
+ const numberOfForks = newIdx;
+ pushTreeFork(returnFiber, numberOfForks);
+ }
return resultingFirstChild;
}
@@ -843,6 +851,10 @@ function ChildReconciler(shouldTrackSideEffects) {
}
previousNewFiber = newFiber;
}
+ if (getIsHydrating()) {
+ const numberOfForks = newIdx;
+ pushTreeFork(returnFiber, numberOfForks);
+ }
return resultingFirstChild;
}
@@ -886,6 +898,10 @@ function ChildReconciler(shouldTrackSideEffects) {
existingChildren.forEach(child => deleteChild(returnFiber, child));
}
+ if (getIsHydrating()) {
+ const numberOfForks = newIdx;
+ pushTreeFork(returnFiber, numberOfForks);
+ }
return resultingFirstChild;
}
@@ -1013,6 +1029,10 @@ function ChildReconciler(shouldTrackSideEffects) {
if (step.done) {
// We've reached the end of the new children. We can delete the rest.
deleteRemainingChildren(returnFiber, oldFiber);
+ if (getIsHydrating()) {
+ const numberOfForks = newIdx;
+ pushTreeFork(returnFiber, numberOfForks);
+ }
return resultingFirstChild;
}
@@ -1033,6 +1053,10 @@ function ChildReconciler(shouldTrackSideEffects) {
}
previousNewFiber = newFiber;
}
+ if (getIsHydrating()) {
+ const numberOfForks = newIdx;
+ pushTreeFork(returnFiber, numberOfForks);
+ }
return resultingFirstChild;
}
@@ -1076,6 +1100,10 @@ function ChildReconciler(shouldTrackSideEffects) {
existingChildren.forEach(child => deleteChild(returnFiber, child));
}
+ if (getIsHydrating()) {
+ const numberOfForks = newIdx;
+ pushTreeFork(returnFiber, numberOfForks);
+ }
return resultingFirstChild;
}
diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.new.js b/packages/react-reconciler/src/ReactFiberBeginWork.new.js
index 4fe648bc3e76..653ee9e1b4ea 100644
--- a/packages/react-reconciler/src/ReactFiberBeginWork.new.js
+++ b/packages/react-reconciler/src/ReactFiberBeginWork.new.js
@@ -186,6 +186,7 @@ import {
invalidateContextProvider,
} from './ReactFiberContext.new';
import {
+ getIsHydrating,
enterHydrationState,
reenterHydrationStateFromDehydratedSuspenseInstance,
resetHydrationState,
@@ -235,6 +236,11 @@ import {createClassErrorUpdate} from './ReactFiberThrow.new';
import {completeSuspendedOffscreenHostContainer} from './ReactFiberCompleteWork.new';
import is from 'shared/objectIs';
import {setIsStrictModeForDevtools} from './ReactFiberDevToolsHook.new';
+import {
+ getForksAtLevel,
+ isForkedChild,
+ pushTreeId,
+} from './ReactFiberTreeContext.new';
const ReactCurrentOwner = ReactSharedInternals.ReactCurrentOwner;
@@ -1757,6 +1763,7 @@ function mountIndeterminateComponent(
}
}
}
+
reconcileChildren(null, workInProgress, value, renderLanes);
if (__DEV__) {
validateFunctionComponentInDev(workInProgress, Component);
@@ -1845,6 +1852,7 @@ function validateFunctionComponentInDev(workInProgress: Fiber, Component: any) {
const SUSPENDED_MARKER: SuspenseState = {
dehydrated: null,
+ treeContext: null,
retryLane: NoLane,
};
@@ -2693,6 +2701,7 @@ function updateDehydratedSuspenseComponent(
reenterHydrationStateFromDehydratedSuspenseInstance(
workInProgress,
suspenseInstance,
+ suspenseState.treeContext,
);
const nextProps = workInProgress.pendingProps;
const primaryChildren = nextProps.children;
@@ -3675,6 +3684,21 @@ function beginWork(
}
} else {
didReceiveUpdate = false;
+
+ if (getIsHydrating() && isForkedChild(workInProgress)) {
+ // Check if this child belongs to a list of muliple children in
+ // its parent.
+ //
+ // In a true multi-threaded implementation, we would render children on
+ // parallel threads. This would represent the beginning of a new render
+ // thread for this subtree.
+ //
+ // We only use this for id generation during hydration, which is why the
+ // logic is located in this special branch.
+ const slotIndex = workInProgress.index;
+ const numberOfForks = getForksAtLevel(workInProgress);
+ pushTreeId(workInProgress, numberOfForks, slotIndex);
+ }
}
// Before entering the begin phase, clear pending update priority.
diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.old.js b/packages/react-reconciler/src/ReactFiberBeginWork.old.js
index f116897a8661..9833ef481af7 100644
--- a/packages/react-reconciler/src/ReactFiberBeginWork.old.js
+++ b/packages/react-reconciler/src/ReactFiberBeginWork.old.js
@@ -186,6 +186,7 @@ import {
invalidateContextProvider,
} from './ReactFiberContext.old';
import {
+ getIsHydrating,
enterHydrationState,
reenterHydrationStateFromDehydratedSuspenseInstance,
resetHydrationState,
@@ -235,6 +236,11 @@ import {createClassErrorUpdate} from './ReactFiberThrow.old';
import {completeSuspendedOffscreenHostContainer} from './ReactFiberCompleteWork.old';
import is from 'shared/objectIs';
import {setIsStrictModeForDevtools} from './ReactFiberDevToolsHook.old';
+import {
+ getForksAtLevel,
+ isForkedChild,
+ pushTreeId,
+} from './ReactFiberTreeContext.old';
const ReactCurrentOwner = ReactSharedInternals.ReactCurrentOwner;
@@ -1757,6 +1763,7 @@ function mountIndeterminateComponent(
}
}
}
+
reconcileChildren(null, workInProgress, value, renderLanes);
if (__DEV__) {
validateFunctionComponentInDev(workInProgress, Component);
@@ -1845,6 +1852,7 @@ function validateFunctionComponentInDev(workInProgress: Fiber, Component: any) {
const SUSPENDED_MARKER: SuspenseState = {
dehydrated: null,
+ treeContext: null,
retryLane: NoLane,
};
@@ -2693,6 +2701,7 @@ function updateDehydratedSuspenseComponent(
reenterHydrationStateFromDehydratedSuspenseInstance(
workInProgress,
suspenseInstance,
+ suspenseState.treeContext,
);
const nextProps = workInProgress.pendingProps;
const primaryChildren = nextProps.children;
@@ -3675,6 +3684,21 @@ function beginWork(
}
} else {
didReceiveUpdate = false;
+
+ if (getIsHydrating() && isForkedChild(workInProgress)) {
+ // Check if this child belongs to a list of muliple children in
+ // its parent.
+ //
+ // In a true multi-threaded implementation, we would render children on
+ // parallel threads. This would represent the beginning of a new render
+ // thread for this subtree.
+ //
+ // We only use this for id generation during hydration, which is why the
+ // logic is located in this special branch.
+ const slotIndex = workInProgress.index;
+ const numberOfForks = getForksAtLevel(workInProgress);
+ pushTreeId(workInProgress, numberOfForks, slotIndex);
+ }
}
// Before entering the begin phase, clear pending update priority.
diff --git a/packages/react-reconciler/src/ReactFiberCompleteWork.new.js b/packages/react-reconciler/src/ReactFiberCompleteWork.new.js
index 20a7fc52db13..feb38f00461a 100644
--- a/packages/react-reconciler/src/ReactFiberCompleteWork.new.js
+++ b/packages/react-reconciler/src/ReactFiberCompleteWork.new.js
@@ -155,6 +155,7 @@ import {
popRootCachePool,
popCachePool,
} from './ReactFiberCacheComponent.new';
+import {popTreeContext} from './ReactFiberTreeContext.new';
function markUpdate(workInProgress: Fiber) {
// Tag the fiber with an update effect. This turns a Placement into
@@ -822,7 +823,11 @@ function completeWork(
renderLanes: Lanes,
): Fiber | null {
const newProps = workInProgress.pendingProps;
-
+ // Note: This intentionally doesn't check if we're hydrating because comparing
+ // to the current tree provider fiber is just as fast and less error-prone.
+ // Ideally we would have a special version of the work loop only
+ // for hydration.
+ popTreeContext(workInProgress);
switch (workInProgress.tag) {
case IndeterminateComponent:
case LazyComponent:
diff --git a/packages/react-reconciler/src/ReactFiberCompleteWork.old.js b/packages/react-reconciler/src/ReactFiberCompleteWork.old.js
index 305359aef206..ea4d71e8ba37 100644
--- a/packages/react-reconciler/src/ReactFiberCompleteWork.old.js
+++ b/packages/react-reconciler/src/ReactFiberCompleteWork.old.js
@@ -155,6 +155,7 @@ import {
popRootCachePool,
popCachePool,
} from './ReactFiberCacheComponent.old';
+import {popTreeContext} from './ReactFiberTreeContext.old';
function markUpdate(workInProgress: Fiber) {
// Tag the fiber with an update effect. This turns a Placement into
@@ -822,7 +823,11 @@ function completeWork(
renderLanes: Lanes,
): Fiber | null {
const newProps = workInProgress.pendingProps;
-
+ // Note: This intentionally doesn't check if we're hydrating because comparing
+ // to the current tree provider fiber is just as fast and less error-prone.
+ // Ideally we would have a special version of the work loop only
+ // for hydration.
+ popTreeContext(workInProgress);
switch (workInProgress.tag) {
case IndeterminateComponent:
case LazyComponent:
diff --git a/packages/react-reconciler/src/ReactFiberFlags.js b/packages/react-reconciler/src/ReactFiberFlags.js
index a82278222bf0..805c4bed918e 100644
--- a/packages/react-reconciler/src/ReactFiberFlags.js
+++ b/packages/react-reconciler/src/ReactFiberFlags.js
@@ -12,54 +12,55 @@ import {enableCreateEventHandleAPI} from 'shared/ReactFeatureFlags';
export type Flags = number;
// Don't change these two values. They're used by React Dev Tools.
-export const NoFlags = /* */ 0b0000000000000000000000000;
-export const PerformedWork = /* */ 0b0000000000000000000000001;
+export const NoFlags = /* */ 0b00000000000000000000000000;
+export const PerformedWork = /* */ 0b00000000000000000000000001;
// You can change the rest (and add more).
-export const Placement = /* */ 0b0000000000000000000000010;
-export const Update = /* */ 0b0000000000000000000000100;
+export const Placement = /* */ 0b00000000000000000000000010;
+export const Update = /* */ 0b00000000000000000000000100;
export const PlacementAndUpdate = /* */ Placement | Update;
-export const Deletion = /* */ 0b0000000000000000000001000;
-export const ChildDeletion = /* */ 0b0000000000000000000010000;
-export const ContentReset = /* */ 0b0000000000000000000100000;
-export const Callback = /* */ 0b0000000000000000001000000;
-export const DidCapture = /* */ 0b0000000000000000010000000;
-export const ForceClientRender = /* */ 0b0000000000000000100000000;
-export const Ref = /* */ 0b0000000000000001000000000;
-export const Snapshot = /* */ 0b0000000000000010000000000;
-export const Passive = /* */ 0b0000000000000100000000000;
-export const Hydrating = /* */ 0b0000000000001000000000000;
+export const Deletion = /* */ 0b00000000000000000000001000;
+export const ChildDeletion = /* */ 0b00000000000000000000010000;
+export const ContentReset = /* */ 0b00000000000000000000100000;
+export const Callback = /* */ 0b00000000000000000001000000;
+export const DidCapture = /* */ 0b00000000000000000010000000;
+export const ForceClientRender = /* */ 0b00000000000000000100000000;
+export const Ref = /* */ 0b00000000000000001000000000;
+export const Snapshot = /* */ 0b00000000000000010000000000;
+export const Passive = /* */ 0b00000000000000100000000000;
+export const Hydrating = /* */ 0b00000000000001000000000000;
export const HydratingAndUpdate = /* */ Hydrating | Update;
-export const Visibility = /* */ 0b0000000000010000000000000;
-export const StoreConsistency = /* */ 0b0000000000100000000000000;
+export const Visibility = /* */ 0b00000000000010000000000000;
+export const StoreConsistency = /* */ 0b00000000000100000000000000;
export const LifecycleEffectMask =
Passive | Update | Callback | Ref | Snapshot | StoreConsistency;
// Union of all commit flags (flags with the lifetime of a particular commit)
-export const HostEffectMask = /* */ 0b0000000000111111111111111;
+export const HostEffectMask = /* */ 0b00000000000111111111111111;
// These are not really side effects, but we still reuse this field.
-export const Incomplete = /* */ 0b0000000001000000000000000;
-export const ShouldCapture = /* */ 0b0000000010000000000000000;
-export const ForceUpdateForLegacySuspense = /* */ 0b0000000100000000000000000;
-export const DidPropagateContext = /* */ 0b0000001000000000000000000;
-export const NeedsPropagation = /* */ 0b0000010000000000000000000;
+export const Incomplete = /* */ 0b00000000001000000000000000;
+export const ShouldCapture = /* */ 0b00000000010000000000000000;
+export const ForceUpdateForLegacySuspense = /* */ 0b00000000100000000000000000;
+export const DidPropagateContext = /* */ 0b00000001000000000000000000;
+export const NeedsPropagation = /* */ 0b00000010000000000000000000;
+export const Forked = /* */ 0b00000100000000000000000000;
// Static tags describe aspects of a fiber that are not specific to a render,
// e.g. a fiber uses a passive effect (even if there are no updates on this particular render).
// This enables us to defer more work in the unmount case,
// since we can defer traversing the tree during layout to look for Passive effects,
// and instead rely on the static flag as a signal that there may be cleanup work.
-export const RefStatic = /* */ 0b0000100000000000000000000;
-export const LayoutStatic = /* */ 0b0001000000000000000000000;
-export const PassiveStatic = /* */ 0b0010000000000000000000000;
+export const RefStatic = /* */ 0b00001000000000000000000000;
+export const LayoutStatic = /* */ 0b00010000000000000000000000;
+export const PassiveStatic = /* */ 0b00100000000000000000000000;
// These flags allow us to traverse to fibers that have effects on mount
// without traversing the entire tree after every commit for
// double invoking
-export const MountLayoutDev = /* */ 0b0100000000000000000000000;
-export const MountPassiveDev = /* */ 0b1000000000000000000000000;
+export const MountLayoutDev = /* */ 0b01000000000000000000000000;
+export const MountPassiveDev = /* */ 0b10000000000000000000000000;
// Groups of flags that are used in the commit phase to skip over trees that
// don't contain effects, by checking subtreeFlags.
diff --git a/packages/react-reconciler/src/ReactFiberHooks.new.js b/packages/react-reconciler/src/ReactFiberHooks.new.js
index 732f7d71a7e7..a1d7009a85a4 100644
--- a/packages/react-reconciler/src/ReactFiberHooks.new.js
+++ b/packages/react-reconciler/src/ReactFiberHooks.new.js
@@ -117,6 +117,7 @@ import {
} from './ReactUpdateQueue.new';
import {pushInterleavedQueue} from './ReactFiberInterleavedUpdates.new';
import {warnOnSubscriptionInsideStartTransition} from 'shared/ReactFeatureFlags';
+import {getTreeId, pushTreeFork, pushTreeId} from './ReactFiberTreeContext.new';
const {ReactCurrentDispatcher, ReactCurrentBatchConfig} = ReactSharedInternals;
@@ -203,6 +204,12 @@ let didScheduleRenderPhaseUpdate: boolean = false;
// TODO: Maybe there's some way to consolidate this with
// `didScheduleRenderPhaseUpdate`. Or with `numberOfReRenders`.
let didScheduleRenderPhaseUpdateDuringThisPass: boolean = false;
+// Counts the number of useId hooks in this component.
+let localIdCounter: number = 0;
+// Used for ids that are generated completely client-side (i.e. not during
+// hydration). This counter is global, so client ids are not stable across
+// render attempts.
+let globalClientIdCounter: number = 0;
const RE_RENDER_LIMIT = 25;
@@ -396,6 +403,7 @@ export function renderWithHooks(
// workInProgressHook = null;
// didScheduleRenderPhaseUpdate = false;
+ // localIdCounter = 0;
// TODO Warn if no hooks are used at all during mount, then some are used during update.
// Currently we will identify the update render as a mount because memoizedState === null.
@@ -543,6 +551,21 @@ export function renderWithHooks(
}
}
+ if (localIdCounter !== 0) {
+ localIdCounter = 0;
+ if (getIsHydrating()) {
+ // This component materialized an id. This will affect any ids that appear
+ // in its children.
+ const returnFiber = workInProgress.return;
+ if (returnFiber !== null) {
+ const numberOfForks = 1;
+ const slotIndex = 0;
+ pushTreeFork(workInProgress, numberOfForks);
+ pushTreeId(workInProgress, numberOfForks, slotIndex);
+ }
+ }
+ }
+
return children;
}
@@ -612,6 +635,7 @@ export function resetHooksAfterThrow(): void {
}
didScheduleRenderPhaseUpdateDuringThisPass = false;
+ localIdCounter = 0;
}
function mountWorkInProgressHook(): Hook {
@@ -2109,6 +2133,39 @@ function rerenderOpaqueIdentifier(): OpaqueIDType | void {
return id;
}
+function mountId(): string {
+ const hook = mountWorkInProgressHook();
+
+ let id;
+ if (getIsHydrating()) {
+ const treeId = getTreeId();
+
+ // Use a captial R prefix for server-generated ids.
+ 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);
+ }
+ } else {
+ // Use a lowercase r prefix for client-generated ids.
+ const globalClientId = globalClientIdCounter++;
+ id = 'r:' + globalClientId.toString(32);
+ }
+
+ hook.memoizedState = id;
+ return id;
+}
+
+function updateId(): string {
+ const hook = updateWorkInProgressHook();
+ const id: string = hook.memoizedState;
+ return id;
+}
+
function mountRefresh() {
const hook = mountWorkInProgressHook();
const refresh = (hook.memoizedState = refreshCache.bind(
@@ -2425,6 +2482,7 @@ export const ContextOnlyDispatcher: Dispatcher = {
useMutableSource: throwInvalidHookError,
useSyncExternalStore: throwInvalidHookError,
useOpaqueIdentifier: throwInvalidHookError,
+ useId: throwInvalidHookError,
unstable_isNewReconciler: enableNewReconciler,
};
@@ -2453,6 +2511,7 @@ const HooksDispatcherOnMount: Dispatcher = {
useMutableSource: mountMutableSource,
useSyncExternalStore: mountSyncExternalStore,
useOpaqueIdentifier: mountOpaqueIdentifier,
+ useId: mountId,
unstable_isNewReconciler: enableNewReconciler,
};
@@ -2481,6 +2540,7 @@ const HooksDispatcherOnUpdate: Dispatcher = {
useMutableSource: updateMutableSource,
useSyncExternalStore: updateSyncExternalStore,
useOpaqueIdentifier: updateOpaqueIdentifier,
+ useId: updateId,
unstable_isNewReconciler: enableNewReconciler,
};
@@ -2509,6 +2569,7 @@ const HooksDispatcherOnRerender: Dispatcher = {
useMutableSource: updateMutableSource,
useSyncExternalStore: mountSyncExternalStore,
useOpaqueIdentifier: rerenderOpaqueIdentifier,
+ useId: updateId,
unstable_isNewReconciler: enableNewReconciler,
};
@@ -2680,6 +2741,11 @@ if (__DEV__) {
mountHookTypesDev();
return mountOpaqueIdentifier();
},
+ useId(): string {
+ currentHookNameInDev = 'useId';
+ mountHookTypesDev();
+ return mountId();
+ },
unstable_isNewReconciler: enableNewReconciler,
};
@@ -2822,6 +2888,11 @@ if (__DEV__) {
updateHookTypesDev();
return mountOpaqueIdentifier();
},
+ useId(): string {
+ currentHookNameInDev = 'useId';
+ updateHookTypesDev();
+ return mountId();
+ },
unstable_isNewReconciler: enableNewReconciler,
};
@@ -2964,6 +3035,11 @@ if (__DEV__) {
updateHookTypesDev();
return updateOpaqueIdentifier();
},
+ useId(): string {
+ currentHookNameInDev = 'useId';
+ updateHookTypesDev();
+ return updateId();
+ },
unstable_isNewReconciler: enableNewReconciler,
};
@@ -3107,6 +3183,11 @@ if (__DEV__) {
updateHookTypesDev();
return rerenderOpaqueIdentifier();
},
+ useId(): string {
+ currentHookNameInDev = 'useId';
+ updateHookTypesDev();
+ return updateId();
+ },
unstable_isNewReconciler: enableNewReconciler,
};
@@ -3266,6 +3347,12 @@ if (__DEV__) {
mountHookTypesDev();
return mountOpaqueIdentifier();
},
+ useId(): string {
+ currentHookNameInDev = 'useId';
+ warnInvalidHookAccess();
+ mountHookTypesDev();
+ return mountId();
+ },
unstable_isNewReconciler: enableNewReconciler,
};
@@ -3425,6 +3512,12 @@ if (__DEV__) {
updateHookTypesDev();
return updateOpaqueIdentifier();
},
+ useId(): string {
+ currentHookNameInDev = 'useId';
+ warnInvalidHookAccess();
+ updateHookTypesDev();
+ return updateId();
+ },
unstable_isNewReconciler: enableNewReconciler,
};
@@ -3585,6 +3678,12 @@ if (__DEV__) {
updateHookTypesDev();
return rerenderOpaqueIdentifier();
},
+ useId(): string {
+ currentHookNameInDev = 'useId';
+ warnInvalidHookAccess();
+ updateHookTypesDev();
+ return updateId();
+ },
unstable_isNewReconciler: enableNewReconciler,
};
diff --git a/packages/react-reconciler/src/ReactFiberHooks.old.js b/packages/react-reconciler/src/ReactFiberHooks.old.js
index b78f24e8b47f..167698271dba 100644
--- a/packages/react-reconciler/src/ReactFiberHooks.old.js
+++ b/packages/react-reconciler/src/ReactFiberHooks.old.js
@@ -117,6 +117,7 @@ import {
} from './ReactUpdateQueue.old';
import {pushInterleavedQueue} from './ReactFiberInterleavedUpdates.old';
import {warnOnSubscriptionInsideStartTransition} from 'shared/ReactFeatureFlags';
+import {getTreeId, pushTreeFork, pushTreeId} from './ReactFiberTreeContext.old';
const {ReactCurrentDispatcher, ReactCurrentBatchConfig} = ReactSharedInternals;
@@ -203,6 +204,12 @@ let didScheduleRenderPhaseUpdate: boolean = false;
// TODO: Maybe there's some way to consolidate this with
// `didScheduleRenderPhaseUpdate`. Or with `numberOfReRenders`.
let didScheduleRenderPhaseUpdateDuringThisPass: boolean = false;
+// Counts the number of useId hooks in this component.
+let localIdCounter: number = 0;
+// Used for ids that are generated completely client-side (i.e. not during
+// hydration). This counter is global, so client ids are not stable across
+// render attempts.
+let globalClientIdCounter: number = 0;
const RE_RENDER_LIMIT = 25;
@@ -396,6 +403,7 @@ export function renderWithHooks(
// workInProgressHook = null;
// didScheduleRenderPhaseUpdate = false;
+ // localIdCounter = 0;
// TODO Warn if no hooks are used at all during mount, then some are used during update.
// Currently we will identify the update render as a mount because memoizedState === null.
@@ -543,6 +551,21 @@ export function renderWithHooks(
}
}
+ if (localIdCounter !== 0) {
+ localIdCounter = 0;
+ if (getIsHydrating()) {
+ // This component materialized an id. This will affect any ids that appear
+ // in its children.
+ const returnFiber = workInProgress.return;
+ if (returnFiber !== null) {
+ const numberOfForks = 1;
+ const slotIndex = 0;
+ pushTreeFork(workInProgress, numberOfForks);
+ pushTreeId(workInProgress, numberOfForks, slotIndex);
+ }
+ }
+ }
+
return children;
}
@@ -612,6 +635,7 @@ export function resetHooksAfterThrow(): void {
}
didScheduleRenderPhaseUpdateDuringThisPass = false;
+ localIdCounter = 0;
}
function mountWorkInProgressHook(): Hook {
@@ -2109,6 +2133,39 @@ function rerenderOpaqueIdentifier(): OpaqueIDType | void {
return id;
}
+function mountId(): string {
+ const hook = mountWorkInProgressHook();
+
+ let id;
+ if (getIsHydrating()) {
+ const treeId = getTreeId();
+
+ // Use a captial R prefix for server-generated ids.
+ 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);
+ }
+ } else {
+ // Use a lowercase r prefix for client-generated ids.
+ const globalClientId = globalClientIdCounter++;
+ id = 'r:' + globalClientId.toString(32);
+ }
+
+ hook.memoizedState = id;
+ return id;
+}
+
+function updateId(): string {
+ const hook = updateWorkInProgressHook();
+ const id: string = hook.memoizedState;
+ return id;
+}
+
function mountRefresh() {
const hook = mountWorkInProgressHook();
const refresh = (hook.memoizedState = refreshCache.bind(
@@ -2425,6 +2482,7 @@ export const ContextOnlyDispatcher: Dispatcher = {
useMutableSource: throwInvalidHookError,
useSyncExternalStore: throwInvalidHookError,
useOpaqueIdentifier: throwInvalidHookError,
+ useId: throwInvalidHookError,
unstable_isNewReconciler: enableNewReconciler,
};
@@ -2453,6 +2511,7 @@ const HooksDispatcherOnMount: Dispatcher = {
useMutableSource: mountMutableSource,
useSyncExternalStore: mountSyncExternalStore,
useOpaqueIdentifier: mountOpaqueIdentifier,
+ useId: mountId,
unstable_isNewReconciler: enableNewReconciler,
};
@@ -2481,6 +2540,7 @@ const HooksDispatcherOnUpdate: Dispatcher = {
useMutableSource: updateMutableSource,
useSyncExternalStore: updateSyncExternalStore,
useOpaqueIdentifier: updateOpaqueIdentifier,
+ useId: updateId,
unstable_isNewReconciler: enableNewReconciler,
};
@@ -2509,6 +2569,7 @@ const HooksDispatcherOnRerender: Dispatcher = {
useMutableSource: updateMutableSource,
useSyncExternalStore: mountSyncExternalStore,
useOpaqueIdentifier: rerenderOpaqueIdentifier,
+ useId: updateId,
unstable_isNewReconciler: enableNewReconciler,
};
@@ -2680,6 +2741,11 @@ if (__DEV__) {
mountHookTypesDev();
return mountOpaqueIdentifier();
},
+ useId(): string {
+ currentHookNameInDev = 'useId';
+ mountHookTypesDev();
+ return mountId();
+ },
unstable_isNewReconciler: enableNewReconciler,
};
@@ -2822,6 +2888,11 @@ if (__DEV__) {
updateHookTypesDev();
return mountOpaqueIdentifier();
},
+ useId(): string {
+ currentHookNameInDev = 'useId';
+ updateHookTypesDev();
+ return mountId();
+ },
unstable_isNewReconciler: enableNewReconciler,
};
@@ -2964,6 +3035,11 @@ if (__DEV__) {
updateHookTypesDev();
return updateOpaqueIdentifier();
},
+ useId(): string {
+ currentHookNameInDev = 'useId';
+ updateHookTypesDev();
+ return updateId();
+ },
unstable_isNewReconciler: enableNewReconciler,
};
@@ -3107,6 +3183,11 @@ if (__DEV__) {
updateHookTypesDev();
return rerenderOpaqueIdentifier();
},
+ useId(): string {
+ currentHookNameInDev = 'useId';
+ updateHookTypesDev();
+ return updateId();
+ },
unstable_isNewReconciler: enableNewReconciler,
};
@@ -3266,6 +3347,12 @@ if (__DEV__) {
mountHookTypesDev();
return mountOpaqueIdentifier();
},
+ useId(): string {
+ currentHookNameInDev = 'useId';
+ warnInvalidHookAccess();
+ mountHookTypesDev();
+ return mountId();
+ },
unstable_isNewReconciler: enableNewReconciler,
};
@@ -3425,6 +3512,12 @@ if (__DEV__) {
updateHookTypesDev();
return updateOpaqueIdentifier();
},
+ useId(): string {
+ currentHookNameInDev = 'useId';
+ warnInvalidHookAccess();
+ updateHookTypesDev();
+ return updateId();
+ },
unstable_isNewReconciler: enableNewReconciler,
};
@@ -3585,6 +3678,12 @@ if (__DEV__) {
updateHookTypesDev();
return rerenderOpaqueIdentifier();
},
+ useId(): string {
+ currentHookNameInDev = 'useId';
+ warnInvalidHookAccess();
+ updateHookTypesDev();
+ return updateId();
+ },
unstable_isNewReconciler: enableNewReconciler,
};
diff --git a/packages/react-reconciler/src/ReactFiberHydrationContext.new.js b/packages/react-reconciler/src/ReactFiberHydrationContext.new.js
index 7275f1663cad..eabc5e43116b 100644
--- a/packages/react-reconciler/src/ReactFiberHydrationContext.new.js
+++ b/packages/react-reconciler/src/ReactFiberHydrationContext.new.js
@@ -17,6 +17,7 @@ import type {
HostContext,
} from './ReactFiberHostConfig';
import type {SuspenseState} from './ReactFiberSuspenseComponent.new';
+import type {TreeContext} from './ReactFiberTreeContext.new';
import {
HostComponent,
@@ -62,6 +63,10 @@ import {
} from './ReactFiberHostConfig';
import {enableSuspenseServerRenderer} from 'shared/ReactFeatureFlags';
import {OffscreenLane} from './ReactFiberLane.new';
+import {
+ getSuspendedTreeContext,
+ restoreSuspendedTreeContext,
+} from './ReactFiberTreeContext.new';
// The deepest Fiber on the stack involved in a hydration context.
// This may have been an insertion or a hydration.
@@ -96,6 +101,7 @@ function enterHydrationState(fiber: Fiber): boolean {
function reenterHydrationStateFromDehydratedSuspenseInstance(
fiber: Fiber,
suspenseInstance: SuspenseInstance,
+ treeContext: TreeContext | null,
): boolean {
if (!supportsHydration) {
return false;
@@ -105,6 +111,9 @@ function reenterHydrationStateFromDehydratedSuspenseInstance(
);
hydrationParentFiber = fiber;
isHydrating = true;
+ if (treeContext !== null) {
+ restoreSuspendedTreeContext(fiber, treeContext);
+ }
return true;
}
@@ -287,6 +296,7 @@ function tryHydrate(fiber, nextInstance) {
if (suspenseInstance !== null) {
const suspenseState: SuspenseState = {
dehydrated: suspenseInstance,
+ treeContext: getSuspendedTreeContext(),
retryLane: OffscreenLane,
};
fiber.memoizedState = suspenseState;
diff --git a/packages/react-reconciler/src/ReactFiberHydrationContext.old.js b/packages/react-reconciler/src/ReactFiberHydrationContext.old.js
index 654de3f9a289..48e60581e0f2 100644
--- a/packages/react-reconciler/src/ReactFiberHydrationContext.old.js
+++ b/packages/react-reconciler/src/ReactFiberHydrationContext.old.js
@@ -17,6 +17,7 @@ import type {
HostContext,
} from './ReactFiberHostConfig';
import type {SuspenseState} from './ReactFiberSuspenseComponent.old';
+import type {TreeContext} from './ReactFiberTreeContext.old';
import {
HostComponent,
@@ -62,6 +63,10 @@ import {
} from './ReactFiberHostConfig';
import {enableSuspenseServerRenderer} from 'shared/ReactFeatureFlags';
import {OffscreenLane} from './ReactFiberLane.old';
+import {
+ getSuspendedTreeContext,
+ restoreSuspendedTreeContext,
+} from './ReactFiberTreeContext.old';
// The deepest Fiber on the stack involved in a hydration context.
// This may have been an insertion or a hydration.
@@ -96,6 +101,7 @@ function enterHydrationState(fiber: Fiber): boolean {
function reenterHydrationStateFromDehydratedSuspenseInstance(
fiber: Fiber,
suspenseInstance: SuspenseInstance,
+ treeContext: TreeContext | null,
): boolean {
if (!supportsHydration) {
return false;
@@ -105,6 +111,9 @@ function reenterHydrationStateFromDehydratedSuspenseInstance(
);
hydrationParentFiber = fiber;
isHydrating = true;
+ if (treeContext !== null) {
+ restoreSuspendedTreeContext(fiber, treeContext);
+ }
return true;
}
@@ -287,6 +296,7 @@ function tryHydrate(fiber, nextInstance) {
if (suspenseInstance !== null) {
const suspenseState: SuspenseState = {
dehydrated: suspenseInstance,
+ treeContext: getSuspendedTreeContext(),
retryLane: OffscreenLane,
};
fiber.memoizedState = suspenseState;
diff --git a/packages/react-reconciler/src/ReactFiberLane.new.js b/packages/react-reconciler/src/ReactFiberLane.new.js
index c1f34e1052fc..7e1461c7a722 100644
--- a/packages/react-reconciler/src/ReactFiberLane.new.js
+++ b/packages/react-reconciler/src/ReactFiberLane.new.js
@@ -23,6 +23,7 @@ import {
} from 'shared/ReactFeatureFlags';
import {isDevToolsPresent} from './ReactFiberDevToolsHook.new';
import {ConcurrentUpdatesByDefaultMode, NoMode} from './ReactTypeOfMode';
+import {clz32} from './clz32';
// Lane values below should be kept in sync with getLabelForLane(), used by react-devtools-scheduling-profiler.
// If those values are changed that package should be rebuilt and redeployed.
@@ -791,17 +792,3 @@ export function movePendingFibersToMemoized(root: FiberRoot, lanes: Lanes) {
lanes &= ~lane;
}
}
-
-const clz32 = Math.clz32 ? Math.clz32 : clz32Fallback;
-
-// Count leading zeros. Only used on lanes, so assume input is an integer.
-// Based on:
-// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/clz32
-const log = Math.log;
-const LN2 = Math.LN2;
-function clz32Fallback(lanes: Lanes | Lane) {
- if (lanes === 0) {
- return 32;
- }
- return (31 - ((log(lanes) / LN2) | 0)) | 0;
-}
diff --git a/packages/react-reconciler/src/ReactFiberLane.old.js b/packages/react-reconciler/src/ReactFiberLane.old.js
index c81191f6a07e..6b4be15e649f 100644
--- a/packages/react-reconciler/src/ReactFiberLane.old.js
+++ b/packages/react-reconciler/src/ReactFiberLane.old.js
@@ -23,6 +23,7 @@ import {
} from 'shared/ReactFeatureFlags';
import {isDevToolsPresent} from './ReactFiberDevToolsHook.old';
import {ConcurrentUpdatesByDefaultMode, NoMode} from './ReactTypeOfMode';
+import {clz32} from './clz32';
// Lane values below should be kept in sync with getLabelForLane(), used by react-devtools-scheduling-profiler.
// If those values are changed that package should be rebuilt and redeployed.
@@ -791,17 +792,3 @@ export function movePendingFibersToMemoized(root: FiberRoot, lanes: Lanes) {
lanes &= ~lane;
}
}
-
-const clz32 = Math.clz32 ? Math.clz32 : clz32Fallback;
-
-// Count leading zeros. Only used on lanes, so assume input is an integer.
-// Based on:
-// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/clz32
-const log = Math.log;
-const LN2 = Math.LN2;
-function clz32Fallback(lanes: Lanes | Lane) {
- if (lanes === 0) {
- return 32;
- }
- return (31 - ((log(lanes) / LN2) | 0)) | 0;
-}
diff --git a/packages/react-reconciler/src/ReactFiberSuspenseComponent.new.js b/packages/react-reconciler/src/ReactFiberSuspenseComponent.new.js
index 5ad7ae650249..9dbaf7fb76ef 100644
--- a/packages/react-reconciler/src/ReactFiberSuspenseComponent.new.js
+++ b/packages/react-reconciler/src/ReactFiberSuspenseComponent.new.js
@@ -11,6 +11,8 @@ import type {ReactNodeList, Wakeable} from 'shared/ReactTypes';
import type {Fiber} from './ReactInternalTypes';
import type {SuspenseInstance} from './ReactFiberHostConfig';
import type {Lane} from './ReactFiberLane.new';
+import type {TreeContext} from './ReactFiberTreeContext.new';
+
import {SuspenseComponent, SuspenseListComponent} from './ReactWorkTags';
import {NoFlags, DidCapture} from './ReactFiberFlags';
import {
@@ -40,6 +42,7 @@ export type SuspenseState = {|
// here to indicate that it is dehydrated (flag) and for quick access
// to check things like isSuspenseInstancePending.
dehydrated: null | SuspenseInstance,
+ treeContext: null | TreeContext,
// Represents the lane we should attempt to hydrate a dehydrated boundary at.
// OffscreenLane is the default for dehydrated boundaries.
// NoLane is the default for normal boundaries, which turns into "normal" pri.
diff --git a/packages/react-reconciler/src/ReactFiberSuspenseComponent.old.js b/packages/react-reconciler/src/ReactFiberSuspenseComponent.old.js
index 51bef1df3a56..726f0ca52005 100644
--- a/packages/react-reconciler/src/ReactFiberSuspenseComponent.old.js
+++ b/packages/react-reconciler/src/ReactFiberSuspenseComponent.old.js
@@ -11,6 +11,8 @@ import type {ReactNodeList, Wakeable} from 'shared/ReactTypes';
import type {Fiber} from './ReactInternalTypes';
import type {SuspenseInstance} from './ReactFiberHostConfig';
import type {Lane} from './ReactFiberLane.old';
+import type {TreeContext} from './ReactFiberTreeContext.old';
+
import {SuspenseComponent, SuspenseListComponent} from './ReactWorkTags';
import {NoFlags, DidCapture} from './ReactFiberFlags';
import {
@@ -40,6 +42,7 @@ export type SuspenseState = {|
// here to indicate that it is dehydrated (flag) and for quick access
// to check things like isSuspenseInstancePending.
dehydrated: null | SuspenseInstance,
+ treeContext: null | TreeContext,
// Represents the lane we should attempt to hydrate a dehydrated boundary at.
// OffscreenLane is the default for dehydrated boundaries.
// NoLane is the default for normal boundaries, which turns into "normal" pri.
diff --git a/packages/react-reconciler/src/ReactFiberTreeContext.new.js b/packages/react-reconciler/src/ReactFiberTreeContext.new.js
new file mode 100644
index 000000000000..0725ba577e64
--- /dev/null
+++ b/packages/react-reconciler/src/ReactFiberTreeContext.new.js
@@ -0,0 +1,273 @@
+/**
+ * Copyright (c) Facebook, Inc. and its affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ *
+ * @flow
+ */
+
+// Ids are base 32 strings whose binary representation corresponds to the
+// position of a node in a tree.
+
+// Every time the tree forks into multiple children, we add additional bits to
+// the left of the sequence that represent the position of the child within the
+// current level of children.
+//
+// 00101 00010001011010101
+// ╰─┬─╯ ╰───────┬───────╯
+// Fork 5 of 20 Parent id
+//
+// The leading 0s are important. In the above example, you only need 3 bits to
+// represent slot 5. However, you need 5 bits to represent all the forks at
+// the current level, so we must account for the empty bits at the end.
+//
+// For this same reason, slots are 1-indexed instead of 0-indexed. Otherwise,
+// the zeroth id at a level would be indistinguishable from its parent.
+//
+// If a node has only one child, and does not materialize an id (i.e. does not
+// contain a useId hook), then we don't need to allocate any space in the
+// sequence. It's treated as a transparent indirection. For example, these two
+// trees produce the same ids:
+//
+// <> <>
+//
+//
+// >
+//
+// >
+//
+// However, we cannot skip any node that materializes an id. Otherwise, a parent
+// id that does not fork would be indistinguishable from its child id. For
+// example, this tree does not fork, but the parent and child must have
+// different ids.
+//
+//
+//
+//
+//
+// To handle this scenario, every time we materialize an id, we allocate a
+// new level with a single slot. You can think of this as a fork with only one
+// prong, or an array of children with length 1.
+//
+// It's possible for the the size of the sequence to exceed 32 bits, the max
+// size for bitwise operations. When this happens, we make more room by
+// converting the right part of the id to a string and storing it in an overflow
+// variable. We use a base 32 string representation, because 32 is the largest
+// power of 2 that is supported by toString(). We want the base to be large so
+// that the resulting ids are compact, and we want the base to be a power of 2
+// because every log2(base) bits corresponds to a single character, i.e. every
+// log2(32) = 5 bits. That means we can lop bits off the end 5 at a time without
+// affecting the final result.
+
+import {getIsHydrating} from './ReactFiberHydrationContext.new';
+import {clz32} from './clz32';
+import {Forked, NoFlags} from './ReactFiberFlags';
+
+export type TreeContext = {
+ id: number,
+ overflow: string,
+};
+
+// TODO: Use the unified fiber stack module instead of this local one?
+// Intentionally not using it yet to derisk the initial implementation, because
+// the way we push/pop these values is a bit unusual. If there's a mistake, I'd
+// rather the ids be wrong than crash the whole reconciler.
+const forkStack: Array = [];
+let forkStackIndex: number = 0;
+let treeForkProvider: Fiber | null = null;
+let treeForkCount: number = 0;
+
+const idStack: Array = [];
+let idStackIndex: number = 0;
+let treeContextProvider: Fiber | null = null;
+let treeContextId: number = 1;
+let treeContextOverflow: string = '';
+
+export function isForkedChild(workInProgress: Fiber): boolean {
+ warnIfNotHydrating();
+ return (workInProgress.flags & Forked) !== NoFlags;
+}
+
+export function getForksAtLevel(workInProgress: Fiber): number {
+ warnIfNotHydrating();
+ return treeForkCount;
+}
+
+export function getTreeId(): string {
+ const overflow = treeContextOverflow;
+ const idWithLeadingBit = treeContextId;
+ const id = idWithLeadingBit & ~getLeadingBit(idWithLeadingBit);
+ return id.toString(32) + overflow;
+}
+
+export function pushTreeFork(
+ workInProgress: Fiber,
+ totalChildren: number,
+): void {
+ // This is called right after we reconcile an array (or iterator) of child
+ // fibers, because that's the only place where we know how many children in
+ // the whole set without doing extra work later, or storing addtional
+ // information on the fiber.
+ //
+ // That's why this function is separate from pushTreeId — it's called during
+ // the render phase of the fork parent, not the child, which is where we push
+ // the other context values.
+ //
+ // In the Fizz implementation this is much simpler because the child is
+ // rendered in the same callstack as the parent.
+ //
+ // It might be better to just add a `forks` field to the Fiber type. It would
+ // make this module simpler.
+
+ warnIfNotHydrating();
+
+ forkStack[forkStackIndex++] = treeForkCount;
+ forkStack[forkStackIndex++] = treeForkProvider;
+
+ treeForkProvider = workInProgress;
+ treeForkCount = totalChildren;
+}
+
+export function pushTreeId(
+ workInProgress: Fiber,
+ totalChildren: number,
+ index: number,
+) {
+ warnIfNotHydrating();
+
+ idStack[idStackIndex++] = treeContextId;
+ idStack[idStackIndex++] = treeContextOverflow;
+ idStack[idStackIndex++] = treeContextProvider;
+
+ treeContextProvider = workInProgress;
+
+ const baseIdWithLeadingBit = treeContextId;
+ const baseOverflow = treeContextOverflow;
+
+ // The leftmost 1 marks the end of the sequence, non-inclusive. It's not part
+ // of the id; we use it to account for leading 0s.
+ const baseLength = getBitLength(baseIdWithLeadingBit) - 1;
+ const baseId = baseIdWithLeadingBit & ~(1 << baseLength);
+
+ const slot = index + 1;
+ const length = getBitLength(totalChildren) + baseLength;
+
+ // 30 is the max length we can store without overflowing, taking into
+ // consideration the leading 1 we use to mark the end of the sequence.
+ if (length > 30) {
+ // We overflowed the bitwise-safe range. Fall back to slower algorithm.
+ // This branch assumes the length of the base id is greater than 5; it won't
+ // work for smaller ids, because you need 5 bits per character.
+ //
+ // We encode the id in multiple steps: first the base id, then the
+ // remaining digits.
+ //
+ // Each 5 bit sequence corresponds to a single base 32 character. So for
+ // example, if the current id is 23 bits long, we can convert 20 of those
+ // bits into a string of 4 characters, with 3 bits left over.
+ //
+ // First calculate how many bits in the base id represent a complete
+ // sequence of characters.
+ const numberOfOverflowBits = baseLength - (baseLength % 5);
+
+ // Then create a bitmask that selects only those bits.
+ const newOverflowBits = (1 << numberOfOverflowBits) - 1;
+
+ // Select the bits, and convert them to a base 32 string.
+ const newOverflow = (baseId & newOverflowBits).toString(32);
+
+ // Now we can remove those bits from the base id.
+ const restOfBaseId = baseId >> numberOfOverflowBits;
+ const restOfBaseLength = baseLength - numberOfOverflowBits;
+
+ // Finally, encode the rest of the bits using the normal algorithm. Because
+ // we made more room, this time it won't overflow.
+ const restOfLength = getBitLength(totalChildren) + restOfBaseLength;
+ const restOfNewBits = slot << restOfBaseLength;
+ const id = restOfNewBits | restOfBaseId;
+ const overflow = newOverflow + baseOverflow;
+
+ treeContextId = (1 << restOfLength) | id;
+ treeContextOverflow = overflow;
+ } else {
+ // Normal path
+ const newBits = slot << baseLength;
+ const id = newBits | baseId;
+ const overflow = baseOverflow;
+
+ treeContextId = (1 << length) | id;
+ treeContextOverflow = overflow;
+ }
+}
+
+function getBitLength(number: number): number {
+ return 32 - clz32(number);
+}
+
+function getLeadingBit(id: number) {
+ return 1 << (getBitLength(id) - 1);
+}
+
+export function popTreeContext(workInProgress: Fiber) {
+ // Restore the previous values.
+
+ // This is a bit more complicated than other context-like modules in Fiber
+ // because the same Fiber may appear on the stack multiple times and for
+ // different reasons. We have to keep popping until the work-in-progress is
+ // no longer at the top of the stack.
+
+ while (workInProgress === treeForkProvider) {
+ treeForkProvider = forkStack[--forkStackIndex];
+ forkStack[forkStackIndex] = null;
+ treeForkCount = forkStack[--forkStackIndex];
+ forkStack[forkStackIndex] = null;
+ }
+
+ while (workInProgress === treeContextProvider) {
+ treeContextProvider = idStack[--idStackIndex];
+ idStack[idStackIndex] = null;
+ treeContextOverflow = idStack[--idStackIndex];
+ idStack[idStackIndex] = null;
+ treeContextId = idStack[--idStackIndex];
+ idStack[idStackIndex] = null;
+ }
+}
+
+export function getSuspendedTreeContext(): TreeContext | null {
+ warnIfNotHydrating();
+ if (treeContextProvider !== null) {
+ return {
+ id: treeContextId,
+ overflow: treeContextOverflow,
+ };
+ } else {
+ return null;
+ }
+}
+
+export function restoreSuspendedTreeContext(
+ workInProgress: Fiber,
+ suspendedContext: TreeContext,
+) {
+ warnIfNotHydrating();
+
+ idStack[idStackIndex++] = treeContextId;
+ idStack[idStackIndex++] = treeContextOverflow;
+ idStack[idStackIndex++] = treeContextProvider;
+
+ treeContextId = suspendedContext.id;
+ treeContextOverflow = suspendedContext.overflow;
+ treeContextProvider = workInProgress;
+}
+
+function warnIfNotHydrating() {
+ if (__DEV__) {
+ if (!getIsHydrating()) {
+ console.error(
+ 'Expected to be hydrating. This is a bug in React. Please file ' +
+ 'an issue.',
+ );
+ }
+ }
+}
diff --git a/packages/react-reconciler/src/ReactFiberTreeContext.old.js b/packages/react-reconciler/src/ReactFiberTreeContext.old.js
new file mode 100644
index 000000000000..a4ba3c3ddb93
--- /dev/null
+++ b/packages/react-reconciler/src/ReactFiberTreeContext.old.js
@@ -0,0 +1,273 @@
+/**
+ * Copyright (c) Facebook, Inc. and its affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ *
+ * @flow
+ */
+
+// Ids are base 32 strings whose binary representation corresponds to the
+// position of a node in a tree.
+
+// Every time the tree forks into multiple children, we add additional bits to
+// the left of the sequence that represent the position of the child within the
+// current level of children.
+//
+// 00101 00010001011010101
+// ╰─┬─╯ ╰───────┬───────╯
+// Fork 5 of 20 Parent id
+//
+// The leading 0s are important. In the above example, you only need 3 bits to
+// represent slot 5. However, you need 5 bits to represent all the forks at
+// the current level, so we must account for the empty bits at the end.
+//
+// For this same reason, slots are 1-indexed instead of 0-indexed. Otherwise,
+// the zeroth id at a level would be indistinguishable from its parent.
+//
+// If a node has only one child, and does not materialize an id (i.e. does not
+// contain a useId hook), then we don't need to allocate any space in the
+// sequence. It's treated as a transparent indirection. For example, these two
+// trees produce the same ids:
+//
+// <> <>
+//
+//
+// >
+//
+// >
+//
+// However, we cannot skip any node that materializes an id. Otherwise, a parent
+// id that does not fork would be indistinguishable from its child id. For
+// example, this tree does not fork, but the parent and child must have
+// different ids.
+//
+//
+//
+//
+//
+// To handle this scenario, every time we materialize an id, we allocate a
+// new level with a single slot. You can think of this as a fork with only one
+// prong, or an array of children with length 1.
+//
+// It's possible for the the size of the sequence to exceed 32 bits, the max
+// size for bitwise operations. When this happens, we make more room by
+// converting the right part of the id to a string and storing it in an overflow
+// variable. We use a base 32 string representation, because 32 is the largest
+// power of 2 that is supported by toString(). We want the base to be large so
+// that the resulting ids are compact, and we want the base to be a power of 2
+// because every log2(base) bits corresponds to a single character, i.e. every
+// log2(32) = 5 bits. That means we can lop bits off the end 5 at a time without
+// affecting the final result.
+
+import {getIsHydrating} from './ReactFiberHydrationContext.old';
+import {clz32} from './clz32';
+import {Forked, NoFlags} from './ReactFiberFlags';
+
+export type TreeContext = {
+ id: number,
+ overflow: string,
+};
+
+// TODO: Use the unified fiber stack module instead of this local one?
+// Intentionally not using it yet to derisk the initial implementation, because
+// the way we push/pop these values is a bit unusual. If there's a mistake, I'd
+// rather the ids be wrong than crash the whole reconciler.
+const forkStack: Array = [];
+let forkStackIndex: number = 0;
+let treeForkProvider: Fiber | null = null;
+let treeForkCount: number = 0;
+
+const idStack: Array = [];
+let idStackIndex: number = 0;
+let treeContextProvider: Fiber | null = null;
+let treeContextId: number = 1;
+let treeContextOverflow: string = '';
+
+export function isForkedChild(workInProgress: Fiber): boolean {
+ warnIfNotHydrating();
+ return (workInProgress.flags & Forked) !== NoFlags;
+}
+
+export function getForksAtLevel(workInProgress: Fiber): number {
+ warnIfNotHydrating();
+ return treeForkCount;
+}
+
+export function getTreeId(): string {
+ const overflow = treeContextOverflow;
+ const idWithLeadingBit = treeContextId;
+ const id = idWithLeadingBit & ~getLeadingBit(idWithLeadingBit);
+ return id.toString(32) + overflow;
+}
+
+export function pushTreeFork(
+ workInProgress: Fiber,
+ totalChildren: number,
+): void {
+ // This is called right after we reconcile an array (or iterator) of child
+ // fibers, because that's the only place where we know how many children in
+ // the whole set without doing extra work later, or storing addtional
+ // information on the fiber.
+ //
+ // That's why this function is separate from pushTreeId — it's called during
+ // the render phase of the fork parent, not the child, which is where we push
+ // the other context values.
+ //
+ // In the Fizz implementation this is much simpler because the child is
+ // rendered in the same callstack as the parent.
+ //
+ // It might be better to just add a `forks` field to the Fiber type. It would
+ // make this module simpler.
+
+ warnIfNotHydrating();
+
+ forkStack[forkStackIndex++] = treeForkCount;
+ forkStack[forkStackIndex++] = treeForkProvider;
+
+ treeForkProvider = workInProgress;
+ treeForkCount = totalChildren;
+}
+
+export function pushTreeId(
+ workInProgress: Fiber,
+ totalChildren: number,
+ index: number,
+) {
+ warnIfNotHydrating();
+
+ idStack[idStackIndex++] = treeContextId;
+ idStack[idStackIndex++] = treeContextOverflow;
+ idStack[idStackIndex++] = treeContextProvider;
+
+ treeContextProvider = workInProgress;
+
+ const baseIdWithLeadingBit = treeContextId;
+ const baseOverflow = treeContextOverflow;
+
+ // The leftmost 1 marks the end of the sequence, non-inclusive. It's not part
+ // of the id; we use it to account for leading 0s.
+ const baseLength = getBitLength(baseIdWithLeadingBit) - 1;
+ const baseId = baseIdWithLeadingBit & ~(1 << baseLength);
+
+ const slot = index + 1;
+ const length = getBitLength(totalChildren) + baseLength;
+
+ // 30 is the max length we can store without overflowing, taking into
+ // consideration the leading 1 we use to mark the end of the sequence.
+ if (length > 30) {
+ // We overflowed the bitwise-safe range. Fall back to slower algorithm.
+ // This branch assumes the length of the base id is greater than 5; it won't
+ // work for smaller ids, because you need 5 bits per character.
+ //
+ // We encode the id in multiple steps: first the base id, then the
+ // remaining digits.
+ //
+ // Each 5 bit sequence corresponds to a single base 32 character. So for
+ // example, if the current id is 23 bits long, we can convert 20 of those
+ // bits into a string of 4 characters, with 3 bits left over.
+ //
+ // First calculate how many bits in the base id represent a complete
+ // sequence of characters.
+ const numberOfOverflowBits = baseLength - (baseLength % 5);
+
+ // Then create a bitmask that selects only those bits.
+ const newOverflowBits = (1 << numberOfOverflowBits) - 1;
+
+ // Select the bits, and convert them to a base 32 string.
+ const newOverflow = (baseId & newOverflowBits).toString(32);
+
+ // Now we can remove those bits from the base id.
+ const restOfBaseId = baseId >> numberOfOverflowBits;
+ const restOfBaseLength = baseLength - numberOfOverflowBits;
+
+ // Finally, encode the rest of the bits using the normal algorithm. Because
+ // we made more room, this time it won't overflow.
+ const restOfLength = getBitLength(totalChildren) + restOfBaseLength;
+ const restOfNewBits = slot << restOfBaseLength;
+ const id = restOfNewBits | restOfBaseId;
+ const overflow = newOverflow + baseOverflow;
+
+ treeContextId = (1 << restOfLength) | id;
+ treeContextOverflow = overflow;
+ } else {
+ // Normal path
+ const newBits = slot << baseLength;
+ const id = newBits | baseId;
+ const overflow = baseOverflow;
+
+ treeContextId = (1 << length) | id;
+ treeContextOverflow = overflow;
+ }
+}
+
+function getBitLength(number: number): number {
+ return 32 - clz32(number);
+}
+
+function getLeadingBit(id: number) {
+ return 1 << (getBitLength(id) - 1);
+}
+
+export function popTreeContext(workInProgress: Fiber) {
+ // Restore the previous values.
+
+ // This is a bit more complicated than other context-like modules in Fiber
+ // because the same Fiber may appear on the stack multiple times and for
+ // different reasons. We have to keep popping until the work-in-progress is
+ // no longer at the top of the stack.
+
+ while (workInProgress === treeForkProvider) {
+ treeForkProvider = forkStack[--forkStackIndex];
+ forkStack[forkStackIndex] = null;
+ treeForkCount = forkStack[--forkStackIndex];
+ forkStack[forkStackIndex] = null;
+ }
+
+ while (workInProgress === treeContextProvider) {
+ treeContextProvider = idStack[--idStackIndex];
+ idStack[idStackIndex] = null;
+ treeContextOverflow = idStack[--idStackIndex];
+ idStack[idStackIndex] = null;
+ treeContextId = idStack[--idStackIndex];
+ idStack[idStackIndex] = null;
+ }
+}
+
+export function getSuspendedTreeContext(): TreeContext | null {
+ warnIfNotHydrating();
+ if (treeContextProvider !== null) {
+ return {
+ id: treeContextId,
+ overflow: treeContextOverflow,
+ };
+ } else {
+ return null;
+ }
+}
+
+export function restoreSuspendedTreeContext(
+ workInProgress: Fiber,
+ suspendedContext: TreeContext,
+) {
+ warnIfNotHydrating();
+
+ idStack[idStackIndex++] = treeContextId;
+ idStack[idStackIndex++] = treeContextOverflow;
+ idStack[idStackIndex++] = treeContextProvider;
+
+ treeContextId = suspendedContext.id;
+ treeContextOverflow = suspendedContext.overflow;
+ treeContextProvider = workInProgress;
+}
+
+function warnIfNotHydrating() {
+ if (__DEV__) {
+ if (!getIsHydrating()) {
+ console.error(
+ 'Expected to be hydrating. This is a bug in React. Please file ' +
+ 'an issue.',
+ );
+ }
+ }
+}
diff --git a/packages/react-reconciler/src/ReactFiberUnwindWork.new.js b/packages/react-reconciler/src/ReactFiberUnwindWork.new.js
index ba8bd4b57395..bb002b9e71b3 100644
--- a/packages/react-reconciler/src/ReactFiberUnwindWork.new.js
+++ b/packages/react-reconciler/src/ReactFiberUnwindWork.new.js
@@ -50,8 +50,14 @@ import {
popCachePool,
} from './ReactFiberCacheComponent.new';
import {transferActualDuration} from './ReactProfilerTimer.new';
+import {popTreeContext} from './ReactFiberTreeContext.new';
function unwindWork(workInProgress: Fiber, renderLanes: Lanes) {
+ // Note: This intentionally doesn't check if we're hydrating because comparing
+ // to the current tree provider fiber is just as fast and less error-prone.
+ // Ideally we would have a special version of the work loop only
+ // for hydration.
+ popTreeContext(workInProgress);
switch (workInProgress.tag) {
case ClassComponent: {
const Component = workInProgress.type;
@@ -164,6 +170,11 @@ function unwindWork(workInProgress: Fiber, renderLanes: Lanes) {
}
function unwindInterruptedWork(interruptedWork: Fiber, renderLanes: Lanes) {
+ // Note: This intentionally doesn't check if we're hydrating because comparing
+ // to the current tree provider fiber is just as fast and less error-prone.
+ // Ideally we would have a special version of the work loop only
+ // for hydration.
+ popTreeContext(interruptedWork);
switch (interruptedWork.tag) {
case ClassComponent: {
const childContextTypes = interruptedWork.type.childContextTypes;
diff --git a/packages/react-reconciler/src/ReactFiberUnwindWork.old.js b/packages/react-reconciler/src/ReactFiberUnwindWork.old.js
index ad8e479700db..7f161513a4af 100644
--- a/packages/react-reconciler/src/ReactFiberUnwindWork.old.js
+++ b/packages/react-reconciler/src/ReactFiberUnwindWork.old.js
@@ -50,8 +50,14 @@ import {
popCachePool,
} from './ReactFiberCacheComponent.old';
import {transferActualDuration} from './ReactProfilerTimer.old';
+import {popTreeContext} from './ReactFiberTreeContext.old';
function unwindWork(workInProgress: Fiber, renderLanes: Lanes) {
+ // Note: This intentionally doesn't check if we're hydrating because comparing
+ // to the current tree provider fiber is just as fast and less error-prone.
+ // Ideally we would have a special version of the work loop only
+ // for hydration.
+ popTreeContext(workInProgress);
switch (workInProgress.tag) {
case ClassComponent: {
const Component = workInProgress.type;
@@ -164,6 +170,11 @@ function unwindWork(workInProgress: Fiber, renderLanes: Lanes) {
}
function unwindInterruptedWork(interruptedWork: Fiber, renderLanes: Lanes) {
+ // Note: This intentionally doesn't check if we're hydrating because comparing
+ // to the current tree provider fiber is just as fast and less error-prone.
+ // Ideally we would have a special version of the work loop only
+ // for hydration.
+ popTreeContext(interruptedWork);
switch (interruptedWork.tag) {
case ClassComponent: {
const childContextTypes = interruptedWork.type.childContextTypes;
diff --git a/packages/react-reconciler/src/ReactInternalTypes.js b/packages/react-reconciler/src/ReactInternalTypes.js
index e971828233c1..9dac4f40aad5 100644
--- a/packages/react-reconciler/src/ReactInternalTypes.js
+++ b/packages/react-reconciler/src/ReactInternalTypes.js
@@ -44,6 +44,7 @@ export type HookType =
| 'useMutableSource'
| 'useSyncExternalStore'
| 'useOpaqueIdentifier'
+ | 'useId'
| 'useCacheRefresh';
export type ContextDependency = {
@@ -317,6 +318,7 @@ export type Dispatcher = {|
getServerSnapshot?: () => T,
): T,
useOpaqueIdentifier(): any,
+ useId(): string,
useCacheRefresh?: () => (?() => T, ?T) => void,
unstable_isNewReconciler?: boolean,
diff --git a/packages/react-reconciler/src/clz32.js b/packages/react-reconciler/src/clz32.js
new file mode 100644
index 000000000000..80a9cfb91148
--- /dev/null
+++ b/packages/react-reconciler/src/clz32.js
@@ -0,0 +1,25 @@
+/**
+ * Copyright (c) Facebook, Inc. and its affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ *
+ * @flow
+ */
+
+// TODO: This is pretty well supported by browsers. Maybe we can drop it.
+
+export const clz32 = Math.clz32 ? Math.clz32 : clz32Fallback;
+
+// Count leading zeros.
+// Based on:
+// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/clz32
+const log = Math.log;
+const LN2 = Math.LN2;
+function clz32Fallback(x: number): number {
+ const asUint = x >>> 0;
+ if (asUint === 0) {
+ return 32;
+ }
+ return (31 - ((log(asUint) / LN2) | 0)) | 0;
+}
diff --git a/packages/react-server/src/ReactFizzHooks.js b/packages/react-server/src/ReactFizzHooks.js
index 2596632652b4..bf1a2d56e0f9 100644
--- a/packages/react-server/src/ReactFizzHooks.js
+++ b/packages/react-server/src/ReactFizzHooks.js
@@ -17,8 +17,10 @@ import type {
} from 'shared/ReactTypes';
import type {ResponseState, OpaqueIDType} from './ReactServerFormatConfig';
+import type {Task} from './ReactFizzServer';
import {readContext as readContextImpl} from './ReactFizzNewContext';
+import {getTreeId} from './ReactFizzTreeContext';
import {makeServerID} from './ReactServerFormatConfig';
@@ -45,12 +47,15 @@ type Hook = {|
|};
let currentlyRenderingComponent: Object | null = null;
+let currentlyRenderingTask: Task | null = null;
let firstWorkInProgressHook: Hook | null = null;
let workInProgressHook: Hook | null = null;
// Whether the work-in-progress hook is a re-rendered hook
let isReRender: boolean = false;
// Whether an update was scheduled during the currently executing render pass.
let didScheduleRenderPhaseUpdate: boolean = false;
+// Counts the number of useId hooks in this component
+let localIdCounter: number = 0;
// Lazily created map of render-phase updates
let renderPhaseUpdates: Map, Update> | null = null;
// Counter to prevent infinite loops.
@@ -163,18 +168,22 @@ function createWorkInProgressHook(): Hook {
return workInProgressHook;
}
-export function prepareToUseHooks(componentIdentity: Object): void {
+export function prepareToUseHooks(task: Task, componentIdentity: Object): void {
currentlyRenderingComponent = componentIdentity;
+ currentlyRenderingTask = task;
if (__DEV__) {
isInHookUserCodeInDev = false;
}
// The following should have already been reset
// didScheduleRenderPhaseUpdate = false;
+ // localIdCounter = 0;
// firstWorkInProgressHook = null;
// numberOfReRenders = 0;
// renderPhaseUpdates = null;
// workInProgressHook = null;
+
+ localIdCounter = 0;
}
export function finishHooks(
@@ -203,6 +212,14 @@ export function finishHooks(
return children;
}
+export function checkDidRenderIdHook() {
+ // This should be called immediately after every finishHooks call.
+ // Conceptually, it's part of the return value of finishHooks; it's only a
+ // separate function to avoid using an array tuple.
+ const didRenderIdHook = localIdCounter !== 0;
+ return didRenderIdHook;
+}
+
// Reset the internal hooks state if an error occurs while rendering a component
export function resetHooksState(): void {
if (__DEV__) {
@@ -210,6 +227,7 @@ export function resetHooksState(): void {
}
currentlyRenderingComponent = null;
+ currentlyRenderingTask = null;
didScheduleRenderPhaseUpdate = false;
firstWorkInProgressHook = null;
numberOfReRenders = 0;
@@ -495,6 +513,24 @@ function useOpaqueIdentifier(): OpaqueIDType {
return makeServerID(currentResponseState);
}
+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);
+ }
+
+ return id;
+}
+
function unsupportedRefresh() {
throw new Error('Cache cannot be refreshed during server rendering.');
}
@@ -524,6 +560,7 @@ export const Dispatcher: DispatcherType = {
useDeferredValue,
useTransition,
useOpaqueIdentifier,
+ useId,
// Subscriptions are not setup in a server environment.
useMutableSource,
useSyncExternalStore,
diff --git a/packages/react-server/src/ReactFizzServer.js b/packages/react-server/src/ReactFizzServer.js
index 6692b9264864..f06cbc8fb61a 100644
--- a/packages/react-server/src/ReactFizzServer.js
+++ b/packages/react-server/src/ReactFizzServer.js
@@ -25,6 +25,7 @@ import type {
} from './ReactServerFormatConfig';
import type {ContextSnapshot} from './ReactFizzNewContext';
import type {ComponentStackNode} from './ReactFizzComponentStack';
+import type {TreeContext} from './ReactFizzTreeContext';
import {
scheduleWork,
@@ -78,12 +79,14 @@ import {
import {
prepareToUseHooks,
finishHooks,
+ checkDidRenderIdHook,
resetHooksState,
Dispatcher,
currentResponseState,
setCurrentResponseState,
} from './ReactFizzHooks';
import {getStackByComponentStackNode} from './ReactFizzComponentStack';
+import {emptyTreeContext, pushTreeContext} from './ReactFizzTreeContext';
import {
getIteratorFn,
@@ -134,7 +137,7 @@ type SuspenseBoundary = {
fallbackAbortableTasks: Set, // used to cancel task on the fallback if the boundary completes or gets canceled.
};
-type Task = {
+export type Task = {
node: ReactNodeList,
ping: () => void,
blockedBoundary: Root | SuspenseBoundary,
@@ -142,6 +145,7 @@ type Task = {
abortSet: Set, // the abortable set that this task belongs to
legacyContext: LegacyContext, // the current legacy context that this task is executing in
context: ContextSnapshot, // the current new context that this task is executing in
+ treeContext: TreeContext, // the current tree context that this task is executing in
componentStack: null | ComponentStackNode, // DEV-only component stack
};
@@ -265,6 +269,7 @@ export function createRequest(
abortSet,
emptyContextObject,
rootContextSnapshot,
+ emptyTreeContext,
);
pingedTasks.push(rootTask);
return request;
@@ -302,6 +307,7 @@ function createTask(
abortSet: Set,
legacyContext: LegacyContext,
context: ContextSnapshot,
+ treeContext: TreeContext,
): Task {
request.allPendingTasks++;
if (blockedBoundary === null) {
@@ -317,6 +323,7 @@ function createTask(
abortSet,
legacyContext,
context,
+ treeContext,
}: any);
if (__DEV__) {
task.componentStack = null;
@@ -497,6 +504,7 @@ function renderSuspenseBoundary(
fallbackAbortSet,
task.legacyContext,
task.context,
+ task.treeContext,
);
if (__DEV__) {
suspendedFallbackTask.componentStack = task.componentStack;
@@ -564,7 +572,7 @@ function renderWithHooks(
secondArg: SecondArg,
): any {
const componentIdentity = {};
- prepareToUseHooks(componentIdentity);
+ prepareToUseHooks(task, componentIdentity);
const result = Component(props, secondArg);
return finishHooks(Component, props, result, secondArg);
}
@@ -671,6 +679,7 @@ function renderIndeterminateComponent(
}
const value = renderWithHooks(request, task, Component, props, legacyContext);
+ const hasId = checkDidRenderIdHook();
if (__DEV__) {
// Support for module components is deprecated and is removed behind a flag.
@@ -742,7 +751,21 @@ function renderIndeterminateComponent(
}
// We're now successfully past this task, and we don't have to pop back to
// the previous task every again, so we can use the destructive recursive form.
- renderNodeDestructive(request, task, value);
+ if (hasId) {
+ // This component materialized an id. We treat this as its own level, with
+ // a single "child" slot.
+ const prevTreeContext = task.treeContext;
+ const totalChildren = 1;
+ const index = 0;
+ task.treeContext = pushTreeContext(prevTreeContext, totalChildren, index);
+ try {
+ renderNodeDestructive(request, task, value);
+ } finally {
+ task.treeContext = prevTreeContext;
+ }
+ } else {
+ renderNodeDestructive(request, task, value);
+ }
}
popComponentStackInDEV(task);
}
@@ -827,7 +850,22 @@ function renderForwardRef(
): void {
pushFunctionComponentStackInDEV(task, type.render);
const children = renderWithHooks(request, task, type.render, props, ref);
- renderNodeDestructive(request, task, children);
+ const hasId = checkDidRenderIdHook();
+ if (hasId) {
+ // This component materialized an id. We treat this as its own level, with
+ // a single "child" slot.
+ const prevTreeContext = task.treeContext;
+ const totalChildren = 1;
+ const index = 0;
+ task.treeContext = pushTreeContext(prevTreeContext, totalChildren, index);
+ try {
+ renderNodeDestructive(request, task, children);
+ } finally {
+ task.treeContext = prevTreeContext;
+ }
+ } else {
+ renderNodeDestructive(request, task, children);
+ }
popComponentStackInDEV(task);
}
@@ -1122,12 +1160,7 @@ function renderNodeDestructive(
}
if (isArray(node)) {
- for (let i = 0; i < node.length; i++) {
- // Recursively render the rest. We need to use the non-destructive form
- // so that we can safely pop back up and render the sibling if something
- // suspends.
- renderNode(request, task, node[i]);
- }
+ renderChildrenArray(request, task, node);
return;
}
@@ -1138,18 +1171,23 @@ function renderNodeDestructive(
}
const iterator = iteratorFn.call(node);
if (iterator) {
+ // We need to know how many total children are in this set, so that we
+ // can allocate enough id slots to acommodate them. So we must exhaust
+ // the iterator before we start recursively rendering the children.
+ // TODO: This is not great but I think it's inherent to the id
+ // generation algorithm.
let step = iterator.next();
// If there are not entries, we need to push an empty so we start by checking that.
if (!step.done) {
+ const children = [];
do {
- // Recursively render the rest. We need to use the non-destructive form
- // so that we can safely pop back up and render the sibling if something
- // suspends.
- renderNode(request, task, step.value);
+ children.push(step.value);
step = iterator.next();
} while (!step.done);
+ renderChildrenArray(request, task, children);
return;
}
+ return;
}
}
@@ -1191,6 +1229,21 @@ function renderNodeDestructive(
}
}
+function renderChildrenArray(request, task, children) {
+ const totalChildren = children.length;
+ for (let i = 0; i < totalChildren; i++) {
+ const prevTreeContext = task.treeContext;
+ task.treeContext = pushTreeContext(prevTreeContext, totalChildren, i);
+ try {
+ // We need to use the non-destructive form so that we can safely pop back
+ // up and render the sibling if something suspends.
+ renderNode(request, task, children[i]);
+ } finally {
+ task.treeContext = prevTreeContext;
+ }
+ }
+}
+
function spawnNewSuspendedTask(
request: Request,
task: Task,
@@ -1214,6 +1267,7 @@ function spawnNewSuspendedTask(
task.abortSet,
task.legacyContext,
task.context,
+ task.treeContext,
);
if (__DEV__) {
if (task.componentStack !== null) {
@@ -1257,6 +1311,7 @@ function renderNode(request: Request, task: Task, node: ReactNodeList): void {
if (__DEV__) {
task.componentStack = previousComponentStack;
}
+ return;
} else {
// Restore the context. We assume that this will be restored by the inner
// functions in case nothing throws so we don't use "finally" here.
diff --git a/packages/react-server/src/ReactFizzTreeContext.js b/packages/react-server/src/ReactFizzTreeContext.js
new file mode 100644
index 000000000000..c9a47e5af72a
--- /dev/null
+++ b/packages/react-server/src/ReactFizzTreeContext.js
@@ -0,0 +1,168 @@
+/**
+ * Copyright (c) Facebook, Inc. and its affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ *
+ * @flow
+ */
+
+// Ids are base 32 strings whose binary representation corresponds to the
+// position of a node in a tree.
+
+// Every time the tree forks into multiple children, we add additional bits to
+// the left of the sequence that represent the position of the child within the
+// current level of children.
+//
+// 00101 00010001011010101
+// ╰─┬─╯ ╰───────┬───────╯
+// Fork 5 of 20 Parent id
+//
+// The leading 0s are important. In the above example, you only need 3 bits to
+// represent slot 5. However, you need 5 bits to represent all the forks at
+// the current level, so we must account for the empty bits at the end.
+//
+// For this same reason, slots are 1-indexed instead of 0-indexed. Otherwise,
+// the zeroth id at a level would be indistinguishable from its parent.
+//
+// If a node has only one child, and does not materialize an id (i.e. does not
+// contain a useId hook), then we don't need to allocate any space in the
+// sequence. It's treated as a transparent indirection. For example, these two
+// trees produce the same ids:
+//
+// <> <>
+//
+//
+// >
+//
+// >
+//
+// However, we cannot skip any node that materializes an id. Otherwise, a parent
+// id that does not fork would be indistinguishable from its child id. For
+// example, this tree does not fork, but the parent and child must have
+// different ids.
+//
+//
+//
+//
+//
+// To handle this scenario, every time we materialize an id, we allocate a
+// new level with a single slot. You can think of this as a fork with only one
+// prong, or an array of children with length 1.
+//
+// It's possible for the the size of the sequence to exceed 32 bits, the max
+// size for bitwise operations. When this happens, we make more room by
+// converting the right part of the id to a string and storing it in an overflow
+// variable. We use a base 32 string representation, because 32 is the largest
+// power of 2 that is supported by toString(). We want the base to be large so
+// that the resulting ids are compact, and we want the base to be a power of 2
+// because every log2(base) bits corresponds to a single character, i.e. every
+// log2(32) = 5 bits. That means we can lop bits off the end 5 at a time without
+// affecting the final result.
+
+export type TreeContext = {
+ +id: number,
+ +overflow: string,
+};
+
+export const emptyTreeContext = {
+ id: 1,
+ overflow: '',
+};
+
+export function getTreeId(context: TreeContext): string {
+ const overflow = context.overflow;
+ const idWithLeadingBit = context.id;
+ const id = idWithLeadingBit & ~getLeadingBit(idWithLeadingBit);
+ return id.toString(32) + overflow;
+}
+
+export function pushTreeContext(
+ baseContext: TreeContext,
+ totalChildren: number,
+ index: number,
+): TreeContext {
+ const baseIdWithLeadingBit = baseContext.id;
+ const baseOverflow = baseContext.overflow;
+
+ // The leftmost 1 marks the end of the sequence, non-inclusive. It's not part
+ // of the id; we use it to account for leading 0s.
+ const baseLength = getBitLength(baseIdWithLeadingBit) - 1;
+ const baseId = baseIdWithLeadingBit & ~(1 << baseLength);
+
+ const slot = index + 1;
+ const length = getBitLength(totalChildren) + baseLength;
+
+ // 30 is the max length we can store without overflowing, taking into
+ // consideration the leading 1 we use to mark the end of the sequence.
+ if (length > 30) {
+ // We overflowed the bitwise-safe range. Fall back to slower algorithm.
+ // This branch assumes the length of the base id is greater than 5; it won't
+ // work for smaller ids, because you need 5 bits per character.
+ //
+ // We encode the id in multiple steps: first the base id, then the
+ // remaining digits.
+ //
+ // Each 5 bit sequence corresponds to a single base 32 character. So for
+ // example, if the current id is 23 bits long, we can convert 20 of those
+ // bits into a string of 4 characters, with 3 bits left over.
+ //
+ // First calculate how many bits in the base id represent a complete
+ // sequence of characters.
+ const numberOfOverflowBits = baseLength - (baseLength % 5);
+
+ // Then create a bitmask that selects only those bits.
+ const newOverflowBits = (1 << numberOfOverflowBits) - 1;
+
+ // Select the bits, and convert them to a base 32 string.
+ const newOverflow = (baseId & newOverflowBits).toString(32);
+
+ // Now we can remove those bits from the base id.
+ const restOfBaseId = baseId >> numberOfOverflowBits;
+ const restOfBaseLength = baseLength - numberOfOverflowBits;
+
+ // Finally, encode the rest of the bits using the normal algorithm. Because
+ // we made more room, this time it won't overflow.
+ const restOfLength = getBitLength(totalChildren) + restOfBaseLength;
+ const restOfNewBits = slot << restOfBaseLength;
+ const id = restOfNewBits | restOfBaseId;
+ const overflow = newOverflow + baseOverflow;
+ return {
+ id: (1 << restOfLength) | id,
+ overflow,
+ };
+ } else {
+ // Normal path
+ const newBits = slot << baseLength;
+ const id = newBits | baseId;
+ const overflow = baseOverflow;
+ return {
+ id: (1 << length) | id,
+ overflow,
+ };
+ }
+}
+
+function getBitLength(number: number): number {
+ return 32 - clz32(number);
+}
+
+function getLeadingBit(id: number) {
+ return 1 << (getBitLength(id) - 1);
+}
+
+// TODO: Math.clz32 is supported in Node 12+. Maybe we can drop the fallback.
+const clz32 = Math.clz32 ? Math.clz32 : clz32Fallback;
+
+// Count leading zeros.
+// Based on:
+// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/clz32
+const log = Math.log;
+const LN2 = Math.LN2;
+function clz32Fallback(x: number): number {
+ const asUint = x >>> 0;
+ if (asUint === 0) {
+ return 32;
+ }
+ return (31 - ((log(asUint) / LN2) | 0)) | 0;
+}
diff --git a/packages/react-server/src/ReactFlightServer.js b/packages/react-server/src/ReactFlightServer.js
index 7bd7a95c2561..bba34065cc26 100644
--- a/packages/react-server/src/ReactFlightServer.js
+++ b/packages/react-server/src/ReactFlightServer.js
@@ -846,6 +846,7 @@ const Dispatcher: DispatcherType = {
useImperativeHandle: (unsupportedHook: any),
useEffect: (unsupportedHook: any),
useOpaqueIdentifier: (unsupportedHook: any),
+ useId: (unsupportedHook: any),
useMutableSource: (unsupportedHook: any),
useSyncExternalStore: (unsupportedHook: any),
useCacheRefresh(): (?() => T, ?T) => void {
diff --git a/packages/react-suspense-test-utils/src/ReactSuspenseTestUtils.js b/packages/react-suspense-test-utils/src/ReactSuspenseTestUtils.js
index a3fe87372982..731124ea5159 100644
--- a/packages/react-suspense-test-utils/src/ReactSuspenseTestUtils.js
+++ b/packages/react-suspense-test-utils/src/ReactSuspenseTestUtils.js
@@ -43,6 +43,7 @@ export function waitForSuspense(fn: () => T): Promise {
useDeferredValue: unsupported,
useTransition: unsupported,
useOpaqueIdentifier: unsupported,
+ useId: unsupported,
useMutableSource: unsupported,
useSyncExternalStore: unsupported,
useCacheRefresh: unsupported,
diff --git a/packages/react/index.classic.fb.js b/packages/react/index.classic.fb.js
index c5854f8f6d39..a2e678c580b2 100644
--- a/packages/react/index.classic.fb.js
+++ b/packages/react/index.classic.fb.js
@@ -41,6 +41,7 @@ export {
unstable_getCacheForType,
unstable_useCacheRefresh,
unstable_useOpaqueIdentifier,
+ unstable_useId,
useCallback,
useContext,
useDebugValue,
diff --git a/packages/react/index.experimental.js b/packages/react/index.experimental.js
index 24fc9782595d..20c22828583f 100644
--- a/packages/react/index.experimental.js
+++ b/packages/react/index.experimental.js
@@ -37,6 +37,7 @@ export {
unstable_getCacheForType,
unstable_useCacheRefresh,
unstable_useOpaqueIdentifier,
+ unstable_useId,
useCallback,
useContext,
useDebugValue,
diff --git a/packages/react/index.js b/packages/react/index.js
index 9a6a99ee5218..3108c06c5528 100644
--- a/packages/react/index.js
+++ b/packages/react/index.js
@@ -62,6 +62,7 @@ export {
unstable_getCacheForType,
unstable_useCacheRefresh,
unstable_useOpaqueIdentifier,
+ unstable_useId,
useCallback,
useContext,
useDebugValue,
diff --git a/packages/react/index.modern.fb.js b/packages/react/index.modern.fb.js
index 8d08a43b9094..eef99fdabf2a 100644
--- a/packages/react/index.modern.fb.js
+++ b/packages/react/index.modern.fb.js
@@ -40,6 +40,7 @@ export {
unstable_getCacheForType,
unstable_useCacheRefresh,
unstable_useOpaqueIdentifier,
+ unstable_useId,
useCallback,
useContext,
useDebugValue,
diff --git a/packages/react/index.stable.js b/packages/react/index.stable.js
index 867980fa5389..517dc3f8fb2d 100644
--- a/packages/react/index.stable.js
+++ b/packages/react/index.stable.js
@@ -30,6 +30,7 @@ export {
memo,
startTransition,
unstable_useOpaqueIdentifier,
+ unstable_useId,
useCallback,
useContext,
useDebugValue,
diff --git a/packages/react/src/React.js b/packages/react/src/React.js
index d29858c9b07f..868538c83f59 100644
--- a/packages/react/src/React.js
+++ b/packages/react/src/React.js
@@ -53,6 +53,7 @@ import {
useTransition,
useDeferredValue,
useOpaqueIdentifier,
+ useId,
useCacheRefresh,
} from './ReactHooks';
import {
@@ -127,5 +128,6 @@ export {
// enableScopeAPI
REACT_SCOPE_TYPE as unstable_Scope,
useOpaqueIdentifier as unstable_useOpaqueIdentifier,
+ useId as unstable_useId,
act,
};
diff --git a/packages/react/src/ReactHooks.js b/packages/react/src/ReactHooks.js
index 1892f926c59c..1f987de1671b 100644
--- a/packages/react/src/ReactHooks.js
+++ b/packages/react/src/ReactHooks.js
@@ -174,6 +174,11 @@ export function useOpaqueIdentifier(): OpaqueIDType | void {
return dispatcher.useOpaqueIdentifier();
}
+export function useId(): string {
+ const dispatcher = resolveDispatcher();
+ return dispatcher.useId();
+}
+
export function useMutableSource