Skip to content

Commit f589d8b

Browse files
committed
Ensure we rerender with the original suspender
1 parent 2bf4444 commit f589d8b

File tree

3 files changed

+48
-15
lines changed

3 files changed

+48
-15
lines changed

packages/react-server/src/ReactFizzServer.js

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,7 @@ import isArray from 'shared/isArray';
193193
import {
194194
SuspenseException,
195195
getSuspendedThenable,
196+
ensureSuspendableThenableStateDEV,
196197
getSuspendedCallSiteStackDEV,
197198
getSuspendedCallSiteDebugTaskDEV,
198199
setCaptureSuspendedCallSiteDEV,
@@ -1030,7 +1031,7 @@ function pushHaltedAwaitOnComponentStack(
10301031
}
10311032

10321033
// performWork + retryTask without mutation
1033-
function rerenderHaltedTask(request: Request, task: Task): void {
1034+
function rerenderStalledTask(request: Request, task: Task): void {
10341035
const prevContext = getActiveContext();
10351036
const prevDispatcher = ReactSharedInternals.H;
10361037
ReactSharedInternals.H = HooksDispatcher;
@@ -1080,9 +1081,14 @@ function pushSuspendedCallSiteOnComponentStack(
10801081
task: Task,
10811082
): void {
10821083
setCaptureSuspendedCallSiteDEV(true);
1084+
const restoreThenableState = ensureSuspendableThenableStateDEV(
1085+
// refined at the callsite
1086+
((task.thenableState: any): ThenableState),
1087+
);
10831088
try {
1084-
rerenderHaltedTask(request, task);
1089+
rerenderStalledTask(request, task);
10851090
} finally {
1091+
restoreThenableState();
10861092
setCaptureSuspendedCallSiteDEV(false);
10871093
}
10881094

@@ -4616,13 +4622,6 @@ function abortTask(task: Task, request: Request, error: mixed): void {
46164622
}
46174623
pushHaltedAwaitOnComponentStack(task, debugInfo);
46184624
if (task.thenableState !== null) {
4619-
// TODO: really?
4620-
// If the thenable was resolved in the meantime, we won't get a stack.
4621-
// We won't know which thenable in thenableState is newly settled though.
4622-
// We can't just clear status fields on each thenable because then the
4623-
// stack may point at a thenable that wasn't stalled. In those cases
4624-
// it's better to point at the callsite of the stalled Component as an
4625-
// entrypoint instead of the wrong thenable.
46264625
pushSuspendedCallSiteOnComponentStack(request, task);
46274626
}
46284627
}

packages/react-server/src/ReactFizzThenable.js

Lines changed: 38 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,9 @@
77
* @flow
88
*/
99

10-
// Corresponds to ReactFiberWakeable and ReactFlightWakeable modules. Generally,
10+
// Corresponds to ReactFiberThenable and ReactFlightThenable modules. Generally,
1111
// changes to one module should be reflected in the others.
1212

13-
// TODO: Rename this module and the corresponding Fiber one to "Thenable"
14-
// instead of "Wakeable". Or some other more appropriate name.
15-
1613
import type {
1714
Thenable,
1815
PendingThenable,
@@ -185,6 +182,10 @@ export function setCaptureSuspendedCallSiteDEV(capture: boolean): void {
185182
let suspendedCallSiteStack: ComponentStackNode | null = null;
186183
let suspendedCallSiteDebugTask: ConsoleTask | null = null;
187184
function captureSuspendedCallSite(): void {
185+
// This is currently only used when aborting in Fizz.
186+
// You can only abort the render in Fizz and Flight.
187+
// In Fiber we only track suspended use via DevTools.
188+
// In Flight, we track suspended use via async debug info.
188189
const currentTask = currentTaskInDEV;
189190
if (currentTask === null) {
190191
// eslint-disable-next-line react-internal/prod-error-codes -- not a prod error
@@ -248,3 +249,36 @@ export function getSuspendedCallSiteDebugTaskDEV(): ConsoleTask | null {
248249
);
249250
}
250251
}
252+
253+
export function ensureSuspendableThenableStateDEV(
254+
thenableState: ThenableState,
255+
): () => void {
256+
if (__DEV__) {
257+
const lastThenable = thenableState[thenableState.length - 1];
258+
switch (lastThenable.status) {
259+
case 'fulfilled':
260+
const previousThenableValue = lastThenable.value;
261+
delete lastThenable.value;
262+
delete (lastThenable: any).status;
263+
return () => {
264+
lastThenable.value = previousThenableValue;
265+
lastThenable.status = 'fulfilled';
266+
};
267+
case 'rejected':
268+
const previousThenableReason = lastThenable.reason;
269+
delete lastThenable.reason;
270+
delete (lastThenable: any).status;
271+
return () => {
272+
lastThenable.reason = previousThenableReason;
273+
lastThenable.status = 'rejected';
274+
};
275+
}
276+
return noop;
277+
} else {
278+
// eslint-disable-next-line react-internal/prod-error-codes
279+
throw new Error(
280+
'ensureSuspendableThenableStateDEV was called in a production environment. ' +
281+
'This is a bug in React.',
282+
);
283+
}
284+
}

packages/react-server/src/__tests__/ReactServer-test.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -163,8 +163,8 @@ describe('ReactServer', () => {
163163
__DEV__
164164
? '' +
165165
// The concrete location may change as this test is updated.
166-
// Just make sure they still point at the same code
167-
'\n at Component (./ReactServer-test.js:95:13)' +
166+
// Just make sure they still point at React.use(p2)
167+
'\n at Component (./ReactServer-test.js:94:13)' +
168168
'\n at Indirection (./ReactServer-test.js:101:44)' +
169169
'\n at App (./ReactServer-test.js:109:46)'
170170
: null,

0 commit comments

Comments
 (0)