@samitouri / QOS-React / commits / 7a2dc48539

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);