Skip to content

Commit 7a2dc48

Browse files
authored
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
1 parent e221972 commit 7a2dc48

File tree

3 files changed

+140
-0
lines changed

3 files changed

+140
-0
lines changed

packages/react-debug-tools/src/__tests__/ReactDevToolsHooksIntegration-test.js

+114
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,18 @@ describe('React hooks DevTools integration', () => {
1414
let React;
1515
let ReactDebugTools;
1616
let ReactTestRenderer;
17+
let Scheduler;
1718
let act;
1819
let overrideHookState;
20+
let scheduleUpdate;
21+
let setSuspenseHandler;
1922

2023
beforeEach(() => {
2124
global.__REACT_DEVTOOLS_GLOBAL_HOOK__ = {
2225
inject: injected => {
2326
overrideHookState = injected.overrideHookState;
27+
scheduleUpdate = injected.scheduleUpdate;
28+
setSuspenseHandler = injected.setSuspenseHandler;
2429
},
2530
supportsFiber: true,
2631
onCommitFiberRoot: () => {},
@@ -32,6 +37,7 @@ describe('React hooks DevTools integration', () => {
3237
React = require('react');
3338
ReactDebugTools = require('react-debug-tools');
3439
ReactTestRenderer = require('react-test-renderer');
40+
Scheduler = require('scheduler');
3541

3642
act = ReactTestRenderer.act;
3743
});
@@ -173,4 +179,112 @@ describe('React hooks DevTools integration', () => {
173179
});
174180
}
175181
});
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+
});
176290
});

packages/react-reconciler/src/ReactFiberBeginWork.js

+7
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ import {
9696
registerSuspenseInstanceRetry,
9797
} from './ReactFiberHostConfig';
9898
import type {SuspenseInstance} from './ReactFiberHostConfig';
99+
import {shouldSuspend} from './ReactFiberReconciler';
99100
import {
100101
pushHostContext,
101102
pushHostContainer,
@@ -1392,6 +1393,12 @@ function updateSuspenseComponent(
13921393
const mode = workInProgress.mode;
13931394
const nextProps = workInProgress.pendingProps;
13941395

1396+
if (__DEV__) {
1397+
if (shouldSuspend(workInProgress)) {
1398+
workInProgress.effectTag |= DidCapture;
1399+
}
1400+
}
1401+
13951402
// We should attempt to render the primary children unless this boundary
13961403
// already suspended during this render (`alreadyCaptured` is true).
13971404
let nextState: SuspenseState | null = workInProgress.memoizedState;

packages/react-reconciler/src/ReactFiberReconciler.js

+19
Original file line numberDiff line numberDiff line change
@@ -340,8 +340,16 @@ export function findHostInstanceWithNoPortals(
340340
return hostFiber.stateNode;
341341
}
342342

343+
let shouldSuspendImpl = fiber => false;
344+
345+
export function shouldSuspend(fiber: Fiber): boolean {
346+
return shouldSuspendImpl(fiber);
347+
}
348+
343349
let overrideHookState = null;
344350
let overrideProps = null;
351+
let scheduleUpdate = null;
352+
let setSuspenseHandler = null;
345353

346354
if (__DEV__) {
347355
const copyWithSetImpl = (
@@ -409,6 +417,15 @@ if (__DEV__) {
409417
}
410418
scheduleWork(fiber, Sync);
411419
};
420+
421+
scheduleUpdate = (fiber: Fiber) => {
422+
flushPassiveEffects();
423+
scheduleWork(fiber, Sync);
424+
};
425+
426+
setSuspenseHandler = (newShouldSuspendImpl: Fiber => boolean) => {
427+
shouldSuspendImpl = newShouldSuspendImpl;
428+
};
412429
}
413430

414431
export function injectIntoDevTools(devToolsConfig: DevToolsConfig): boolean {
@@ -419,6 +436,8 @@ export function injectIntoDevTools(devToolsConfig: DevToolsConfig): boolean {
419436
...devToolsConfig,
420437
overrideHookState,
421438
overrideProps,
439+
setSuspenseHandler,
440+
scheduleUpdate,
422441
currentDispatcherRef: ReactCurrentDispatcher,
423442
findHostInstanceByFiber(fiber: Fiber): Instance | TextInstance | null {
424443
const hostFiber = findCurrentHostFiber(fiber);

0 commit comments

Comments
 (0)