diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.js b/packages/react-reconciler/src/ReactFiberBeginWork.js index c4ab486ada5cb..02e26ae746274 100644 --- a/packages/react-reconciler/src/ReactFiberBeginWork.js +++ b/packages/react-reconciler/src/ReactFiberBeginWork.js @@ -1903,12 +1903,20 @@ function updateDehydratedSuspenseComponent( // they should be. let serverDisplayTime = requestCurrentTime(); // Schedule a normal pri update to render this content. - workInProgress.expirationTime = computeAsyncExpiration(serverDisplayTime); + let newExpirationTime = computeAsyncExpiration(serverDisplayTime); + if (enableSchedulerTracing) { + markSpawnedWork(newExpirationTime); + } + workInProgress.expirationTime = newExpirationTime; } else { // We'll continue hydrating the rest at offscreen priority since we'll already // be showing the right content coming from the server, it is no rush. workInProgress.expirationTime = Never; + if (enableSchedulerTracing) { + markSpawnedWork(Never); + } } + return null; } diff --git a/packages/react-reconciler/src/ReactFiberCompleteWork.js b/packages/react-reconciler/src/ReactFiberCompleteWork.js index c8fe383451794..034cdbcb3de81 100644 --- a/packages/react-reconciler/src/ReactFiberCompleteWork.js +++ b/packages/react-reconciler/src/ReactFiberCompleteWork.js @@ -979,9 +979,6 @@ function completeWork( 'A dehydrated suspense component was completed without a hydrated node. ' + 'This is probably a bug in React.', ); - if (enableSchedulerTracing) { - markSpawnedWork(Never); - } skipPastDehydratedSuspenseInstance(workInProgress); } else { // We should never have been in a hydration state if we didn't have a current. diff --git a/packages/react/src/__tests__/ReactDOMTracing-test.internal.js b/packages/react/src/__tests__/ReactDOMTracing-test.internal.js index f41394b713100..1170368466ac1 100644 --- a/packages/react/src/__tests__/ReactDOMTracing-test.internal.js +++ b/packages/react/src/__tests__/ReactDOMTracing-test.internal.js @@ -710,6 +710,78 @@ describe('ReactDOMTracing', () => { done(); }); + + it('traces interaction across client-rendered hydration', async done => { + let suspend = false; + let promise = new Promise(() => {}); + let ref = React.createRef(); + + function Child() { + if (suspend) { + throw promise; + } else { + return 'Hello'; + } + } + + function App() { + return ( +
+ + + + + +
+ ); + } + + // Render the final HTML. + suspend = true; + const finalHTML = ReactDOMServer.renderToString(); + + const container = document.createElement('div'); + container.innerHTML = finalHTML; + + let interaction; + + const root = ReactDOM.unstable_createRoot(container, {hydrate: true}); + + // Hydrate without suspending to fill in the client-rendered content. + suspend = false; + SchedulerTracing.unstable_trace('initialization', 0, () => { + interaction = Array.from(SchedulerTracing.unstable_getCurrent())[0]; + + root.render(); + }); + + expect(onWorkStopped).toHaveBeenCalledTimes(1); + + // Advance time a bit so that we get into a new expiration bucket. + Scheduler.unstable_advanceTime(300); + jest.advanceTimersByTime(300); + + Scheduler.unstable_flushAll(); + jest.runAllTimers(); + + expect(ref.current).not.toBe(null); + + // We should've had two commits that was traced. + // First one that hydrates the parent, and then one that hydrates + // the boundary at higher than Never priority. + expect(onWorkStopped).toHaveBeenCalledTimes(3); + + expect(onInteractionTraced).toHaveBeenCalledTimes(1); + expect(onInteractionTraced).toHaveBeenLastNotifiedOfInteraction( + interaction, + ); + expect(onInteractionScheduledWorkCompleted).toHaveBeenCalledTimes(1); + expect( + onInteractionScheduledWorkCompleted, + ).toHaveBeenLastNotifiedOfInteraction(interaction); + + done(); + }); }); }); });