diff --git a/packages/react-dom/src/__tests__/ReactDOMUseId-test.js b/packages/react-dom/src/__tests__/ReactDOMUseId-test.js
index 09ccdc86d14e6..dd7bbaa8b41fa 100644
--- a/packages/react-dom/src/__tests__/ReactDOMUseId-test.js
+++ b/packages/react-dom/src/__tests__/ReactDOMUseId-test.js
@@ -16,6 +16,7 @@ let ReactDOMFizzServer;
let Stream;
let Suspense;
let useId;
+let useState;
let document;
let writable;
let container;
@@ -35,6 +36,7 @@ describe('useId', () => {
Stream = require('stream');
Suspense = React.Suspense;
useId = React.useId;
+ useState = React.useState;
// Test Environment
const jsdom = new JSDOM(
@@ -198,6 +200,35 @@ describe('useId', () => {
`);
});
+ test('StrictMode double rendering', async () => {
+ const {StrictMode} = React;
+
+ function App() {
+ return (
+
+
+
+ );
+ }
+
+ await serverAct(async () => {
+ const {pipe} = ReactDOMFizzServer.renderToPipeableStream();
+ pipe(writable);
+ });
+ await clientAct(async () => {
+ ReactDOM.hydrateRoot(container, );
+ });
+ expect(container).toMatchInlineSnapshot(`
+
+ `);
+ });
+
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
@@ -313,6 +344,32 @@ describe('useId', () => {
`);
});
+ test('local render phase updates', async () => {
+ function App({swap}) {
+ const [count, setCount] = useState(0);
+ if (count < 3) {
+ setCount(count + 1);
+ }
+ return useId();
+ }
+
+ await serverAct(async () => {
+ const {pipe} = ReactDOMFizzServer.renderToPipeableStream();
+ pipe(writable);
+ });
+ await clientAct(async () => {
+ ReactDOM.hydrateRoot(container, );
+ });
+ expect(container).toMatchInlineSnapshot(`
+
+ R:0
+
+
+ `);
+ });
+
test('basic incremental hydration', async () => {
function App() {
return (
diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.new.js b/packages/react-reconciler/src/ReactFiberBeginWork.new.js
index 653ee9e1b4ea7..d66bba0dc2ee3 100644
--- a/packages/react-reconciler/src/ReactFiberBeginWork.new.js
+++ b/packages/react-reconciler/src/ReactFiberBeginWork.new.js
@@ -174,7 +174,11 @@ import {
prepareToReadContext,
scheduleWorkOnParentPath,
} from './ReactFiberNewContext.new';
-import {renderWithHooks, bailoutHooks} from './ReactFiberHooks.new';
+import {
+ renderWithHooks,
+ checkDidRenderIdHook,
+ bailoutHooks,
+} from './ReactFiberHooks.new';
import {stopProfilerTimerIfRunning} from './ReactProfilerTimer.new';
import {
getMaskedContext,
@@ -240,6 +244,7 @@ import {
getForksAtLevel,
isForkedChild,
pushTreeId,
+ pushMaterializedTreeId,
} from './ReactFiberTreeContext.new';
const ReactCurrentOwner = ReactSharedInternals.ReactCurrentOwner;
@@ -365,6 +370,7 @@ function updateForwardRef(
// The rest is a fork of updateFunctionComponent
let nextChildren;
+ let hasId;
prepareToReadContext(workInProgress, renderLanes);
if (enableSchedulingProfiler) {
markComponentRenderStarted(workInProgress);
@@ -380,6 +386,7 @@ function updateForwardRef(
ref,
renderLanes,
);
+ hasId = checkDidRenderIdHook();
if (
debugRenderPhaseSideEffectsForStrictMode &&
workInProgress.mode & StrictLegacyMode
@@ -394,6 +401,7 @@ function updateForwardRef(
ref,
renderLanes,
);
+ hasId = checkDidRenderIdHook();
} finally {
setIsStrictModeForDevtools(false);
}
@@ -408,6 +416,7 @@ function updateForwardRef(
ref,
renderLanes,
);
+ hasId = checkDidRenderIdHook();
}
if (enableSchedulingProfiler) {
markComponentRenderStopped();
@@ -418,6 +427,10 @@ function updateForwardRef(
return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
}
+ if (getIsHydrating() && hasId) {
+ pushMaterializedTreeId(workInProgress);
+ }
+
// React DevTools reads this flag.
workInProgress.flags |= PerformedWork;
reconcileChildren(current, workInProgress, nextChildren, renderLanes);
@@ -970,6 +983,7 @@ function updateFunctionComponent(
}
let nextChildren;
+ let hasId;
prepareToReadContext(workInProgress, renderLanes);
if (enableSchedulingProfiler) {
markComponentRenderStarted(workInProgress);
@@ -985,6 +999,7 @@ function updateFunctionComponent(
context,
renderLanes,
);
+ hasId = checkDidRenderIdHook();
if (
debugRenderPhaseSideEffectsForStrictMode &&
workInProgress.mode & StrictLegacyMode
@@ -999,6 +1014,7 @@ function updateFunctionComponent(
context,
renderLanes,
);
+ hasId = checkDidRenderIdHook();
} finally {
setIsStrictModeForDevtools(false);
}
@@ -1013,6 +1029,7 @@ function updateFunctionComponent(
context,
renderLanes,
);
+ hasId = checkDidRenderIdHook();
}
if (enableSchedulingProfiler) {
markComponentRenderStopped();
@@ -1023,6 +1040,10 @@ function updateFunctionComponent(
return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
}
+ if (getIsHydrating() && hasId) {
+ pushMaterializedTreeId(workInProgress);
+ }
+
// React DevTools reads this flag.
workInProgress.flags |= PerformedWork;
reconcileChildren(current, workInProgress, nextChildren, renderLanes);
@@ -1593,6 +1614,7 @@ function mountIndeterminateComponent(
prepareToReadContext(workInProgress, renderLanes);
let value;
+ let hasId;
if (enableSchedulingProfiler) {
markComponentRenderStarted(workInProgress);
@@ -1629,6 +1651,7 @@ function mountIndeterminateComponent(
context,
renderLanes,
);
+ hasId = checkDidRenderIdHook();
setIsRendering(false);
} else {
value = renderWithHooks(
@@ -1639,6 +1662,7 @@ function mountIndeterminateComponent(
context,
renderLanes,
);
+ hasId = checkDidRenderIdHook();
}
if (enableSchedulingProfiler) {
markComponentRenderStopped();
@@ -1758,12 +1782,17 @@ function mountIndeterminateComponent(
context,
renderLanes,
);
+ hasId = checkDidRenderIdHook();
} finally {
setIsStrictModeForDevtools(false);
}
}
}
+ if (getIsHydrating() && hasId) {
+ pushMaterializedTreeId(workInProgress);
+ }
+
reconcileChildren(null, workInProgress, value, renderLanes);
if (__DEV__) {
validateFunctionComponentInDev(workInProgress, Component);
diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.old.js b/packages/react-reconciler/src/ReactFiberBeginWork.old.js
index 9833ef481af70..62d51046b3c46 100644
--- a/packages/react-reconciler/src/ReactFiberBeginWork.old.js
+++ b/packages/react-reconciler/src/ReactFiberBeginWork.old.js
@@ -174,7 +174,11 @@ import {
prepareToReadContext,
scheduleWorkOnParentPath,
} from './ReactFiberNewContext.old';
-import {renderWithHooks, bailoutHooks} from './ReactFiberHooks.old';
+import {
+ renderWithHooks,
+ checkDidRenderIdHook,
+ bailoutHooks,
+} from './ReactFiberHooks.old';
import {stopProfilerTimerIfRunning} from './ReactProfilerTimer.old';
import {
getMaskedContext,
@@ -240,6 +244,7 @@ import {
getForksAtLevel,
isForkedChild,
pushTreeId,
+ pushMaterializedTreeId,
} from './ReactFiberTreeContext.old';
const ReactCurrentOwner = ReactSharedInternals.ReactCurrentOwner;
@@ -365,6 +370,7 @@ function updateForwardRef(
// The rest is a fork of updateFunctionComponent
let nextChildren;
+ let hasId;
prepareToReadContext(workInProgress, renderLanes);
if (enableSchedulingProfiler) {
markComponentRenderStarted(workInProgress);
@@ -380,6 +386,7 @@ function updateForwardRef(
ref,
renderLanes,
);
+ hasId = checkDidRenderIdHook();
if (
debugRenderPhaseSideEffectsForStrictMode &&
workInProgress.mode & StrictLegacyMode
@@ -394,6 +401,7 @@ function updateForwardRef(
ref,
renderLanes,
);
+ hasId = checkDidRenderIdHook();
} finally {
setIsStrictModeForDevtools(false);
}
@@ -408,6 +416,7 @@ function updateForwardRef(
ref,
renderLanes,
);
+ hasId = checkDidRenderIdHook();
}
if (enableSchedulingProfiler) {
markComponentRenderStopped();
@@ -418,6 +427,10 @@ function updateForwardRef(
return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
}
+ if (getIsHydrating() && hasId) {
+ pushMaterializedTreeId(workInProgress);
+ }
+
// React DevTools reads this flag.
workInProgress.flags |= PerformedWork;
reconcileChildren(current, workInProgress, nextChildren, renderLanes);
@@ -970,6 +983,7 @@ function updateFunctionComponent(
}
let nextChildren;
+ let hasId;
prepareToReadContext(workInProgress, renderLanes);
if (enableSchedulingProfiler) {
markComponentRenderStarted(workInProgress);
@@ -985,6 +999,7 @@ function updateFunctionComponent(
context,
renderLanes,
);
+ hasId = checkDidRenderIdHook();
if (
debugRenderPhaseSideEffectsForStrictMode &&
workInProgress.mode & StrictLegacyMode
@@ -999,6 +1014,7 @@ function updateFunctionComponent(
context,
renderLanes,
);
+ hasId = checkDidRenderIdHook();
} finally {
setIsStrictModeForDevtools(false);
}
@@ -1013,6 +1029,7 @@ function updateFunctionComponent(
context,
renderLanes,
);
+ hasId = checkDidRenderIdHook();
}
if (enableSchedulingProfiler) {
markComponentRenderStopped();
@@ -1023,6 +1040,10 @@ function updateFunctionComponent(
return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
}
+ if (getIsHydrating() && hasId) {
+ pushMaterializedTreeId(workInProgress);
+ }
+
// React DevTools reads this flag.
workInProgress.flags |= PerformedWork;
reconcileChildren(current, workInProgress, nextChildren, renderLanes);
@@ -1593,6 +1614,7 @@ function mountIndeterminateComponent(
prepareToReadContext(workInProgress, renderLanes);
let value;
+ let hasId;
if (enableSchedulingProfiler) {
markComponentRenderStarted(workInProgress);
@@ -1629,6 +1651,7 @@ function mountIndeterminateComponent(
context,
renderLanes,
);
+ hasId = checkDidRenderIdHook();
setIsRendering(false);
} else {
value = renderWithHooks(
@@ -1639,6 +1662,7 @@ function mountIndeterminateComponent(
context,
renderLanes,
);
+ hasId = checkDidRenderIdHook();
}
if (enableSchedulingProfiler) {
markComponentRenderStopped();
@@ -1758,12 +1782,17 @@ function mountIndeterminateComponent(
context,
renderLanes,
);
+ hasId = checkDidRenderIdHook();
} finally {
setIsStrictModeForDevtools(false);
}
}
}
+ if (getIsHydrating() && hasId) {
+ pushMaterializedTreeId(workInProgress);
+ }
+
reconcileChildren(null, workInProgress, value, renderLanes);
if (__DEV__) {
validateFunctionComponentInDev(workInProgress, Component);
diff --git a/packages/react-reconciler/src/ReactFiberHooks.new.js b/packages/react-reconciler/src/ReactFiberHooks.new.js
index 1238d673725ac..2ea341864642c 100644
--- a/packages/react-reconciler/src/ReactFiberHooks.new.js
+++ b/packages/react-reconciler/src/ReactFiberHooks.new.js
@@ -110,7 +110,7 @@ import {
} from './ReactUpdateQueue.new';
import {pushInterleavedQueue} from './ReactFiberInterleavedUpdates.new';
import {warnOnSubscriptionInsideStartTransition} from 'shared/ReactFeatureFlags';
-import {getTreeId, pushTreeFork, pushTreeId} from './ReactFiberTreeContext.new';
+import {getTreeId} from './ReactFiberTreeContext.new';
const {ReactCurrentDispatcher, ReactCurrentBatchConfig} = ReactSharedInternals;
@@ -432,6 +432,7 @@ export function renderWithHooks(
let numberOfReRenders: number = 0;
do {
didScheduleRenderPhaseUpdateDuringThisPass = false;
+ localIdCounter = 0;
if (numberOfReRenders >= RE_RENDER_LIMIT) {
throw new Error(
@@ -513,6 +514,8 @@ export function renderWithHooks(
}
didScheduleRenderPhaseUpdate = false;
+ // This is reset by checkDidRenderIdHook
+ // localIdCounter = 0;
if (didRenderTooFewHooks) {
throw new Error(
@@ -541,25 +544,18 @@ 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;
}
+export function checkDidRenderIdHook() {
+ // This should be called immediately after every renderWithHooks call.
+ // Conceptually, it's part of the return value of renderWithHooks; it's only a
+ // separate function to avoid using an array tuple.
+ const didRenderIdHook = localIdCounter !== 0;
+ localIdCounter = 0;
+ return didRenderIdHook;
+}
+
export function bailoutHooks(
current: Fiber,
workInProgress: Fiber,
diff --git a/packages/react-reconciler/src/ReactFiberHooks.old.js b/packages/react-reconciler/src/ReactFiberHooks.old.js
index 3b0b87cb1ab7c..ff6a9f652fb4d 100644
--- a/packages/react-reconciler/src/ReactFiberHooks.old.js
+++ b/packages/react-reconciler/src/ReactFiberHooks.old.js
@@ -110,7 +110,7 @@ import {
} from './ReactUpdateQueue.old';
import {pushInterleavedQueue} from './ReactFiberInterleavedUpdates.old';
import {warnOnSubscriptionInsideStartTransition} from 'shared/ReactFeatureFlags';
-import {getTreeId, pushTreeFork, pushTreeId} from './ReactFiberTreeContext.old';
+import {getTreeId} from './ReactFiberTreeContext.old';
const {ReactCurrentDispatcher, ReactCurrentBatchConfig} = ReactSharedInternals;
@@ -432,6 +432,7 @@ export function renderWithHooks(
let numberOfReRenders: number = 0;
do {
didScheduleRenderPhaseUpdateDuringThisPass = false;
+ localIdCounter = 0;
if (numberOfReRenders >= RE_RENDER_LIMIT) {
throw new Error(
@@ -513,6 +514,8 @@ export function renderWithHooks(
}
didScheduleRenderPhaseUpdate = false;
+ // This is reset by checkDidRenderIdHook
+ // localIdCounter = 0;
if (didRenderTooFewHooks) {
throw new Error(
@@ -541,25 +544,18 @@ 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;
}
+export function checkDidRenderIdHook() {
+ // This should be called immediately after every renderWithHooks call.
+ // Conceptually, it's part of the return value of renderWithHooks; it's only a
+ // separate function to avoid using an array tuple.
+ const didRenderIdHook = localIdCounter !== 0;
+ localIdCounter = 0;
+ return didRenderIdHook;
+}
+
export function bailoutHooks(
current: Fiber,
workInProgress: Fiber,
diff --git a/packages/react-reconciler/src/ReactFiberTreeContext.new.js b/packages/react-reconciler/src/ReactFiberTreeContext.new.js
index 0725ba577e647..968e66155d3fa 100644
--- a/packages/react-reconciler/src/ReactFiberTreeContext.new.js
+++ b/packages/react-reconciler/src/ReactFiberTreeContext.new.js
@@ -201,6 +201,20 @@ export function pushTreeId(
}
}
+export function pushMaterializedTreeId(workInProgress: Fiber) {
+ warnIfNotHydrating();
+
+ // 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);
+ }
+}
+
function getBitLength(number: number): number {
return 32 - clz32(number);
}
diff --git a/packages/react-reconciler/src/ReactFiberTreeContext.old.js b/packages/react-reconciler/src/ReactFiberTreeContext.old.js
index a4ba3c3ddb931..82990b22f983d 100644
--- a/packages/react-reconciler/src/ReactFiberTreeContext.old.js
+++ b/packages/react-reconciler/src/ReactFiberTreeContext.old.js
@@ -201,6 +201,20 @@ export function pushTreeId(
}
}
+export function pushMaterializedTreeId(workInProgress: Fiber) {
+ warnIfNotHydrating();
+
+ // 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);
+ }
+}
+
function getBitLength(number: number): number {
return 32 - clz32(number);
}
diff --git a/packages/react-server/src/ReactFizzHooks.js b/packages/react-server/src/ReactFizzHooks.js
index 7b31e6b37ed9e..926a1969bb48a 100644
--- a/packages/react-server/src/ReactFizzHooks.js
+++ b/packages/react-server/src/ReactFizzHooks.js
@@ -199,6 +199,7 @@ export function finishHooks(
// work-in-progress hooks and applying the additional updates on top. Keep
// restarting until no more updates are scheduled.
didScheduleRenderPhaseUpdate = false;
+ localIdCounter = 0;
numberOfReRenders += 1;
// Start over from the beginning of the list