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

3 files changed

Lines changed: 140 additions & 0 deletions

File tree

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

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,18 @@ describe('React hooks DevTools integration', () => {
1414
letReact;
1515
letReactDebugTools;
1616
letReactTestRenderer;
17+
letScheduler;
1718
letact;
1819
letoverrideHookState;
20+
letscheduleUpdate;
21+
letsetSuspenseHandler;
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+
functionMyComponent(){
190+
return'Done';
191+
}
192+
193+
constrenderer=ReactTestRenderer.create(
194+
<div>
195+
<React.Suspensefallback={'Loading'}>
196+
<MyComponent/>
197+
</React.Suspense>
198+
</div>,
199+
);
200+
constfiber=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+
functionMyComponent(){
243+
return'Done';
244+
}
245+
246+
constrenderer=ReactTestRenderer.create(
247+
<div>
248+
<React.Suspensefallback={'Loading'}>
249+
<MyComponent/>
250+
</React.Suspense>
251+
</div>,
252+
{unstable_isConcurrent: true},
253+
);
254+
expect(Scheduler).toFlushAndYield([]);
255+
constfiber=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‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ import {
9696
registerSuspenseInstanceRetry,
9797
}from'./ReactFiberHostConfig';
9898
importtype{SuspenseInstance}from'./ReactFiberHostConfig';
99+
import{shouldSuspend}from'./ReactFiberReconciler';
99100
import{
100101
pushHostContext,
101102
pushHostContainer,
@@ -1392,6 +1393,12 @@ function updateSuspenseComponent(
13921393
constmode=workInProgress.mode;
13931394
constnextProps=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
letnextState: SuspenseState|null=workInProgress.memoizedState;

‎packages/react-reconciler/src/ReactFiberReconciler.js‎

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -340,8 +340,16 @@ export function findHostInstanceWithNoPortals(
340340
returnhostFiber.stateNode;
341341
}
342342

343+
letshouldSuspendImpl=fiber=>false;
344+
345+
exportfunctionshouldSuspend(fiber: Fiber): boolean{
346+
returnshouldSuspendImpl(fiber);
347+
}
348+
343349
letoverrideHookState=null;
344350
letoverrideProps=null;
351+
letscheduleUpdate=null;
352+
letsetSuspenseHandler=null;
345353

346354
if(__DEV__){
347355
constcopyWithSetImpl=(
@@ -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
exportfunctioninjectIntoDevTools(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)