diff --git a/packages/react-debug-tools/src/__tests__/ReactDevToolsHooksIntegration-test.js b/packages/react-debug-tools/src/__tests__/ReactDevToolsHooksIntegration-test.js
index 113524f832af5..f7ea7ffebc031 100644
--- a/packages/react-debug-tools/src/__tests__/ReactDevToolsHooksIntegration-test.js
+++ b/packages/react-debug-tools/src/__tests__/ReactDevToolsHooksIntegration-test.js
@@ -14,13 +14,18 @@ describe('React hooks DevTools integration', () => {
let React;
let ReactDebugTools;
let ReactTestRenderer;
+ let Scheduler;
let act;
let overrideHookState;
+ let scheduleUpdate;
+ let setSuspenseHandler;
beforeEach(() => {
global.__REACT_DEVTOOLS_GLOBAL_HOOK__ = {
inject: injected => {
overrideHookState = injected.overrideHookState;
+ scheduleUpdate = injected.scheduleUpdate;
+ setSuspenseHandler = injected.setSuspenseHandler;
},
supportsFiber: true,
onCommitFiberRoot: () => {},
@@ -32,6 +37,7 @@ describe('React hooks DevTools integration', () => {
React = require('react');
ReactDebugTools = require('react-debug-tools');
ReactTestRenderer = require('react-test-renderer');
+ Scheduler = require('scheduler');
act = ReactTestRenderer.act;
});
@@ -173,4 +179,112 @@ describe('React hooks DevTools integration', () => {
});
}
});
+
+ it('should support overriding suspense in sync mode', () => {
+ if (__DEV__) {
+ // Lock the first render
+ setSuspenseHandler(() => true);
+ }
+
+ function MyComponent() {
+ return 'Done';
+ }
+
+ const renderer = ReactTestRenderer.create(
+
+
+
+
+
,
+ );
+ const fiber = renderer.root._currentFiber().child;
+ if (__DEV__) {
+ // First render was locked
+ expect(renderer.toJSON().children).toEqual(['Loading']);
+ scheduleUpdate(fiber); // Re-render
+ expect(renderer.toJSON().children).toEqual(['Loading']);
+
+ // Release the lock
+ setSuspenseHandler(() => false);
+ scheduleUpdate(fiber); // Re-render
+ expect(renderer.toJSON().children).toEqual(['Done']);
+ scheduleUpdate(fiber); // Re-render
+ expect(renderer.toJSON().children).toEqual(['Done']);
+
+ // Lock again
+ setSuspenseHandler(() => true);
+ scheduleUpdate(fiber); // Re-render
+ expect(renderer.toJSON().children).toEqual(['Loading']);
+
+ // Release the lock again
+ setSuspenseHandler(() => false);
+ scheduleUpdate(fiber); // Re-render
+ expect(renderer.toJSON().children).toEqual(['Done']);
+
+ // Ensure it checks specific fibers.
+ setSuspenseHandler(f => f === fiber || f === fiber.alternate);
+ scheduleUpdate(fiber); // Re-render
+ expect(renderer.toJSON().children).toEqual(['Loading']);
+ setSuspenseHandler(f => f !== fiber && f !== fiber.alternate);
+ scheduleUpdate(fiber); // Re-render
+ expect(renderer.toJSON().children).toEqual(['Done']);
+ } else {
+ expect(renderer.toJSON().children).toEqual(['Done']);
+ }
+ });
+
+ it('should support overriding suspense in concurrent mode', () => {
+ if (__DEV__) {
+ // Lock the first render
+ setSuspenseHandler(() => true);
+ }
+
+ function MyComponent() {
+ return 'Done';
+ }
+
+ const renderer = ReactTestRenderer.create(
+
+
+
+
+
,
+ {unstable_isConcurrent: true},
+ );
+ expect(Scheduler).toFlushAndYield([]);
+ const fiber = renderer.root._currentFiber().child;
+ if (__DEV__) {
+ // First render was locked
+ expect(renderer.toJSON().children).toEqual(['Loading']);
+ scheduleUpdate(fiber); // Re-render
+ expect(renderer.toJSON().children).toEqual(['Loading']);
+
+ // Release the lock
+ setSuspenseHandler(() => false);
+ scheduleUpdate(fiber); // Re-render
+ expect(renderer.toJSON().children).toEqual(['Done']);
+ scheduleUpdate(fiber); // Re-render
+ expect(renderer.toJSON().children).toEqual(['Done']);
+
+ // Lock again
+ setSuspenseHandler(() => true);
+ scheduleUpdate(fiber); // Re-render
+ expect(renderer.toJSON().children).toEqual(['Loading']);
+
+ // Release the lock again
+ setSuspenseHandler(() => false);
+ scheduleUpdate(fiber); // Re-render
+ expect(renderer.toJSON().children).toEqual(['Done']);
+
+ // Ensure it checks specific fibers.
+ setSuspenseHandler(f => f === fiber || f === fiber.alternate);
+ scheduleUpdate(fiber); // Re-render
+ expect(renderer.toJSON().children).toEqual(['Loading']);
+ setSuspenseHandler(f => f !== fiber && f !== fiber.alternate);
+ scheduleUpdate(fiber); // Re-render
+ expect(renderer.toJSON().children).toEqual(['Done']);
+ } else {
+ expect(renderer.toJSON().children).toEqual(['Done']);
+ }
+ });
});
diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.js b/packages/react-reconciler/src/ReactFiberBeginWork.js
index e6b04911a7025..2832021950f69 100644
--- a/packages/react-reconciler/src/ReactFiberBeginWork.js
+++ b/packages/react-reconciler/src/ReactFiberBeginWork.js
@@ -96,6 +96,7 @@ import {
registerSuspenseInstanceRetry,
} from './ReactFiberHostConfig';
import type {SuspenseInstance} from './ReactFiberHostConfig';
+import {shouldSuspend} from './ReactFiberReconciler';
import {
pushHostContext,
pushHostContainer,
@@ -1392,6 +1393,12 @@ function updateSuspenseComponent(
const mode = workInProgress.mode;
const nextProps = workInProgress.pendingProps;
+ if (__DEV__) {
+ if (shouldSuspend(workInProgress)) {
+ workInProgress.effectTag |= DidCapture;
+ }
+ }
+
// We should attempt to render the primary children unless this boundary
// already suspended during this render (`alreadyCaptured` is true).
let nextState: SuspenseState | null = workInProgress.memoizedState;
diff --git a/packages/react-reconciler/src/ReactFiberReconciler.js b/packages/react-reconciler/src/ReactFiberReconciler.js
index b5e1182376523..77fadd31e9b2e 100644
--- a/packages/react-reconciler/src/ReactFiberReconciler.js
+++ b/packages/react-reconciler/src/ReactFiberReconciler.js
@@ -340,8 +340,16 @@ export function findHostInstanceWithNoPortals(
return hostFiber.stateNode;
}
+let shouldSuspendImpl = fiber => false;
+
+export function shouldSuspend(fiber: Fiber): boolean {
+ return shouldSuspendImpl(fiber);
+}
+
let overrideHookState = null;
let overrideProps = null;
+let scheduleUpdate = null;
+let setSuspenseHandler = null;
if (__DEV__) {
const copyWithSetImpl = (
@@ -409,6 +417,15 @@ if (__DEV__) {
}
scheduleWork(fiber, Sync);
};
+
+ scheduleUpdate = (fiber: Fiber) => {
+ flushPassiveEffects();
+ scheduleWork(fiber, Sync);
+ };
+
+ setSuspenseHandler = (newShouldSuspendImpl: Fiber => boolean) => {
+ shouldSuspendImpl = newShouldSuspendImpl;
+ };
}
export function injectIntoDevTools(devToolsConfig: DevToolsConfig): boolean {
@@ -419,6 +436,8 @@ export function injectIntoDevTools(devToolsConfig: DevToolsConfig): boolean {
...devToolsConfig,
overrideHookState,
overrideProps,
+ setSuspenseHandler,
+ scheduleUpdate,
currentDispatcherRef: ReactCurrentDispatcher,
findHostInstanceByFiber(fiber: Fiber): Instance | TextInstance | null {
const hostFiber = findCurrentHostFiber(fiber);