Skip to content

Commit dd5c208

Browse files
authored
Revert yieldy behavior for non-use Suspense (#25537)
To derisk the rollout of `use`, and simplify the implementation, this reverts the yield-to-microtasks behavior for promises that are thrown directly (as opposed to being unwrapped by `use`). We may add this back later. However, the plan is to deprecate throwing a promise directly and migrate all existing Suspense code to `use`, so the extra code probably isn't worth it.
1 parent 9341775 commit dd5c208

16 files changed

+75
-175
lines changed

packages/react-reconciler/src/ReactFiberHooks.new.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,7 @@ import {now} from './Scheduler';
136136
import {
137137
trackUsedThenable,
138138
getPreviouslyUsedThenableAtIndex,
139-
} from './ReactFiberWakeable.new';
139+
} from './ReactFiberThenable.new';
140140

141141
const {ReactCurrentDispatcher, ReactCurrentBatchConfig} = ReactSharedInternals;
142142

@@ -783,6 +783,7 @@ function use<T>(usable: Usable<T>): T {
783783
const index = thenableIndexCounter;
784784
thenableIndexCounter += 1;
785785

786+
// TODO: Unify this switch statement with the one in trackUsedThenable.
786787
switch (thenable.status) {
787788
case 'fulfilled': {
788789
const fulfilledValue: T = thenable.value;

packages/react-reconciler/src/ReactFiberHooks.old.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,7 @@ import {now} from './Scheduler';
136136
import {
137137
trackUsedThenable,
138138
getPreviouslyUsedThenableAtIndex,
139-
} from './ReactFiberWakeable.old';
139+
} from './ReactFiberThenable.old';
140140

141141
const {ReactCurrentDispatcher, ReactCurrentBatchConfig} = ReactSharedInternals;
142142

@@ -783,6 +783,7 @@ function use<T>(usable: Usable<T>): T {
783783
const index = thenableIndexCounter;
784784
thenableIndexCounter += 1;
785785

786+
// TODO: Unify this switch statement with the one in trackUsedThenable.
786787
switch (thenable.status) {
787788
case 'fulfilled': {
788789
const fulfilledValue: T = thenable.value;

packages/react-reconciler/src/ReactFiberWakeable.new.js packages/react-reconciler/src/ReactFiberThenable.new.js

+10-44
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
*/
99

1010
import type {
11-
Wakeable,
1211
Thenable,
1312
PendingThenable,
1413
FulfilledThenable,
@@ -18,14 +17,8 @@ import type {
1817
import ReactSharedInternals from 'shared/ReactSharedInternals';
1918
const {ReactCurrentActQueue} = ReactSharedInternals;
2019

21-
let suspendedThenable: Thenable<mixed> | null = null;
22-
let adHocSuspendCount: number = 0;
23-
24-
// TODO: Sparse arrays are bad for performance.
20+
let suspendedThenable: Thenable<any> | null = null;
2521
let usedThenables: Array<Thenable<any> | void> | null = null;
26-
let lastUsedThenable: Thenable<any> | null = null;
27-
28-
const MAX_AD_HOC_SUSPEND_COUNT = 50;
2922

3023
export function isTrackingSuspendedThenable(): boolean {
3124
return suspendedThenable !== null;
@@ -39,22 +32,17 @@ export function suspendedThenableDidResolve(): boolean {
3932
return false;
4033
}
4134

42-
export function trackSuspendedWakeable(wakeable: Wakeable) {
43-
// If this wakeable isn't already a thenable, turn it into one now. Then,
44-
// when we resume the work loop, we can check if its status is
45-
// still pending.
46-
// TODO: Get rid of the Wakeable type? It's superseded by UntrackedThenable.
47-
const thenable: Thenable<mixed> = (wakeable: any);
35+
export function trackUsedThenable<T>(thenable: Thenable<T>, index: number) {
36+
if (__DEV__ && ReactCurrentActQueue.current !== null) {
37+
ReactCurrentActQueue.didUsePromise = true;
38+
}
4839

49-
if (thenable !== lastUsedThenable) {
50-
// If this wakeable was not just `use`-d, it must be an ad hoc wakeable
51-
// that was thrown by an older Suspense implementation. Keep a count of
52-
// these so that we can detect an infinite ping loop.
53-
// TODO: Once `use` throws an opaque signal instead of the actual thenable,
54-
// a better way to count ad hoc suspends is whether an actual thenable
55-
// is caught by the work loop.
56-
adHocSuspendCount++;
40+
if (usedThenables === null) {
41+
usedThenables = [thenable];
42+
} else {
43+
usedThenables[index] = thenable;
5744
}
45+
5846
suspendedThenable = thenable;
5947

6048
// We use an expando to track the status and result of a thenable so that we
@@ -105,34 +93,12 @@ export function trackSuspendedWakeable(wakeable: Wakeable) {
10593

10694
export function resetWakeableStateAfterEachAttempt() {
10795
suspendedThenable = null;
108-
adHocSuspendCount = 0;
109-
lastUsedThenable = null;
11096
}
11197

11298
export function resetThenableStateOnCompletion() {
11399
usedThenables = null;
114100
}
115101

116-
export function throwIfInfinitePingLoopDetected() {
117-
if (adHocSuspendCount > MAX_AD_HOC_SUSPEND_COUNT) {
118-
// TODO: Guard against an infinite loop by throwing an error if the same
119-
// component suspends too many times in a row. This should be thrown from
120-
// the render phase so that it gets the component stack.
121-
}
122-
}
123-
124-
export function trackUsedThenable<T>(thenable: Thenable<T>, index: number) {
125-
if (usedThenables === null) {
126-
usedThenables = [];
127-
}
128-
usedThenables[index] = thenable;
129-
lastUsedThenable = thenable;
130-
131-
if (__DEV__ && ReactCurrentActQueue.current !== null) {
132-
ReactCurrentActQueue.didUsePromise = true;
133-
}
134-
}
135-
136102
export function getPreviouslyUsedThenableAtIndex<T>(
137103
index: number,
138104
): Thenable<T> | null {

packages/react-reconciler/src/ReactFiberWakeable.old.js packages/react-reconciler/src/ReactFiberThenable.old.js

+10-44
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
*/
99

1010
import type {
11-
Wakeable,
1211
Thenable,
1312
PendingThenable,
1413
FulfilledThenable,
@@ -18,14 +17,8 @@ import type {
1817
import ReactSharedInternals from 'shared/ReactSharedInternals';
1918
const {ReactCurrentActQueue} = ReactSharedInternals;
2019

21-
let suspendedThenable: Thenable<mixed> | null = null;
22-
let adHocSuspendCount: number = 0;
23-
24-
// TODO: Sparse arrays are bad for performance.
20+
let suspendedThenable: Thenable<any> | null = null;
2521
let usedThenables: Array<Thenable<any> | void> | null = null;
26-
let lastUsedThenable: Thenable<any> | null = null;
27-
28-
const MAX_AD_HOC_SUSPEND_COUNT = 50;
2922

3023
export function isTrackingSuspendedThenable(): boolean {
3124
return suspendedThenable !== null;
@@ -39,22 +32,17 @@ export function suspendedThenableDidResolve(): boolean {
3932
return false;
4033
}
4134

42-
export function trackSuspendedWakeable(wakeable: Wakeable) {
43-
// If this wakeable isn't already a thenable, turn it into one now. Then,
44-
// when we resume the work loop, we can check if its status is
45-
// still pending.
46-
// TODO: Get rid of the Wakeable type? It's superseded by UntrackedThenable.
47-
const thenable: Thenable<mixed> = (wakeable: any);
35+
export function trackUsedThenable<T>(thenable: Thenable<T>, index: number) {
36+
if (__DEV__ && ReactCurrentActQueue.current !== null) {
37+
ReactCurrentActQueue.didUsePromise = true;
38+
}
4839

49-
if (thenable !== lastUsedThenable) {
50-
// If this wakeable was not just `use`-d, it must be an ad hoc wakeable
51-
// that was thrown by an older Suspense implementation. Keep a count of
52-
// these so that we can detect an infinite ping loop.
53-
// TODO: Once `use` throws an opaque signal instead of the actual thenable,
54-
// a better way to count ad hoc suspends is whether an actual thenable
55-
// is caught by the work loop.
56-
adHocSuspendCount++;
40+
if (usedThenables === null) {
41+
usedThenables = [thenable];
42+
} else {
43+
usedThenables[index] = thenable;
5744
}
45+
5846
suspendedThenable = thenable;
5947

6048
// We use an expando to track the status and result of a thenable so that we
@@ -105,34 +93,12 @@ export function trackSuspendedWakeable(wakeable: Wakeable) {
10593

10694
export function resetWakeableStateAfterEachAttempt() {
10795
suspendedThenable = null;
108-
adHocSuspendCount = 0;
109-
lastUsedThenable = null;
11096
}
11197

11298
export function resetThenableStateOnCompletion() {
11399
usedThenables = null;
114100
}
115101

116-
export function throwIfInfinitePingLoopDetected() {
117-
if (adHocSuspendCount > MAX_AD_HOC_SUSPEND_COUNT) {
118-
// TODO: Guard against an infinite loop by throwing an error if the same
119-
// component suspends too many times in a row. This should be thrown from
120-
// the render phase so that it gets the component stack.
121-
}
122-
}
123-
124-
export function trackUsedThenable<T>(thenable: Thenable<T>, index: number) {
125-
if (usedThenables === null) {
126-
usedThenables = [];
127-
}
128-
usedThenables[index] = thenable;
129-
lastUsedThenable = thenable;
130-
131-
if (__DEV__ && ReactCurrentActQueue.current !== null) {
132-
ReactCurrentActQueue.didUsePromise = true;
133-
}
134-
}
135-
136102
export function getPreviouslyUsedThenableAtIndex<T>(
137103
index: number,
138104
): Thenable<T> | null {

packages/react-reconciler/src/ReactFiberWorkLoop.new.js

+6-14
Original file line numberDiff line numberDiff line change
@@ -267,10 +267,9 @@ import {processTransitionCallbacks} from './ReactFiberTracingMarkerComponent.new
267267
import {
268268
resetWakeableStateAfterEachAttempt,
269269
resetThenableStateOnCompletion,
270-
trackSuspendedWakeable,
271270
suspendedThenableDidResolve,
272271
isTrackingSuspendedThenable,
273-
} from './ReactFiberWakeable.new';
272+
} from './ReactFiberThenable.new';
274273
import {schedulePostPaintCallback} from './ReactPostPaintCallback';
275274

276275
const ceil = Math.ceil;
@@ -1739,11 +1738,6 @@ function handleThrow(root, thrownValue): void {
17391738
return;
17401739
}
17411740

1742-
const isWakeable =
1743-
thrownValue !== null &&
1744-
typeof thrownValue === 'object' &&
1745-
typeof thrownValue.then === 'function';
1746-
17471741
if (enableProfilerTimer && erroredWork.mode & ProfileMode) {
17481742
// Record the time spent rendering before an error was thrown. This
17491743
// avoids inaccurate Profiler durations in the case of a
@@ -1753,7 +1747,11 @@ function handleThrow(root, thrownValue): void {
17531747

17541748
if (enableSchedulingProfiler) {
17551749
markComponentRenderStopped();
1756-
if (isWakeable) {
1750+
if (
1751+
thrownValue !== null &&
1752+
typeof thrownValue === 'object' &&
1753+
typeof thrownValue.then === 'function'
1754+
) {
17571755
const wakeable: Wakeable = (thrownValue: any);
17581756
markComponentSuspended(
17591757
erroredWork,
@@ -1768,12 +1766,6 @@ function handleThrow(root, thrownValue): void {
17681766
);
17691767
}
17701768
}
1771-
1772-
if (isWakeable) {
1773-
const wakeable: Wakeable = (thrownValue: any);
1774-
1775-
trackSuspendedWakeable(wakeable);
1776-
}
17771769
}
17781770

17791771
function pushDispatcher(container) {

packages/react-reconciler/src/ReactFiberWorkLoop.old.js

+6-14
Original file line numberDiff line numberDiff line change
@@ -267,10 +267,9 @@ import {processTransitionCallbacks} from './ReactFiberTracingMarkerComponent.old
267267
import {
268268
resetWakeableStateAfterEachAttempt,
269269
resetThenableStateOnCompletion,
270-
trackSuspendedWakeable,
271270
suspendedThenableDidResolve,
272271
isTrackingSuspendedThenable,
273-
} from './ReactFiberWakeable.old';
272+
} from './ReactFiberThenable.old';
274273
import {schedulePostPaintCallback} from './ReactPostPaintCallback';
275274

276275
const ceil = Math.ceil;
@@ -1739,11 +1738,6 @@ function handleThrow(root, thrownValue): void {
17391738
return;
17401739
}
17411740

1742-
const isWakeable =
1743-
thrownValue !== null &&
1744-
typeof thrownValue === 'object' &&
1745-
typeof thrownValue.then === 'function';
1746-
17471741
if (enableProfilerTimer && erroredWork.mode & ProfileMode) {
17481742
// Record the time spent rendering before an error was thrown. This
17491743
// avoids inaccurate Profiler durations in the case of a
@@ -1753,7 +1747,11 @@ function handleThrow(root, thrownValue): void {
17531747

17541748
if (enableSchedulingProfiler) {
17551749
markComponentRenderStopped();
1756-
if (isWakeable) {
1750+
if (
1751+
thrownValue !== null &&
1752+
typeof thrownValue === 'object' &&
1753+
typeof thrownValue.then === 'function'
1754+
) {
17571755
const wakeable: Wakeable = (thrownValue: any);
17581756
markComponentSuspended(
17591757
erroredWork,
@@ -1768,12 +1766,6 @@ function handleThrow(root, thrownValue): void {
17681766
);
17691767
}
17701768
}
1771-
1772-
if (isWakeable) {
1773-
const wakeable: Wakeable = (thrownValue: any);
1774-
1775-
trackSuspendedWakeable(wakeable);
1776-
}
17771769
}
17781770

17791771
function pushDispatcher(container) {

packages/react-reconciler/src/__tests__/ReactOffscreenSuspense-test.js

+16-26
Original file line numberDiff line numberDiff line change
@@ -485,32 +485,22 @@ describe('ReactOffscreen', () => {
485485
// In the same render, also hide the offscreen tree.
486486
root.render(<App show={false} />);
487487

488-
if (gate(flags => flags.enableSyncDefaultUpdates)) {
489-
expect(Scheduler).toFlushUntilNextPaint([
490-
// The outer update will commit, but the inner update is deferred until
491-
// a later render.
492-
'Outer: 1',
493-
494-
// Something suspended. This means we won't commit immediately; there
495-
// will be an async gap between render and commit. In this test, we will
496-
// use this property to schedule a concurrent update. The fact that
497-
// we're using Suspense to schedule a concurrent update is not directly
498-
// relevant to the test — we could also use time slicing, but I've
499-
// chosen to use Suspense the because implementation details of time
500-
// slicing are more volatile.
501-
'Suspend! [Async: 1]',
502-
503-
'Loading...',
504-
]);
505-
} else {
506-
// When default updates are time sliced, React yields before preparing
507-
// the fallback.
508-
expect(Scheduler).toFlushUntilNextPaint([
509-
'Outer: 1',
510-
'Suspend! [Async: 1]',
511-
]);
512-
expect(Scheduler).toFlushUntilNextPaint(['Loading...']);
513-
}
488+
expect(Scheduler).toFlushUntilNextPaint([
489+
// The outer update will commit, but the inner update is deferred until
490+
// a later render.
491+
'Outer: 1',
492+
493+
// Something suspended. This means we won't commit immediately; there
494+
// will be an async gap between render and commit. In this test, we will
495+
// use this property to schedule a concurrent update. The fact that
496+
// we're using Suspense to schedule a concurrent update is not directly
497+
// relevant to the test — we could also use time slicing, but I've
498+
// chosen to use Suspense the because implementation details of time
499+
// slicing are more volatile.
500+
'Suspend! [Async: 1]',
501+
502+
'Loading...',
503+
]);
514504

515505
// Assert that we haven't committed quite yet
516506
expect(root).toMatchRenderedOutput(

packages/react-reconciler/src/__tests__/ReactSuspenseWithNoopRenderer-test.js

+1
Original file line numberDiff line numberDiff line change
@@ -3874,6 +3874,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
38743874
'Suspend! [A2]',
38753875
'Loading...',
38763876
'Suspend! [B2]',
3877+
'Loading...',
38773878
]);
38783879
expect(root).toMatchRenderedOutput(
38793880
<>

packages/react-reconciler/src/__tests__/ReactWakeable-test.js packages/react-reconciler/src/__tests__/ReactThenable-test.js

+5
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,11 @@ describe('ReactWakeable', () => {
2626
return props.text;
2727
}
2828

29+
// This behavior was intentionally disabled to derisk the rollout of `use`.
30+
// It changes the behavior of old, pre-`use` Suspense implementations. We may
31+
// add this back; however, the plan is to migrate all existing Suspense code
32+
// to `use`, so the extra code probably isn't worth it.
33+
// @gate TODO
2934
test('if suspended fiber is pinged in a microtask, retry immediately without unwinding the stack', async () => {
3035
let resolved = false;
3136
function Async() {

0 commit comments

Comments
 (0)