@samitouri / QOS-React-2 / commits / 42b75ab007

Add suspenseCallback feature for runtime tracing of loading states (#16134)

This adds a 'SuspenseCallback' feature flag. When the property is set on a suspense component it will be called during the commit phase with a set of the immediate thenable for this component. This will allow user code to build runtime tracing of the cause for a suspense boundary.

Benoit Girard committed Jul 23, 2019 at 17:13 UTC 42b75ab007a5e7c159933cfdbf2b6845d89fc7f2
10 files changed +251
packages/react-reconciler/src/ReactFiberCommitWork.js
+13
@@ -30,6 +30,7 @@ import {
30 enableSuspenseServerRenderer,
31 enableFlareAPI,
32 enableFundamentalAPI,
33 + enableSuspenseCallback,
34 } from 'shared/ReactFeatureFlags';
35 import {
36 FunctionComponent,
@@ -1320,6 +1321,18 @@ function commitSuspenseComponent(finishedWork: Fiber) {
1321 if (supportsMutation && primaryChildParent !== null) {
1322 hideOrUnhideAllChildren(primaryChildParent, newDidTimeout);
1323 }
1324 +
1325 + if (enableSuspenseCallback && newState !== null) {
1326 + const suspenseCallback = finishedWork.memoizedProps.suspenseCallback;
1327 + if (typeof suspenseCallback === 'function') {
1328 + const thenables: Set<Thenable> | null = (finishedWork.updateQueue: any);
1329 + if (thenables !== null) {
1330 + suspenseCallback(new Set(thenables));
1331 + }
1332 + } else if (__DEV__) {
1333 + warning(false, 'Unexpected type for suspenseCallback.');
1334 + }
1335 + }
1336 }
1337
1338 function attachSuspenseRetryListeners(finishedWork: Fiber) {
packages/react-reconciler/src/ReactFiberCompleteWork.js
+9
@@ -119,6 +119,7 @@ import {
119 } from './ReactFiberHydrationContext';
120 import {
121 enableSchedulerTracing,
122 + enableSuspenseCallback,
123 enableSuspenseServerRenderer,
124 enableFlareAPI,
125 enableFundamentalAPI,
@@ -916,6 +917,14 @@ function completeWork(
917 workInProgress.effectTag |= Update;
918 }
919 }
920 + if (
921 + enableSuspenseCallback &&
922 + workInProgress.updateQueue !== null &&
923 + workInProgress.memoizedProps.suspenseCallback != null
924 + ) {
925 + // Always notify the callback
926 + workInProgress.effectTag |= Update;
927 + }
928 break;
929 }
930 case Fragment:
packages/react-reconciler/src/__tests__/ReactSuspenseCallback-test.internal.js new
+217
@@ -0,0 +1,217 @@
1 +/**
2 + * Copyright (c) Facebook, Inc. and its affiliates.
3 + *
4 + * This source code is licensed under the MIT license found in the
5 + * LICENSE file in the root directory of this source tree.
6 + *
7 + * @emails react-core
8 + * @jest-environment node
9 + */
10 +'use strict';
11 +
12 +let React;
13 +let ReactFeatureFlags;
14 +let ReactNoop;
15 +let Scheduler;
16 +
17 +describe('ReactSuspense', () => {
18 + beforeEach(() => {
19 + jest.resetModules();
20 + ReactFeatureFlags = require('shared/ReactFeatureFlags');
21 + ReactFeatureFlags.enableSuspenseCallback = true;
22 +
23 + React = require('react');
24 + ReactNoop = require('react-noop-renderer');
25 + Scheduler = require('scheduler');
26 + });
27 +
28 + function text(t) {
29 + return {text: t, hidden: false};
30 + }
31 +
32 + function createThenable() {
33 + let completed = false;
34 + const resolveRef = {current: null};
35 + let promise = {
36 + then(resolve, reject) {
37 + resolveRef.current = () => {
38 + completed = true;
39 + resolve();
40 + };
41 + },
42 + };
43 +
44 + const PromiseComp = () => {
45 + if (!completed) {
46 + throw promise;
47 + }
48 + return 'Done';
49 + };
50 + return {promise, resolveRef, PromiseComp};
51 + }
52 +
53 + it('1 then 0 suspense callback', () => {
54 + const {promise, resolveRef, PromiseComp} = createThenable();
55 +
56 + let ops = [];
57 + const suspenseCallback = thenables => {
58 + ops.push(thenables);
59 + };
60 +
61 + const element = (
62 + <React.Suspense suspenseCallback={suspenseCallback} fallback={'Waiting'}>
63 + <PromiseComp />
64 + </React.Suspense>
65 + );
66 +
67 + ReactNoop.render(element);
68 + expect(Scheduler).toFlushWithoutYielding();
69 + expect(ReactNoop.getChildren()).toEqual([text('Waiting')]);
70 + expect(ops).toEqual([new Set([promise])]);
71 + ops = [];
72 +
73 + resolveRef.current();
74 + expect(Scheduler).toFlushWithoutYielding();
75 + expect(ReactNoop.getChildren()).toEqual([text('Done')]);
76 + expect(ops).toEqual([]);
77 + });
78 +
79 + it('2 then 1 then 0 suspense callback', () => {
80 + const {
81 + promise: promise1,
82 + resolveRef: resolveRef1,
83 + PromiseComp: PromiseComp1,
84 + } = createThenable();
85 + const {
86 + promise: promise2,
87 + resolveRef: resolveRef2,
88 + PromiseComp: PromiseComp2,
89 + } = createThenable();
90 +
91 + let ops = [];
92 + const suspenseCallback1 = thenables => {
93 + ops.push(thenables);
94 + };
95 +
96 + const element = (
97 + <React.Suspense
98 + suspenseCallback={suspenseCallback1}
99 + fallback={'Waiting Tier 1'}>
100 + <PromiseComp1 />
101 + <PromiseComp2 />
102 + </React.Suspense>
103 + );
104 +
105 + ReactNoop.render(element);
106 + expect(Scheduler).toFlushWithoutYielding();
107 + expect(ReactNoop.getChildren()).toEqual([text('Waiting Tier 1')]);
108 + expect(ops).toEqual([new Set([promise1, promise2])]);
109 + ops = [];
110 +
111 + resolveRef1.current();
112 + ReactNoop.render(element);
113 + expect(Scheduler).toFlushWithoutYielding();
114 + expect(ReactNoop.getChildren()).toEqual([text('Waiting Tier 1')]);
115 + expect(ops).toEqual([new Set([promise2])]);
116 + ops = [];
117 +
118 + resolveRef2.current();
119 + ReactNoop.render(element);
120 + expect(Scheduler).toFlushWithoutYielding();
121 + expect(ReactNoop.getChildren()).toEqual([text('Done'), text('Done')]);
122 + expect(ops).toEqual([]);
123 + });
124 +
125 + it('nested suspense promises are reported only for their tier', () => {
126 + const {promise, PromiseComp} = createThenable();
127 +
128 + let ops1 = [];
129 + const suspenseCallback1 = thenables => {
130 + ops1.push(thenables);
131 + };
132 + let ops2 = [];
133 + const suspenseCallback2 = thenables => {
134 + ops2.push(thenables);
135 + };
136 +
137 + const element = (
138 + <React.Suspense
139 + suspenseCallback={suspenseCallback1}
140 + fallback={'Waiting Tier 1'}>
141 + <React.Suspense
142 + suspenseCallback={suspenseCallback2}
143 + fallback={'Waiting Tier 2'}>
144 + <PromiseComp />
145 + </React.Suspense>
146 + </React.Suspense>
147 + );
148 +
149 + ReactNoop.render(element);
150 + expect(Scheduler).toFlushWithoutYielding();
151 + expect(ReactNoop.getChildren()).toEqual([text('Waiting Tier 2')]);
152 + expect(ops1).toEqual([]);
153 + expect(ops2).toEqual([new Set([promise])]);
154 + });
155 +
156 + it('competing suspense promises', () => {
157 + const {
158 + promise: promise1,
159 + resolveRef: resolveRef1,
160 + PromiseComp: PromiseComp1,
161 + } = createThenable();
162 + const {
163 + promise: promise2,
164 + resolveRef: resolveRef2,
165 + PromiseComp: PromiseComp2,
166 + } = createThenable();
167 +
168 + let ops1 = [];
169 + const suspenseCallback1 = thenables => {
170 + ops1.push(thenables);
171 + };
172 + let ops2 = [];
173 + const suspenseCallback2 = thenables => {
174 + ops2.push(thenables);
175 + };
176 +
177 + const element = (
178 + <React.Suspense
179 + suspenseCallback={suspenseCallback1}
180 + fallback={'Waiting Tier 1'}>
181 + <React.Suspense
182 + suspenseCallback={suspenseCallback2}
183 + fallback={'Waiting Tier 2'}>
184 + <PromiseComp2 />
185 + </React.Suspense>
186 + <PromiseComp1 />
187 + </React.Suspense>
188 + );
189 +
190 + ReactNoop.render(element);
191 + expect(Scheduler).toFlushWithoutYielding();
192 + expect(ReactNoop.getChildren()).toEqual([text('Waiting Tier 1')]);
193 + expect(ops1).toEqual([new Set([promise1])]);
194 + expect(ops2).toEqual([]);
195 + ops1 = [];
196 + ops2 = [];
197 +
198 + resolveRef1.current();
199 + ReactNoop.render(element);
200 + expect(Scheduler).toFlushWithoutYielding();
201 + expect(ReactNoop.getChildren()).toEqual([
202 + text('Waiting Tier 2'),
203 + text('Done'),
204 + ]);
205 + expect(ops1).toEqual([]);
206 + expect(ops2).toEqual([new Set([promise2])]);
207 + ops1 = [];
208 + ops2 = [];
209 +
210 + resolveRef2.current();
211 + ReactNoop.render(element);
212 + expect(Scheduler).toFlushWithoutYielding();
213 + expect(ReactNoop.getChildren()).toEqual([text('Done'), text('Done')]);
214 + expect(ops1).toEqual([]);
215 + expect(ops2).toEqual([]);
216 + });
217 +});
packages/shared/ReactFeatureFlags.js
+5
@@ -78,3 +78,8 @@ export const revertPassiveEffectsChange = false;
78 // but without making them discrete. The flag exists in case it causes
79 // starvation problems.
80 export const enableUserBlockingEvents = false;
81 +
82 +// Add a callback property to suspense to notify which promises are currently
83 +// in the update queue. This allows reporting and tracing of what is causing
84 +// the user to see a loading state.
85 +export const enableSuspenseCallback = false;
packages/shared/forks/ReactFeatureFlags.native-fb.js
+1
@@ -37,6 +37,7 @@ export const enableJSXTransformAPI = false;
37 export const warnAboutMissingMockScheduler = true;
38 export const revertPassiveEffectsChange = false;
39 export const enableUserBlockingEvents = false;
40 +export const enableSuspenseCallback = false;
41
42 // Only used in www builds.
43 export function addUserTimingListener() {
packages/shared/forks/ReactFeatureFlags.native-oss.js
+1
@@ -32,6 +32,7 @@ export const enableJSXTransformAPI = false;
32 export const warnAboutMissingMockScheduler = false;
33 export const revertPassiveEffectsChange = false;
34 export const enableUserBlockingEvents = false;
35 +export const enableSuspenseCallback = false;
36
37 // Only used in www builds.
38 export function addUserTimingListener() {
packages/shared/forks/ReactFeatureFlags.persistent.js
+1
@@ -32,6 +32,7 @@ export const enableJSXTransformAPI = false;
32 export const warnAboutMissingMockScheduler = true;
33 export const revertPassiveEffectsChange = false;
34 export const enableUserBlockingEvents = false;
35 +export const enableSuspenseCallback = false;
36
37 // Only used in www builds.
38 export function addUserTimingListener() {
packages/shared/forks/ReactFeatureFlags.test-renderer.js
+1
@@ -32,6 +32,7 @@ export const enableJSXTransformAPI = false;
32 export const warnAboutMissingMockScheduler = false;
33 export const revertPassiveEffectsChange = false;
34 export const enableUserBlockingEvents = false;
35 +export const enableSuspenseCallback = false;
36
37 // Only used in www builds.
38 export function addUserTimingListener() {
packages/shared/forks/ReactFeatureFlags.test-renderer.www.js
+1
@@ -32,6 +32,7 @@ export const enableFundamentalAPI = false;
32 export const enableJSXTransformAPI = true;
33 export const warnAboutMissingMockScheduler = true;
34 export const enableUserBlockingEvents = false;
35 +export const enableSuspenseCallback = true;
36
37 // Only used in www builds.
38 export function addUserTimingListener() {
packages/shared/forks/ReactFeatureFlags.www.js
+2
@@ -76,6 +76,8 @@ export const enableJSXTransformAPI = true;
76
77 export const warnAboutMissingMockScheduler = true;
78
79 +export const enableSuspenseCallback = true;
80 +
81 // Flow magic to verify the exports of this file match the original version.
82 // eslint-disable-next-line no-unused-vars
83 type Check<_X, Y: _X, X: Y = _X> = null;