Skip to content

Commit 8309724

Browse files
authored
[Fiber][DevTools] Add scheduleRetry to DevTools Hook (facebook#34635)
When forcing suspense/error we're doing that by scheduling a sync update on the fiber. Resuspending a Suspense boundary can only happen sync update so that makes sense. Erroring also forces a sync commit. This means that no View Transitions fire. However, unsuspending (and dismissing an error dialog) can be async so the reveal should be able to be async. This adds another hook for scheduling using the Retry lane. That way when you play through a reveal sequence of Suspense boundaries (like playing through the timeline), it'll run the animations that would've ran during a loading sequence.
1 parent 09d3cd8 commit 8309724

File tree

5 files changed

+76
-11
lines changed

5 files changed

+76
-11
lines changed

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

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ describe('React hooks DevTools integration', () => {
1717
let act;
1818
let overrideHookState;
1919
let scheduleUpdate;
20+
let scheduleRetry;
2021
let setSuspenseHandler;
2122
let waitForAll;
2223

@@ -27,6 +28,7 @@ describe('React hooks DevTools integration', () => {
2728
inject: injected => {
2829
overrideHookState = injected.overrideHookState;
2930
scheduleUpdate = injected.scheduleUpdate;
31+
scheduleRetry = injected.scheduleRetry;
3032
setSuspenseHandler = injected.setSuspenseHandler;
3133
},
3234
supportsFiber: true,
@@ -312,5 +314,17 @@ describe('React hooks DevTools integration', () => {
312314
} else {
313315
expect(renderer.toJSON().children).toEqual(['Done']);
314316
}
317+
318+
if (scheduleRetry) {
319+
// Lock again, synchronously
320+
setSuspenseHandler(() => true);
321+
await act(() => scheduleUpdate(fiber)); // Re-render
322+
expect(renderer.toJSON().children).toEqual(['Loading']);
323+
324+
// Release the lock again but this time using retry lane
325+
setSuspenseHandler(() => false);
326+
await act(() => scheduleRetry(fiber)); // Re-render
327+
expect(renderer.toJSON().children).toEqual(['Done']);
328+
}
315329
});
316330
});

packages/react-devtools-shared/src/__tests__/store-test.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -838,7 +838,7 @@ describe('Store', () => {
838838
<Suspense name="two" rects={null}>
839839
<Suspense name="three" rects={null}>
840840
`);
841-
await act(() =>
841+
await actAsync(() =>
842842
agent.overrideSuspense({
843843
id: store.getElementIDAtIndex(2),
844844
rendererID,

packages/react-devtools-shared/src/backend/fiber/renderer.js

Lines changed: 48 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1065,6 +1065,7 @@ export function attach(
10651065
setErrorHandler,
10661066
setSuspenseHandler,
10671067
scheduleUpdate,
1068+
scheduleRetry,
10681069
getCurrentFiber,
10691070
} = renderer;
10701071
const supportsTogglingError =
@@ -7754,7 +7755,13 @@ export function attach(
77547755
// First override is added. Switch React to slower path.
77557756
setErrorHandler(shouldErrorFiberAccordingToMap);
77567757
}
7757-
scheduleUpdate(fiber);
7758+
if (!forceError && typeof scheduleRetry === 'function') {
7759+
// If we're dismissing an error and the renderer supports it, use a Retry instead of Sync
7760+
// This would allow View Transitions to proceed as if the error was dismissed using a Transition.
7761+
scheduleRetry(fiber);
7762+
} else {
7763+
scheduleUpdate(fiber);
7764+
}
77587765
}
77597766
77607767
function shouldSuspendFiberAlwaysFalse() {
@@ -7812,7 +7819,13 @@ export function attach(
78127819
setSuspenseHandler(shouldSuspendFiberAlwaysFalse);
78137820
}
78147821
}
7815-
scheduleUpdate(fiber);
7822+
if (!forceFallback && typeof scheduleRetry === 'function') {
7823+
// If we're unsuspending and the renderer supports it, use a Retry instead of Sync
7824+
// to allow for things like View Transitions to proceed the way they would for real.
7825+
scheduleRetry(fiber);
7826+
} else {
7827+
scheduleUpdate(fiber);
7828+
}
78167829
}
78177830
78187831
/**
@@ -7834,11 +7847,10 @@ export function attach(
78347847
}
78357848
78367849
// TODO: Allow overriding the timeline for the specified root.
7837-
forceFallbackForFibers.forEach(fiber => {
7838-
scheduleUpdate(fiber);
7839-
});
7840-
forceFallbackForFibers.clear();
78417850
7851+
const unsuspendedSet: Set<Fiber> = new Set(forceFallbackForFibers);
7852+
7853+
let resuspended = false;
78427854
for (let i = 0; i < suspendedSet.length; ++i) {
78437855
const instance = idToDevToolsInstanceMap.get(suspendedSet[i]);
78447856
if (instance === undefined) {
@@ -7850,15 +7862,41 @@ export function attach(
78507862

78517863
if (instance.kind === FIBER_INSTANCE) {
78527864
const fiber = instance.data;
7853-
forceFallbackForFibers.add(fiber);
7854-
// We could find a minimal set that covers all the Fibers in this suspended set.
7855-
// For now we rely on React's batching of updates.
7856-
scheduleUpdate(fiber);
7865+
if (
7866+
forceFallbackForFibers.has(fiber) ||
7867+
(fiber.alternate !== null &&
7868+
forceFallbackForFibers.has(fiber.alternate))
7869+
) {
7870+
// We're already forcing fallback for this fiber. Mark it as not unsuspended.
7871+
unsuspendedSet.delete(fiber);
7872+
if (fiber.alternate !== null) {
7873+
unsuspendedSet.delete(fiber.alternate);
7874+
}
7875+
} else {
7876+
forceFallbackForFibers.add(fiber);
7877+
// We could find a minimal set that covers all the Fibers in this suspended set.
7878+
// For now we rely on React's batching of updates.
7879+
scheduleUpdate(fiber);
7880+
resuspended = true;
7881+
}
78577882
} else {
78587883
console.warn(`Cannot not suspend ID '${suspendedSet[i]}'.`);
78597884
}
78607885
}
78617886

7887+
// Unsuspend any existing forced fallbacks if they're not in the new set.
7888+
unsuspendedSet.forEach(fiber => {
7889+
forceFallbackForFibers.delete(fiber);
7890+
if (!resuspended && typeof scheduleRetry === 'function') {
7891+
// If nothing new resuspended we don't need this to be sync. If we're only
7892+
// unsuspending then we can schedule this as a Retry if the renderer supports it.
7893+
// That way we can trigger animations.
7894+
scheduleRetry(fiber);
7895+
} else {
7896+
scheduleUpdate(fiber);
7897+
}
7898+
});
7899+
78627900
if (forceFallbackForFibers.size > 0) {
78637901
// First override is added. Switch React to slower path.
78647902
// TODO: Semantics for suspending a timeline are different. We want a suspended

packages/react-devtools-shared/src/backend/types.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,8 @@ export type ReactRenderer = {
155155
) => void,
156156
// 16.9+
157157
scheduleUpdate?: ?(fiber: Object) => void,
158+
// 19.2+
159+
scheduleRetry?: ?(fiber: Object) => void,
158160
setSuspenseHandler?: ?(shouldSuspend: (fiber: Object) => boolean) => void,
159161
// Only injected by React v16.8+ in order to support hooks inspection.
160162
currentDispatcherRef?: LegacyDispatcherRef | CurrentDispatcherRef,

packages/react-reconciler/src/ReactFiberReconciler.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ import {
9898
getHighestPriorityPendingLanes,
9999
higherPriorityLane,
100100
getBumpedLaneForHydrationByLane,
101+
claimNextRetryLane,
101102
} from './ReactFiberLane';
102103
import {
103104
scheduleRefresh,
@@ -599,6 +600,7 @@ let overrideProps = null;
599600
let overridePropsDeletePath = null;
600601
let overridePropsRenamePath = null;
601602
let scheduleUpdate = null;
603+
let scheduleRetry = null;
602604
let setErrorHandler = null;
603605
let setSuspenseHandler = null;
604606

@@ -835,6 +837,14 @@ if (__DEV__) {
835837
}
836838
};
837839

840+
scheduleRetry = (fiber: Fiber) => {
841+
const lane = claimNextRetryLane();
842+
const root = enqueueConcurrentRenderForLane(fiber, lane);
843+
if (root !== null) {
844+
scheduleUpdateOnFiber(root, fiber, lane);
845+
}
846+
};
847+
838848
setErrorHandler = (newShouldErrorImpl: Fiber => ?boolean) => {
839849
shouldErrorImpl = newShouldErrorImpl;
840850
};
@@ -886,6 +896,7 @@ export function injectIntoDevTools(): boolean {
886896
internals.overridePropsDeletePath = overridePropsDeletePath;
887897
internals.overridePropsRenamePath = overridePropsRenamePath;
888898
internals.scheduleUpdate = scheduleUpdate;
899+
internals.scheduleRetry = scheduleRetry;
889900
internals.setErrorHandler = setErrorHandler;
890901
internals.setSuspenseHandler = setSuspenseHandler;
891902
// React Refresh

0 commit comments

Comments
 (0)