Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 55b8279

Browse files
author
Andrew Clark
authored
Strict mode and default mode should have same Suspense semantics (facebook#13882)
In the default mode, Suspense has special semantics where, in addition to timing out immediately, we don't unwind the stack before rendering the fallback. Instead, we commit the tree in an inconsistent state, then synchronous render *again* to switch to the fallback. This is slower but is less likely to cause issues with older components that perform side effects in the render phase (e.g. componentWillMount, componentWillUpdate, and componentWillReceiveProps). We should do this in strict mode, too, so that there are no semantic differences (in prod, at least) between default mode and strict mode. The rationale is that it makes it easier to wrap a tree in strict mode and start migrating components incrementally without worrying about new bugs in production.
1 parent dac9202 commit 55b8279

File tree

3 files changed

+68
-70
lines changed

3 files changed

+68
-70
lines changed

packages/react-reconciler/src/ReactFiberScheduler.js

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -110,12 +110,7 @@ import {
110110
computeAsyncExpiration,
111111
computeInteractiveExpiration,
112112
} from './ReactFiberExpirationTime';
113-
import {
114-
ConcurrentMode,
115-
ProfileMode,
116-
NoContext,
117-
StrictMode,
118-
} from './ReactTypeOfMode';
113+
import {ConcurrentMode, ProfileMode, NoContext} from './ReactTypeOfMode';
119114
import {enqueueUpdate, resetCurrentlyProcessingQueue} from './ReactUpdateQueue';
120115
import {createCapturedValue} from './ReactCapturedValue';
121116
import {
@@ -1602,10 +1597,10 @@ function retrySuspendedRoot(
16021597
}
16031598

16041599
scheduleWorkToRoot(boundaryFiber, retryTime);
1605-
if ((boundaryFiber.mode & StrictMode) === NoContext) {
1606-
// Outside of strict mode, we must schedule an update on the source fiber,
1607-
// too, since it already committed in an inconsistent state and therefore
1608-
// does not have any pending work.
1600+
if ((boundaryFiber.mode & ConcurrentMode) === NoContext) {
1601+
// Outside of concurrent mode, we must schedule an update on the source
1602+
// fiber, too, since it already committed in an inconsistent state and
1603+
// therefore does not have any pending work.
16091604
scheduleWorkToRoot(sourceFiber, retryTime);
16101605
}
16111606

packages/react-reconciler/src/ReactFiberUnwindWork.js

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ import {
3636
LifecycleEffectMask,
3737
} from 'shared/ReactSideEffectTags';
3838
import {enableSchedulerTracing} from 'shared/ReactFeatureFlags';
39-
import {StrictMode, ConcurrentMode} from './ReactTypeOfMode';
39+
import {ConcurrentMode} from './ReactTypeOfMode';
4040

4141
import {createCapturedValue} from './ReactCapturedValue';
4242
import {
@@ -230,15 +230,15 @@ function throwException(
230230
}
231231
thenable.then(onResolveOrReject, onResolveOrReject);
232232

233-
// If the boundary is outside of strict mode, we should *not* suspend
234-
// the commit. Pretend as if the suspended component rendered null and
235-
// keep rendering. In the commit phase, we'll schedule a subsequent
236-
// synchronous update to re-render the Suspense.
233+
// If the boundary is outside of concurrent mode, we should *not*
234+
// suspend the commit. Pretend as if the suspended component rendered
235+
// null and keep rendering. In the commit phase, we'll schedule a
236+
// subsequent synchronous update to re-render the Suspense.
237237
//
238238
// Note: It doesn't matter whether the component that suspended was
239-
// inside a strict mode tree. If the Suspense is outside of it, we
239+
// inside a concurrent mode tree. If the Suspense is outside of it, we
240240
// should *not* suspend the commit.
241-
if ((workInProgress.mode & StrictMode) === NoEffect) {
241+
if ((workInProgress.mode & ConcurrentMode) === NoEffect) {
242242
workInProgress.effectTag |= CallbackEffect;
243243

244244
// Unmount the source fiber's children
@@ -274,8 +274,8 @@ function throwException(
274274
return;
275275
}
276276

277-
// Confirmed that the boundary is in a strict mode tree. Continue with
278-
// the normal suspend path.
277+
// Confirmed that the boundary is in a concurrent mode tree. Continue
278+
// with the normal suspend path.
279279

280280
let absoluteTimeoutMs;
281281
if (earliestTimeoutMs === -1) {

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

Lines changed: 54 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1023,7 +1023,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
10231023

10241024
it(
10251025
'continues rendering asynchronously even if a promise is captured by ' +
1026-
'a sync boundary (strict)',
1026+
'a sync boundary (default mode)',
10271027
async () => {
10281028
class UpdatingText extends React.Component {
10291029
state = {text: this.props.initialText};
@@ -1036,7 +1036,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
10361036
const text2 = React.createRef(null);
10371037
function App() {
10381038
return (
1039-
<StrictMode>
1039+
<Fragment>
10401040
<Suspense
10411041
maxDuration={1000}
10421042
fallback={<Text text="Loading..." />}>
@@ -1063,7 +1063,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
10631063
)}
10641064
</UpdatingText>
10651065
</ConcurrentMode>
1066-
</StrictMode>
1066+
</Fragment>
10671067
);
10681068
}
10691069

@@ -1076,25 +1076,26 @@ describe('ReactSuspenseWithNoopRenderer', () => {
10761076
'Before',
10771077
'Suspend! [Async: 1]',
10781078
'After',
1079-
'Loading...',
10801079
'Before',
10811080
'Sync: 1',
10821081
'After',
10831082
'Did mount',
1083+
// The placeholder is rendered in a subsequent commit
1084+
'Loading...',
10841085
'Promise resolved [Async: 1]',
1085-
'Before',
10861086
'Async: 1',
1087-
'After',
1088-
]);
1089-
expect(ReactNoop.getChildren()).toEqual([
1090-
span('Before'),
1091-
span('Async: 1'),
1092-
span('After'),
1093-
1094-
span('Before'),
1095-
span('Sync: 1'),
1096-
span('After'),
10971087
]);
1088+
expect(ReactNoop.getChildrenAsJSX()).toEqual(
1089+
<React.Fragment>
1090+
<span prop="Before" />
1091+
<span prop="Async: 1" />
1092+
<span prop="After" />
1093+
1094+
<span prop="Before" />
1095+
<span prop="Sync: 1" />
1096+
<span prop="After" />
1097+
</React.Fragment>,
1098+
);
10981099

10991100
// Update. This starts out asynchronously.
11001101
text1.current.setState({text: 'Async: 2'}, () =>
@@ -1105,59 +1106,61 @@ describe('ReactSuspenseWithNoopRenderer', () => {
11051106
);
11061107

11071108
// Start rendering asynchronously
1108-
ReactNoop.flushThrough([
1109-
'Before',
1109+
ReactNoop.flushThrough(['Before']);
1110+
1111+
// Now render the next child, which suspends
1112+
expect(ReactNoop.flushNextYield()).toEqual([
11101113
// This child suspends
11111114
'Suspend! [Async: 2]',
1112-
// But we can still render the rest of the async tree asynchronously
1113-
'After',
11141115
]);
1115-
1116-
// Suspend during an async render.
1117-
expect(ReactNoop.flushNextYield()).toEqual(['Loading...']);
1118-
expect(ReactNoop.flush()).toEqual(['Before', 'Sync: 2', 'After']);
1119-
// Commit was suspended.
1120-
expect(ReactNoop.getChildren()).toEqual([
1121-
span('Before'),
1122-
span('Async: 1'),
1123-
span('After'),
1124-
1125-
span('Before'),
1126-
span('Sync: 1'),
1127-
span('After'),
1128-
]);
1129-
1130-
// When the placeholder is pinged, the boundary re-
1131-
// renders asynchronously.
1132-
ReactNoop.expire(100);
1133-
await advanceTimers(100);
11341116
expect(ReactNoop.flush()).toEqual([
1135-
'Promise resolved [Async: 2]',
1136-
'Before',
1137-
'Async: 2',
11381117
'After',
11391118
'Before',
11401119
'Sync: 2',
11411120
'After',
11421121
'Update 1 did commit',
11431122
'Update 2 did commit',
1123+
1124+
// Switch to the placeholder in a subsequent commit
1125+
'Loading...',
11441126
]);
1127+
expect(ReactNoop.getChildrenAsJSX()).toEqual(
1128+
<React.Fragment>
1129+
<span hidden={true} prop="Before" />
1130+
<span hidden={true} prop="After" />
1131+
<span prop="Loading..." />
11451132

1146-
expect(ReactNoop.getChildren()).toEqual([
1147-
span('Before'),
1148-
span('Async: 2'),
1149-
span('After'),
1133+
<span prop="Before" />
1134+
<span prop="Sync: 2" />
1135+
<span prop="After" />
1136+
</React.Fragment>,
1137+
);
11501138

1151-
span('Before'),
1152-
span('Sync: 2'),
1153-
span('After'),
1139+
// When the placeholder is pinged, the boundary must be re-rendered
1140+
// synchronously.
1141+
await advanceTimers(100);
1142+
expect(ReactNoop.clearYields()).toEqual([
1143+
'Promise resolved [Async: 2]',
1144+
'Async: 2',
11541145
]);
1146+
1147+
expect(ReactNoop.getChildrenAsJSX()).toEqual(
1148+
<React.Fragment>
1149+
<span prop="Before" />
1150+
<span prop="Async: 2" />
1151+
<span prop="After" />
1152+
1153+
<span prop="Before" />
1154+
<span prop="Sync: 2" />
1155+
<span prop="After" />
1156+
</React.Fragment>,
1157+
);
11551158
},
11561159
);
11571160

11581161
it(
11591162
'continues rendering asynchronously even if a promise is captured by ' +
1160-
'a sync boundary (loose)',
1163+
'a sync boundary (strict, non-concurrent)',
11611164
async () => {
11621165
class UpdatingText extends React.Component {
11631166
state = {text: this.props.initialText};
@@ -1170,7 +1173,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
11701173
const text2 = React.createRef(null);
11711174
function App() {
11721175
return (
1173-
<Fragment>
1176+
<StrictMode>
11741177
<Suspense
11751178
maxDuration={1000}
11761179
fallback={<Text text="Loading..." />}>
@@ -1197,7 +1200,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
11971200
)}
11981201
</UpdatingText>
11991202
</ConcurrentMode>
1200-
</Fragment>
1203+
</StrictMode>
12011204
);
12021205
}
12031206

0 commit comments

Comments
 (0)