Skip to content

Commit 40e6029

Browse files
authored
wrap SuspenseInstanceRetry callback so scheduler waits for it (facebook#18805)
1 parent 3cde22a commit 40e6029

File tree

3 files changed

+82
-8
lines changed

3 files changed

+82
-8
lines changed

packages/react-reconciler/src/ReactFiberBeginWork.new.js

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,7 @@ import {
198198
getWorkInProgressRoot,
199199
pushRenderLanes,
200200
} from './ReactFiberWorkLoop.new';
201+
import {unstable_wrap as Schedule_tracing_wrap} from 'scheduler/tracing';
201202

202203
import {disableLogs, reenableLogs} from 'shared/ConsolePatchingDev';
203204

@@ -2336,10 +2337,11 @@ function updateDehydratedSuspenseComponent(
23362337
// Leave the child in place. I.e. the dehydrated fragment.
23372338
workInProgress.child = current.child;
23382339
// Register a callback to retry this boundary once the server has sent the result.
2339-
registerSuspenseInstanceRetry(
2340-
suspenseInstance,
2341-
retryDehydratedSuspenseBoundary.bind(null, current),
2342-
);
2340+
let retry = retryDehydratedSuspenseBoundary.bind(null, current);
2341+
if (enableSchedulerTracing) {
2342+
retry = Schedule_tracing_wrap(retry);
2343+
}
2344+
registerSuspenseInstanceRetry(suspenseInstance, retry);
23432345
return null;
23442346
} else {
23452347
// This is the first attempt.

packages/react-reconciler/src/ReactFiberBeginWork.old.js

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,7 @@ import {
180180
markUnprocessedUpdateTime,
181181
getWorkInProgressRoot,
182182
} from './ReactFiberWorkLoop.old';
183+
import {unstable_wrap as Schedule_tracing_wrap} from 'scheduler/tracing';
183184

184185
import {disableLogs, reenableLogs} from 'shared/ConsolePatchingDev';
185186

@@ -2303,10 +2304,11 @@ function updateDehydratedSuspenseComponent(
23032304
// Leave the child in place. I.e. the dehydrated fragment.
23042305
workInProgress.child = current.child;
23052306
// Register a callback to retry this boundary once the server has sent the result.
2306-
registerSuspenseInstanceRetry(
2307-
suspenseInstance,
2308-
retryDehydratedSuspenseBoundary.bind(null, current),
2309-
);
2307+
let retry = retryDehydratedSuspenseBoundary.bind(null, current);
2308+
if (enableSchedulerTracing) {
2309+
retry = Schedule_tracing_wrap(retry);
2310+
}
2311+
registerSuspenseInstanceRetry(suspenseInstance, retry);
23102312
return null;
23112313
} else {
23122314
// This is the first attempt.

packages/react/src/__tests__/ReactDOMTracing-test.internal.js

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -719,6 +719,76 @@ describe('ReactDOMTracing', () => {
719719
).toHaveBeenLastNotifiedOfInteraction(interaction);
720720
});
721721

722+
// @gate experimental
723+
it('traces interaction across suspended hydration from server', async () => {
724+
// Copied from ReactDOMHostConfig.js
725+
const SUSPENSE_START_DATA = '$';
726+
const SUSPENSE_PENDING_START_DATA = '$?';
727+
728+
const ref = React.createRef();
729+
730+
function App() {
731+
return (
732+
<React.Suspense fallback="Loading...">
733+
<span ref={ref}>Hello</span>
734+
</React.Suspense>
735+
);
736+
}
737+
738+
const container = document.createElement('div');
739+
740+
// Render the final HTML.
741+
const finalHTML = ReactDOMServer.renderToString(<App />);
742+
743+
// Replace the marker with a pending state.
744+
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions#Escaping
745+
const escapedMarker = SUSPENSE_START_DATA.replace(
746+
/[.*+\-?^${}()|[\]\\]/g,
747+
'\\$&',
748+
);
749+
container.innerHTML = finalHTML.replace(
750+
new RegExp(escapedMarker, 'g'),
751+
SUSPENSE_PENDING_START_DATA,
752+
);
753+
754+
let interaction;
755+
756+
const root = ReactDOM.createRoot(container, {hydrate: true});
757+
758+
// Start hydrating but simulate blocking for suspense data from the server.
759+
SchedulerTracing.unstable_trace('initialization', 0, () => {
760+
interaction = Array.from(SchedulerTracing.unstable_getCurrent())[0];
761+
762+
root.render(<App />);
763+
});
764+
Scheduler.unstable_flushAll();
765+
jest.runAllTimers();
766+
767+
expect(ref.current).toBe(null);
768+
expect(onInteractionTraced).toHaveBeenCalledTimes(1);
769+
expect(onInteractionTraced).toHaveBeenLastNotifiedOfInteraction(
770+
interaction,
771+
);
772+
expect(onInteractionScheduledWorkCompleted).not.toHaveBeenCalled();
773+
774+
// Unblock rendering, pretend the content is injected by the server.
775+
const startNode = container.childNodes[0];
776+
expect(startNode).not.toBe(null);
777+
expect(startNode.nodeType).toBe(Node.COMMENT_NODE);
778+
779+
startNode.textContent = SUSPENSE_START_DATA;
780+
startNode._reactRetry();
781+
782+
Scheduler.unstable_flushAll();
783+
jest.runAllTimers();
784+
785+
expect(ref.current).not.toBe(null);
786+
expect(onInteractionScheduledWorkCompleted).toHaveBeenCalledTimes(1);
787+
expect(
788+
onInteractionScheduledWorkCompleted,
789+
).toHaveBeenLastNotifiedOfInteraction(interaction);
790+
});
791+
722792
// @gate experimental
723793
it('traces interaction across client-rendered hydration', () => {
724794
let suspend = false;

0 commit comments

Comments
 (0)