Allow DevTools to toggle Suspense fallbacks (#15232)
* Allow DevTools to toggle Suspense state * Change API to overrideSuspense This lets detect support for overriding Suspense from DevTools. * Add ConcurrentMode test * Newlines * Remove unnecessary change * Naming changes
Dan Abramov committed
Apr 4, 2019 at 15:32 UTC
7a2dc4853948a70a599124b7f47513df793d472a
3 files changed
+140
packages/react-debug-tools/src/__tests__/ReactDevToolsHooksIntegration-test.js
+114
@@ -14,13 +14,18 @@ describe('React hooks DevTools integration', () => {
14
let React;
15
let ReactDebugTools;
16
let ReactTestRenderer;
17
+ let Scheduler;
18
let act;
19
let overrideHookState;
20
+ let scheduleUpdate;
21
+ let setSuspenseHandler;
22
23
beforeEach(() => {
24
global.__REACT_DEVTOOLS_GLOBAL_HOOK__ = {
25
inject: injected => {
26
overrideHookState = injected.overrideHookState;
27
+ scheduleUpdate = injected.scheduleUpdate;
28
+ setSuspenseHandler = injected.setSuspenseHandler;
29
},
30
supportsFiber: true,
31
onCommitFiberRoot: () => {},
@@ -32,6 +37,7 @@ describe('React hooks DevTools integration', () => {
37
React = require('react');
38
ReactDebugTools = require('react-debug-tools');
39
ReactTestRenderer = require('react-test-renderer');
40
+ Scheduler = require('scheduler');
41
42
act = ReactTestRenderer.act;
43
});
@@ -173,4 +179,112 @@ describe('React hooks DevTools integration', () => {
179
});
180
}
181
});
182
+
183
+ it('should support overriding suspense in sync mode', () => {
184
+ if (__DEV__) {
185
+ // Lock the first render
186
+ setSuspenseHandler(() => true);
187
+ }
188
+
189
+ function MyComponent() {
190
+ return 'Done';
191
+ }
192
+
193
+ const renderer = ReactTestRenderer.create(
194
+ <div>
195
+ <React.Suspense fallback={'Loading'}>
196
+ <MyComponent />
197
+ </React.Suspense>
198
+ </div>,
199
+ );
200
+ const fiber = renderer.root._currentFiber().child;
201
+ if (__DEV__) {
202
+ // First render was locked
203
+ expect(renderer.toJSON().children).toEqual(['Loading']);
204
+ scheduleUpdate(fiber); // Re-render
205
+ expect(renderer.toJSON().children).toEqual(['Loading']);
206
+
207
+ // Release the lock
208
+ setSuspenseHandler(() => false);
209
+ scheduleUpdate(fiber); // Re-render
210
+ expect(renderer.toJSON().children).toEqual(['Done']);
211
+ scheduleUpdate(fiber); // Re-render
212
+ expect(renderer.toJSON().children).toEqual(['Done']);
213
+
214
+ // Lock again
215
+ setSuspenseHandler(() => true);
216
+ scheduleUpdate(fiber); // Re-render
217
+ expect(renderer.toJSON().children).toEqual(['Loading']);
218
+
219
+ // Release the lock again
220
+ setSuspenseHandler(() => false);
221
+ scheduleUpdate(fiber); // Re-render
222
+ expect(renderer.toJSON().children).toEqual(['Done']);
223
+
224
+ // Ensure it checks specific fibers.
225
+ setSuspenseHandler(f => f === fiber || f === fiber.alternate);
226
+ scheduleUpdate(fiber); // Re-render
227
+ expect(renderer.toJSON().children).toEqual(['Loading']);
228
+ setSuspenseHandler(f => f !== fiber && f !== fiber.alternate);
229
+ scheduleUpdate(fiber); // Re-render
230
+ expect(renderer.toJSON().children).toEqual(['Done']);
231
+ } else {
232
+ expect(renderer.toJSON().children).toEqual(['Done']);
233
+ }
234
+ });
235
+
236
+ it('should support overriding suspense in concurrent mode', () => {
237
+ if (__DEV__) {
238
+ // Lock the first render
239
+ setSuspenseHandler(() => true);
240
+ }
241
+
242
+ function MyComponent() {
243
+ return 'Done';
244
+ }
245
+
246
+ const renderer = ReactTestRenderer.create(
247
+ <div>
248
+ <React.Suspense fallback={'Loading'}>
249
+ <MyComponent />
250
+ </React.Suspense>
251
+ </div>,
252
+ {unstable_isConcurrent: true},
253
+ );
254
+ expect(Scheduler).toFlushAndYield([]);
255
+ const fiber = renderer.root._currentFiber().child;
256
+ if (__DEV__) {
257
+ // First render was locked
258
+ expect(renderer.toJSON().children).toEqual(['Loading']);
259
+ scheduleUpdate(fiber); // Re-render
260
+ expect(renderer.toJSON().children).toEqual(['Loading']);
261
+
262
+ // Release the lock
263
+ setSuspenseHandler(() => false);
264
+ scheduleUpdate(fiber); // Re-render
265
+ expect(renderer.toJSON().children).toEqual(['Done']);
266
+ scheduleUpdate(fiber); // Re-render
267
+ expect(renderer.toJSON().children).toEqual(['Done']);
268
+
269
+ // Lock again
270
+ setSuspenseHandler(() => true);
271
+ scheduleUpdate(fiber); // Re-render
272
+ expect(renderer.toJSON().children).toEqual(['Loading']);
273
+
274
+ // Release the lock again
275
+ setSuspenseHandler(() => false);
276
+ scheduleUpdate(fiber); // Re-render
277
+ expect(renderer.toJSON().children).toEqual(['Done']);
278
+
279
+ // Ensure it checks specific fibers.
280
+ setSuspenseHandler(f => f === fiber || f === fiber.alternate);
281
+ scheduleUpdate(fiber); // Re-render
282
+ expect(renderer.toJSON().children).toEqual(['Loading']);
283
+ setSuspenseHandler(f => f !== fiber && f !== fiber.alternate);
284
+ scheduleUpdate(fiber); // Re-render
285
+ expect(renderer.toJSON().children).toEqual(['Done']);
286
+ } else {
287
+ expect(renderer.toJSON().children).toEqual(['Done']);
288
+ }
289
+ });
290
});
packages/react-reconciler/src/ReactFiberBeginWork.js
+7
@@ -96,6 +96,7 @@ import {
96
registerSuspenseInstanceRetry,
97
} from './ReactFiberHostConfig';
98
import type {SuspenseInstance} from './ReactFiberHostConfig';
99
+import {shouldSuspend} from './ReactFiberReconciler';
100
import {
101
pushHostContext,
102
pushHostContainer,
@@ -1392,6 +1393,12 @@ function updateSuspenseComponent(
1393
const mode = workInProgress.mode;
1394
const nextProps = workInProgress.pendingProps;
1395
1396
+ if (__DEV__) {
1397
+ if (shouldSuspend(workInProgress)) {
1398
+ workInProgress.effectTag |= DidCapture;
1399
+ }
1400
+ }
1401
+
1402
// We should attempt to render the primary children unless this boundary
1403
// already suspended during this render (`alreadyCaptured` is true).
1404
let nextState: SuspenseState | null = workInProgress.memoizedState;
packages/react-reconciler/src/ReactFiberReconciler.js
+19
@@ -340,8 +340,16 @@ export function findHostInstanceWithNoPortals(
340
return hostFiber.stateNode;
341
}
342
343
+let shouldSuspendImpl = fiber => false;
344
+
345
+export function shouldSuspend(fiber: Fiber): boolean {
346
+ return shouldSuspendImpl(fiber);
347
+}
348
+
349
let overrideHookState = null;
350
let overrideProps = null;
351
+let scheduleUpdate = null;
352
+let setSuspenseHandler = null;
353
354
if (__DEV__) {
355
const copyWithSetImpl = (
@@ -409,6 +417,15 @@ if (__DEV__) {
417
}
418
scheduleWork(fiber, Sync);
419
};
420
+
421
+ scheduleUpdate = (fiber: Fiber) => {
422
+ flushPassiveEffects();
423
+ scheduleWork(fiber, Sync);
424
+ };
425
+
426
+ setSuspenseHandler = (newShouldSuspendImpl: Fiber => boolean) => {
427
+ shouldSuspendImpl = newShouldSuspendImpl;
428
+ };
429
}
430
431
export function injectIntoDevTools(devToolsConfig: DevToolsConfig): boolean {
@@ -419,6 +436,8 @@ export function injectIntoDevTools(devToolsConfig: DevToolsConfig): boolean {
436
...devToolsConfig,
437
overrideHookState,
438
overrideProps,
439
+ setSuspenseHandler,
440
+ scheduleUpdate,
441
currentDispatcherRef: ReactCurrentDispatcher,
442
findHostInstanceByFiber(fiber: Fiber): Instance | TextInstance | null {
443
const hostFiber = findCurrentHostFiber(fiber);