wrap SuspenseInstanceRetry callback so scheduler waits for it (#18805)
Fernando Lores committed
May 5, 2020 at 12:47 UTC
40e6029462b23ce80c4c5fbd4aa422de20b7545e
3 files changed
+82
-8
packages/react-reconciler/src/ReactFiberBeginWork.new.js
+6
-4
@@ -198,6 +198,7 @@ import {
198
getWorkInProgressRoot,
199
pushRenderLanes,
200
} from './ReactFiberWorkLoop.new';
201
+import {unstable_wrap as Schedule_tracing_wrap} from 'scheduler/tracing';
202
203
import {disableLogs, reenableLogs} from 'shared/ConsolePatchingDev';
204
@@ -2336,10 +2337,11 @@ function updateDehydratedSuspenseComponent(
2337
// Leave the child in place. I.e. the dehydrated fragment.
2338
workInProgress.child = current.child;
2339
// 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);
2345
return null;
2346
} else {
2347
// This is the first attempt.
packages/react-reconciler/src/ReactFiberBeginWork.old.js
+6
-4
@@ -180,6 +180,7 @@ import {
180
markUnprocessedUpdateTime,
181
getWorkInProgressRoot,
182
} from './ReactFiberWorkLoop.old';
183
+import {unstable_wrap as Schedule_tracing_wrap} from 'scheduler/tracing';
184
185
import {disableLogs, reenableLogs} from 'shared/ConsolePatchingDev';
186
@@ -2303,10 +2304,11 @@ function updateDehydratedSuspenseComponent(
2304
// Leave the child in place. I.e. the dehydrated fragment.
2305
workInProgress.child = current.child;
2306
// 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);
2312
return null;
2313
} else {
2314
// This is the first attempt.
packages/react/src/__tests__/ReactDOMTracing-test.internal.js
+70
@@ -719,6 +719,76 @@ describe('ReactDOMTracing', () => {
719
).toHaveBeenLastNotifiedOfInteraction(interaction);
720
});
721
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
+
792
// @gate experimental
793
it('traces interaction across client-rendered hydration', () => {
794
let suspend = false;