@samitouri / QOS-React / commits / 5d49b2b7f4

[Fiber] Track SuspendedState on stack instead of global (#34486)

Stacked on #34481. We currently track the suspended state temporarily with a global which is safe as long as we always read it during a sync pass. However, we sometimes read it in closures and then we have to be carefully to pass the right one since it's possible another commit on a different root has started at that point. This avoids this footgun. Another reason to do this is that I want to read it in `startViewTransition` which is in an async gap after which point it's no longer safe. So I have to pass that through the `commitRoot` bound function.

Sebastian Markbåge committed Sep 15, 2025 at 16:10 UTC 5d49b2b7f42b6aebe749914fbd06640ded63c001
10 files changed +165 -88
packages/react-art/src/ReactFiberConfigART.js
+5 -3
@@ -609,11 +609,13 @@ export function preloadInstance(type, props) {
609 return true;
610 }
611
612 -export function startSuspendingCommit() {}
612 +export function startSuspendingCommit() {
613 + return null;
614 +}
615
614 -export function suspendInstance(instance, type, props) {}
616 +export function suspendInstance(state, instance, type, props) {}
617
616 -export function suspendOnActiveViewTransition(container) {}
618 +export function suspendOnActiveViewTransition(state, container) {}
619
620 export function waitForCommitToBeReady(timeoutOffset) {
621 return null;
packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js
+12 -31
@@ -2081,6 +2081,7 @@ function forceLayout(ownerDocument: Document) {
2081 }
2082
2083 export function startViewTransition(
2084 + suspendedState: null | SuspendedState,
2085 rootContainer: Container,
2086 transitionTypes: null | TransitionTypes,
2087 mutationCallback: () => void,
@@ -2443,6 +2444,7 @@ function animateGesture(
2444 }
2445
2446 export function startGestureTransition(
2447 + suspendedState: null | SuspendedState,
2448 rootContainer: Container,
2449 timeline: GestureTimeline,
2450 rangeStart: number,
@@ -5904,7 +5906,7 @@ export function preloadResource(resource: Resource): boolean {
5906 return true;
5907 }
5908
5907 -type SuspendedState = {
5909 +export opaque type SuspendedState = {
5910 stylesheets: null | Map<StylesheetResource, HoistableRoot>,
5911 count: number, // suspensey css and active view transitions
5912 imgCount: number, // suspensey images
@@ -5912,10 +5914,9 @@ type SuspendedState = {
5914 waitingForImages: boolean, // false when we're no longer blocking on images
5915 unsuspend: null | (() => void),
5916 };
5915 -let suspendedState: null | SuspendedState = null;
5917
5917 -export function startSuspendingCommit(): void {
5918 - suspendedState = {
5918 +export function startSuspendingCommit(): SuspendedState {
5919 + return {
5920 stylesheets: null,
5921 count: 0,
5922 imgCount: 0,
@@ -5930,6 +5931,7 @@ export function startSuspendingCommit(): void {
5931 }
5932
5933 export function suspendInstance(
5934 + state: SuspendedState,
5935 instance: Instance,
5936 type: Type,
5937 props: Props,
@@ -5937,12 +5939,6 @@ export function suspendInstance(
5939 if (!enableSuspenseyImages && !enableViewTransition) {
5940 return;
5941 }
5940 - if (suspendedState === null) {
5941 - throw new Error(
5942 - 'Internal React Error: suspendedState null when it was expected to exists. Please report this as a React bug.',
5943 - );
5944 - }
5945 - const state = suspendedState;
5942 if (
5943 // $FlowFixMe[prop-missing]
5944 typeof instance.decode === 'function' &&
@@ -5971,16 +5967,11 @@ export function suspendInstance(
5967 }
5968
5969 export function suspendResource(
5970 + state: SuspendedState,
5971 hoistableRoot: HoistableRoot,
5972 resource: Resource,
5973 props: any,
5974 ): void {
5978 - if (suspendedState === null) {
5979 - throw new Error(
5980 - 'Internal React Error: suspendedState null when it was expected to exists. Please report this as a React bug.',
5981 - );
5982 - }
5983 - const state = suspendedState;
5975 if (resource.type === 'stylesheet') {
5976 if (typeof props.media === 'string') {
5977 // If we don't currently match media we avoid suspending on this resource
@@ -6060,13 +6051,10 @@ export function suspendResource(
6051 }
6052 }
6053
6063 -export function suspendOnActiveViewTransition(rootContainer: Container): void {
6064 - if (suspendedState === null) {
6065 - throw new Error(
6066 - 'Internal React Error: suspendedState null when it was expected to exists. Please report this as a React bug.',
6067 - );
6068 - }
6069 - const state = suspendedState;
6054 +export function suspendOnActiveViewTransition(
6055 + state: SuspendedState,
6056 + rootContainer: Container,
6057 +): void {
6058 const ownerDocument =
6059 rootContainer.nodeType === DOCUMENT_NODE
6060 ? rootContainer
@@ -6090,16 +6078,9 @@ const SUSPENSEY_IMAGE_TIME_ESTIMATE = 500;
6078 let estimatedBytesWithinLimit: number = 0;
6079
6080 export function waitForCommitToBeReady(
6081 + state: SuspendedState,
6082 timeoutOffset: number,
6083 ): null | ((() => void) => () => void) {
6095 - if (suspendedState === null) {
6096 - throw new Error(
6097 - 'Internal React Error: suspendedState null when it was expected to exists. Please report this as a React bug.',
6098 - );
6099 - }
6100 -
6101 - const state = suspendedState;
6102 -
6084 if (state.stylesheets && state.count === 0) {
6085 // We are not currently blocked but we have not inserted all stylesheets.
6086 // If this insertion happens and loads or errors synchronously then we can
packages/react-native-renderer/src/ReactFiberConfigFabric.js
+14 -3
@@ -602,17 +602,28 @@ export function preloadInstance(
602 return true;
603 }
604
605 -export function startSuspendingCommit(): void {}
605 +export opaque type SuspendedState = null;
606 +
607 +export function startSuspendingCommit(): SuspendedState {
608 + return null;
609 +}
610
611 export function suspendInstance(
612 + state: SuspendedState,
613 instance: Instance,
614 type: Type,
615 props: Props,
616 ): void {}
617
613 -export function suspendOnActiveViewTransition(container: Container): void {}
618 +export function suspendOnActiveViewTransition(
619 + state: SuspendedState,
620 + container: Container,
621 +): void {}
622
615 -export function waitForCommitToBeReady(timeoutOffset: number): null {
623 +export function waitForCommitToBeReady(
624 + state: SuspendedState,
625 + timeoutOffset: number,
626 +): null {
627 return null;
628 }
629
packages/react-native-renderer/src/ReactFiberConfigNative.js
+16 -3
@@ -664,6 +664,7 @@ export function hasInstanceAffectedParent(
664 }
665
666 export function startViewTransition(
667 + suspendedState: null | SuspendedState,
668 rootContainer: Container,
669 transitionTypes: null | TransitionTypes,
670 mutationCallback: () => void,
@@ -684,6 +685,7 @@ export function startViewTransition(
685 export type RunningViewTransition = null;
686
687 export function startGestureTransition(
688 + suspendedState: null | SuspendedState,
689 rootContainer: Container,
690 timeline: GestureTimeline,
691 rangeStart: number,
@@ -778,17 +780,28 @@ export function preloadInstance(
780 return true;
781 }
782
781 -export function startSuspendingCommit(): void {}
783 +export opaque type SuspendedState = null;
784 +
785 +export function startSuspendingCommit(): SuspendedState {
786 + return null;
787 +}
788
789 export function suspendInstance(
790 + state: SuspendedState,
791 instance: Instance,
792 type: Type,
793 props: Props,
794 ): void {}
795
789 -export function suspendOnActiveViewTransition(container: Container): void {}
796 +export function suspendOnActiveViewTransition(
797 + state: SuspendedState,
798 + container: Container,
799 +): void {}
800
791 -export function waitForCommitToBeReady(timeoutOffset: number): null {
801 +export function waitForCommitToBeReady(
802 + state: SuspendedState,
803 + timeoutOffset: number,
804 +): null {
805 return null;
806 }
807
packages/react-noop-renderer/src/createReactNoop.js
+21 -24
@@ -90,6 +90,8 @@ type SuspenseyCommitSubscription = {
90 commit: null | (() => void),
91 };
92
93 +export opaque type SuspendedState = SuspenseyCommitSubscription;
94 +
95 export type TransitionStatus = mixed;
96
97 export type FormInstance = Instance;
@@ -311,17 +313,17 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
313 'pending' | 'fulfilled',
314 > | null = null;
315
314 - // Represents a subscription for all the suspensey things that block a
315 - // particular commit. Once they've all loaded, the commit phase can proceed.
316 - let suspenseyCommitSubscription: SuspenseyCommitSubscription | null = null;
317 -
318 - function startSuspendingCommit(): void {
319 - // This is where we might suspend on things that aren't associated with a
320 - // particular node, like document.fonts.ready.
321 - suspenseyCommitSubscription = null;
316 + function startSuspendingCommit(): SuspendedState {
317 + // Represents a subscription for all the suspensey things that block a
318 + // particular commit. Once they've all loaded, the commit phase can proceed.
319 + return {
320 + pendingCount: 0,
321 + commit: null,
322 + };
323 }
324
325 function suspendInstance(
326 + state: SuspendedState,
327 instance: Instance,
328 type: string,
329 props: Props,
@@ -338,20 +340,13 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
340 if (record.status === 'fulfilled') {
341 // Already loaded.
342 } else if (record.status === 'pending') {
341 - if (suspenseyCommitSubscription === null) {
342 - suspenseyCommitSubscription = {
343 - pendingCount: 1,
344 - commit: null,
345 - };
346 - } else {
347 - suspenseyCommitSubscription.pendingCount++;
348 - }
343 + state.pendingCount++;
344 // Stash the subscription on the record. In `resolveSuspenseyThing`,
345 // we'll use this fire the commit once all the things have loaded.
346 if (record.subscriptions === null) {
347 record.subscriptions = [];
348 }
354 - record.subscriptions.push(suspenseyCommitSubscription);
349 + record.subscriptions.push(state);
350 }
351 } else {
352 throw new Error(
@@ -362,15 +357,14 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
357 }
358
359 function waitForCommitToBeReady(
360 + state: SuspendedState,
361 timeoutOffset: number,
362 ): ((commit: () => mixed) => () => void) | null {
367 - const subscription = suspenseyCommitSubscription;
368 - if (subscription !== null) {
369 - suspenseyCommitSubscription = null;
363 + if (state.pendingCount > 0) {
364 return (commit: () => void) => {
371 - subscription.commit = commit;
365 + state.commit = commit;
366 const cancelCommit = () => {
373 - subscription.commit = null;
367 + state.commit = null;
368 };
369 return cancelCommit;
370 };
@@ -693,13 +687,16 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
687 startSuspendingCommit,
688 suspendInstance,
689
696 - suspendResource(resource: mixed): void {
690 + suspendResource(state: SuspendedState, resource: mixed): void {
691 throw new Error(
692 'Resources are not implemented for React Noop yet. This method should not be called',
693 );
694 },
695
702 - suspendOnActiveViewTransition(container: Container): void {
696 + suspendOnActiveViewTransition(
697 + state: SuspendedState,
698 + container: Container,
699 + ): void {
700 // Not implemented
701 },
702
packages/react-reconciler/src/ReactFiberCommitWork.js
+57 -13
@@ -16,6 +16,7 @@ import type {
16 HoistableRoot,
17 FormInstance,
18 Props,
19 + SuspendedState,
20 } from './ReactFiberConfig';
21 import type {Fiber, FiberRoot} from './ReactInternalTypes';
22 import type {Lanes} from './ReactFiberLane';
@@ -4495,31 +4496,46 @@ let suspenseyCommitFlag: Flags = ShouldSuspendCommit;
4496 export function accumulateSuspenseyCommit(
4497 finishedWork: Fiber,
4498 committedLanes: Lanes,
4499 + suspendedState: SuspendedState,
4500 ): void {
4501 resetAppearingViewTransitions();
4500 - accumulateSuspenseyCommitOnFiber(finishedWork, committedLanes);
4502 + accumulateSuspenseyCommitOnFiber(
4503 + finishedWork,
4504 + committedLanes,
4505 + suspendedState,
4506 + );
4507 }
4508
4509 function recursivelyAccumulateSuspenseyCommit(
4510 parentFiber: Fiber,
4511 committedLanes: Lanes,
4512 + suspendedState: SuspendedState,
4513 ): void {
4514 if (parentFiber.subtreeFlags & suspenseyCommitFlag) {
4515 let child = parentFiber.child;
4516 while (child !== null) {
4510 - accumulateSuspenseyCommitOnFiber(child, committedLanes);
4517 + accumulateSuspenseyCommitOnFiber(child, committedLanes, suspendedState);
4518 child = child.sibling;
4519 }
4520 }
4521 }
4522
4516 -function accumulateSuspenseyCommitOnFiber(fiber: Fiber, committedLanes: Lanes) {
4523 +function accumulateSuspenseyCommitOnFiber(
4524 + fiber: Fiber,
4525 + committedLanes: Lanes,
4526 + suspendedState: SuspendedState,
4527 +) {
4528 switch (fiber.tag) {
4529 case HostHoistable: {
4519 - recursivelyAccumulateSuspenseyCommit(fiber, committedLanes);
4530 + recursivelyAccumulateSuspenseyCommit(
4531 + fiber,
4532 + committedLanes,
4533 + suspendedState,
4534 + );
4535 if (fiber.flags & suspenseyCommitFlag) {
4536 if (fiber.memoizedState !== null) {
4537 suspendResource(
4538 + suspendedState,
4539 // This should always be set by visiting HostRoot first
4540 (currentHoistableRoot: any),
4541 fiber.memoizedState,
@@ -4534,14 +4550,18 @@ function accumulateSuspenseyCommitOnFiber(fiber: Fiber, committedLanes: Lanes) {
4550 includesOnlySuspenseyCommitEligibleLanes(committedLanes) ||
4551 maySuspendCommitInSyncRender(type, props)
4552 ) {
4537 - suspendInstance(instance, type, props);
4553 + suspendInstance(suspendedState, instance, type, props);
4554 }
4555 }
4556 }
4557 break;
4558 }
4559 case HostComponent: {
4544 - recursivelyAccumulateSuspenseyCommit(fiber, committedLanes);
4560 + recursivelyAccumulateSuspenseyCommit(
4561 + fiber,
4562 + committedLanes,
4563 + suspendedState,
4564 + );
4565 if (fiber.flags & suspenseyCommitFlag) {
4566 const instance = fiber.stateNode;
4567 const type = fiber.type;
@@ -4551,7 +4571,7 @@ function accumulateSuspenseyCommitOnFiber(fiber: Fiber, committedLanes: Lanes) {
4571 includesOnlySuspenseyCommitEligibleLanes(committedLanes) ||
4572 maySuspendCommitInSyncRender(type, props)
4573 ) {
4554 - suspendInstance(instance, type, props);
4574 + suspendInstance(suspendedState, instance, type, props);
4575 }
4576 }
4577 break;
@@ -4563,10 +4583,18 @@ function accumulateSuspenseyCommitOnFiber(fiber: Fiber, committedLanes: Lanes) {
4583 const container: Container = fiber.stateNode.containerInfo;
4584 currentHoistableRoot = getHoistableRoot(container);
4585
4566 - recursivelyAccumulateSuspenseyCommit(fiber, committedLanes);
4586 + recursivelyAccumulateSuspenseyCommit(
4587 + fiber,
4588 + committedLanes,
4589 + suspendedState,
4590 + );
4591 currentHoistableRoot = previousHoistableRoot;
4592 } else {
4569 - recursivelyAccumulateSuspenseyCommit(fiber, committedLanes);
4593 + recursivelyAccumulateSuspenseyCommit(
4594 + fiber,
4595 + committedLanes,
4596 + suspendedState,
4597 + );
4598 }
4599 break;
4600 }
@@ -4584,10 +4612,18 @@ function accumulateSuspenseyCommitOnFiber(fiber: Fiber, committedLanes: Lanes) {
4612 // instances, even if they're in the current tree.
4613 const prevFlags = suspenseyCommitFlag;
4614 suspenseyCommitFlag = MaySuspendCommit;
4587 - recursivelyAccumulateSuspenseyCommit(fiber, committedLanes);
4615 + recursivelyAccumulateSuspenseyCommit(
4616 + fiber,
4617 + committedLanes,
4618 + suspendedState,
4619 + );
4620 suspenseyCommitFlag = prevFlags;
4621 } else {
4590 - recursivelyAccumulateSuspenseyCommit(fiber, committedLanes);
4622 + recursivelyAccumulateSuspenseyCommit(
4623 + fiber,
4624 + committedLanes,
4625 + suspendedState,
4626 + );
4627 }
4628 }
4629 break;
@@ -4607,13 +4643,21 @@ function accumulateSuspenseyCommitOnFiber(fiber: Fiber, committedLanes: Lanes) {
4643 trackAppearingViewTransition(name, state);
4644 }
4645 }
4610 - recursivelyAccumulateSuspenseyCommit(fiber, committedLanes);
4646 + recursivelyAccumulateSuspenseyCommit(
4647 + fiber,
4648 + committedLanes,
4649 + suspendedState,
4650 + );
4651 break;
4652 }
4653 // Fallthrough
4654 }
4655 default: {
4616 - recursivelyAccumulateSuspenseyCommit(fiber, committedLanes);
4656 + recursivelyAccumulateSuspenseyCommit(
4657 + fiber,
4658 + committedLanes,
4659 + suspendedState,
4660 + );
4661 }
4662 }
4663 }
packages/react-reconciler/src/ReactFiberWorkLoop.js
+17 -4
@@ -26,6 +26,7 @@ import type {
26 Resource,
27 ViewTransitionInstance,
28 RunningViewTransition,
29 + SuspendedState,
30 } from './ReactFiberConfig';
31 import type {RootState} from './ReactFiberRoot';
32 import {
@@ -1379,6 +1380,7 @@ function finishConcurrentRender(
1380 workInProgressRootInterleavedUpdatedLanes,
1381 workInProgressSuspendedRetryLanes,
1382 exitStatus,
1383 + null,
1384 IMMEDIATE_COMMIT,
1385 renderStartTime,
1386 renderEndTime,
@@ -1487,22 +1489,23 @@ function commitRootWhenReady(
1489 subtreeFlags & ShouldSuspendCommit ||
1490 (subtreeFlags & BothVisibilityAndMaySuspendCommit) ===
1491 BothVisibilityAndMaySuspendCommit;
1492 + let suspendedState: null | SuspendedState = null;
1493 if (isViewTransitionEligible || maySuspendCommit || isGestureTransition) {
1494 // Before committing, ask the renderer whether the host tree is ready.
1495 // If it's not, we'll wait until it notifies us.
1493 - startSuspendingCommit();
1496 + suspendedState = startSuspendingCommit();
1497 // This will walk the completed fiber tree and attach listeners to all
1498 // the suspensey resources. The renderer is responsible for accumulating
1499 // all the load events. This all happens in a single synchronous
1500 // transaction, so it track state in its own module scope.
1501 // This will also track any newly added or appearing ViewTransition
1502 // components for the purposes of forming pairs.
1500 - accumulateSuspenseyCommit(finishedWork, lanes);
1503 + accumulateSuspenseyCommit(finishedWork, lanes, suspendedState);
1504 if (isViewTransitionEligible || isGestureTransition) {
1505 // If we're stopping gestures we don't have to wait for any pending
1506 // view transition. We'll stop it when we commit.
1507 if (!enableGestureTransition || root.stoppingGestures === null) {
1505 - suspendOnActiveViewTransition(root.containerInfo);
1508 + suspendOnActiveViewTransition(suspendedState, root.containerInfo);
1509 }
1510 }
1511 // For timeouts we use the previous fallback commit for retries and
@@ -1516,7 +1519,10 @@ function commitRootWhenReady(
1519 // At the end, ask the renderer if it's ready to commit, or if we should
1520 // suspend. If it's not ready, it will return a callback to subscribe to
1521 // a ready event.
1519 - const schedulePendingCommit = waitForCommitToBeReady(timeoutOffset);
1522 + const schedulePendingCommit = waitForCommitToBeReady(
1523 + suspendedState,
1524 + timeoutOffset,
1525 + );
1526 if (schedulePendingCommit !== null) {
1527 // NOTE: waitForCommitToBeReady returns a subscribe function so that we
1528 // only allocate a function if the commit isn't ready yet. The other
@@ -1538,6 +1544,7 @@ function commitRootWhenReady(
1544 updatedLanes,
1545 suspendedRetryLanes,
1546 exitStatus,
1547 + suspendedState,
1548 SUSPENDED_COMMIT,
1549 completedRenderStartTime,
1550 completedRenderEndTime,
@@ -1561,6 +1568,7 @@ function commitRootWhenReady(
1568 updatedLanes,
1569 suspendedRetryLanes,
1570 exitStatus,
1571 + suspendedState,
1572 suspendedCommitReason,
1573 completedRenderStartTime,
1574 completedRenderEndTime,
@@ -3284,6 +3292,7 @@ function commitRoot(
3292 updatedLanes: Lanes,
3293 suspendedRetryLanes: Lanes,
3294 exitStatus: RootExitStatus,
3295 + suspendedState: null | SuspendedState,
3296 suspendedCommitReason: SuspendedCommitReason, // Profiling-only
3297 completedRenderStartTime: number, // Profiling-only
3298 completedRenderEndTime: number, // Profiling-only
@@ -3435,6 +3444,7 @@ function commitRoot(
3444 root,
3445 finishedWork,
3446 recoverableErrors,
3447 + suspendedState,
3448 enableProfilerTimer
3449 ? suspendedCommitReason === IMMEDIATE_COMMIT
3450 ? completedRenderEndTime
@@ -3573,6 +3583,7 @@ function commitRoot(
3583 pendingEffectsStatus = PENDING_MUTATION_PHASE;
3584 if (enableViewTransition && willStartViewTransition) {
3585 pendingViewTransition = startViewTransition(
3586 + suspendedState,
3587 root.containerInfo,
3588 pendingTransitionTypes,
3589 flushMutationEffects,
@@ -3987,6 +3998,7 @@ function commitGestureOnRoot(
3998 root: FiberRoot,
3999 finishedWork: Fiber,
4000 recoverableErrors: null | Array<CapturedValue<mixed>>,
4001 + suspendedState: null | SuspendedState,
4002 renderEndTime: number, // Profiling-only
4003 ): void {
4004 // We assume that the gesture we just rendered was the first one in the queue.
@@ -4017,6 +4029,7 @@ function commitGestureOnRoot(
4029 pendingEffectsStatus = PENDING_GESTURE_MUTATION_PHASE;
4030
4031 pendingViewTransition = finishedGesture.running = startGestureTransition(
4032 + suspendedState,
4033 root.containerInfo,
4034 finishedGesture.provider,
4035 finishedGesture.rangeStart,
packages/react-reconciler/src/__tests__/ReactFiberHostContext-test.internal.js
+6 -4
@@ -106,10 +106,12 @@ describe('ReactFiberHostContext', () => {
106 preloadInstance(instance, type, props) {
107 return true;
108 },
109 - startSuspendingCommit() {},
110 - suspendInstance(instance, type, props) {},
111 - suspendOnActiveViewTransition(container) {},
112 - waitForCommitToBeReady(timeoutOffset: number) {
109 + startSuspendingCommit() {
110 + return null;
111 + },
112 + suspendInstance(state, instance, type, props) {},
113 + suspendOnActiveViewTransition(state, container) {},
114 + waitForCommitToBeReady(state, timeoutOffset) {
115 return null;
116 },
117 supportsMutation: true,
packages/react-reconciler/src/forks/ReactFiberConfig.custom.js
+1
@@ -41,6 +41,7 @@ export opaque type NoTimeout = mixed;
41 export opaque type RendererInspectionConfig = mixed;
42 export opaque type TransitionStatus = mixed;
43 export opaque type FormInstance = mixed;
44 +export opaque type SuspendedState = mixed;
45 export type RunningViewTransition = mixed;
46 export type ViewTransitionInstance = null | {name: string, ...};
47 export opaque type InstanceMeasurement = mixed;
packages/react-test-renderer/src/ReactFiberConfigTestHost.js
+16 -3
@@ -414,6 +414,7 @@ export function hasInstanceAffectedParent(
414 }
415
416 export function startViewTransition(
417 + suspendedState: null | SuspendedState,
418 rootContainer: Container,
419 transitionTypes: null | TransitionTypes,
420 mutationCallback: () => void,
@@ -434,6 +435,7 @@ export function startViewTransition(
435 export type RunningViewTransition = null;
436
437 export function startGestureTransition(
438 + suspendedState: null | SuspendedState,
439 rootContainer: Container,
440 timeline: GestureTimeline,
441 rangeStart: number,
@@ -561,17 +563,28 @@ export function preloadInstance(
563 return true;
564 }
565
564 -export function startSuspendingCommit(): void {}
566 +export opaque type SuspendedState = null;
567 +
568 +export function startSuspendingCommit(): SuspendedState {
569 + return null;
570 +}
571
572 export function suspendInstance(
573 + state: SuspendedState,
574 instance: Instance,
575 type: Type,
576 props: Props,
577 ): void {}
578
572 -export function suspendOnActiveViewTransition(container: Container): void {}
579 +export function suspendOnActiveViewTransition(
580 + state: SuspendedState,
581 + container: Container,
582 +): void {}
583
574 -export function waitForCommitToBeReady(timeoutOffset: number): null {
584 +export function waitForCommitToBeReady(
585 + state: SuspendedState,
586 + timeoutOffset: number,
587 +): null {
588 return null;
589 }
590