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