Skip to content

Commit 47ebc90

Browse files
authored
Put render phase update change behind a flag (facebook#18850)
In the new reconciler, I made a change to how render phase updates work. (By render phase updates, I mean when a component updates another component during its render phase. Or when a class component updates itself during the render phase. It does not include when a hook updates its own component during the render phase. Those have their own semantics. So really I mean anything triggers the "`setState` in render" warning.) The old behavior is to give the update the same "thread" (expiration time) as whatever is currently rendering. So if you call `setState` on a component that happens later in the same render, it will flush during that render. Ideally, we want to remove the special case and treat them as if they came from an interleaved event. Regardless, this pattern is not officially supported. This behavior is only a fallback. The flag only exists until we can roll out the `setState` warnning, since existing code might accidentally rely on the current behavior.
1 parent 6778c53 commit 47ebc90

12 files changed

+63
-9
lines changed

packages/react-dom/src/__tests__/ReactDOMServerIntegrationHooks-test.js

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1686,7 +1686,9 @@ describe('ReactDOMServerHooks', () => {
16861686
<App />,
16871687
);
16881688

1689-
if (gate(flags => flags.new)) {
1689+
if (
1690+
gate(flags => flags.new && flags.deferRenderPhaseUpdateToNextBatch)
1691+
) {
16901692
expect(() => Scheduler.unstable_flushAll()).toErrorDev([
16911693
'The object passed back from useOpaqueIdentifier is meant to be passed through to attributes only. ' +
16921694
'Do not read the value directly.',
@@ -1730,7 +1732,9 @@ describe('ReactDOMServerHooks', () => {
17301732
<App />,
17311733
);
17321734

1733-
if (gate(flags => flags.new)) {
1735+
if (
1736+
gate(flags => flags.new && flags.deferRenderPhaseUpdateToNextBatch)
1737+
) {
17341738
expect(() => Scheduler.unstable_flushAll()).toErrorDev([
17351739
'The object passed back from useOpaqueIdentifier is meant to be passed through to attributes only. ' +
17361740
'Do not read the value directly.',

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

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import {
2727
enableProfilerCommitHooks,
2828
enableSchedulerTracing,
2929
warnAboutUnmockedScheduler,
30+
deferRenderPhaseUpdateToNextBatch,
3031
} from 'shared/ReactFeatureFlags';
3132
import ReactSharedInternals from 'shared/ReactSharedInternals';
3233
import invariant from 'shared/invariant';
@@ -123,6 +124,7 @@ import {
123124
isSubsetOfLanes,
124125
mergeLanes,
125126
removeLanes,
127+
pickArbitraryLane,
126128
hasDiscreteLanes,
127129
hasUpdatePriority,
128130
getNextLanes,
@@ -354,6 +356,21 @@ export function requestUpdateLane(
354356
return getCurrentPriorityLevel() === ImmediateSchedulerPriority
355357
? (SyncLane: Lane)
356358
: (SyncBatchedLane: Lane);
359+
} else if (
360+
!deferRenderPhaseUpdateToNextBatch &&
361+
(executionContext & RenderContext) !== NoContext &&
362+
workInProgressRootRenderLanes !== NoLanes
363+
) {
364+
// This is a render phase update. These are not officially supported. The
365+
// old behavior is to give this the same "thread" (expiration time) as
366+
// whatever is currently rendering. So if you call `setState` on a component
367+
// that happens later in the same render, it will flush. Ideally, we want to
368+
// remove the special case and treat them as if they came from an
369+
// interleaved event. Regardless, this pattern is not officially supported.
370+
// This behavior is only a fallback. The flag only exists until we can roll
371+
// out the setState warnning, since existing code might accidentally rely on
372+
// the current behavior.
373+
return pickArbitraryLane(workInProgressRootRenderLanes);
357374
}
358375

359376
// The algorithm for assigning an update to a lane should be stable for all
@@ -542,11 +559,19 @@ function markUpdateLaneFromFiberToRoot(
542559
markRootUpdated(root, lane);
543560
if (workInProgressRoot === root) {
544561
// Received an update to a tree that's in the middle of rendering. Mark
545-
// that there is unprocessed work on this root.
546-
workInProgressRootUpdatedLanes = mergeLanes(
547-
workInProgressRootUpdatedLanes,
548-
lane,
549-
);
562+
// that there was an interleaved update work on this root. Unless the
563+
// `deferRenderPhaseUpdateToNextBatch` flag is off and this is a render
564+
// phase update. In that case, we don't treat render phase updates as if
565+
// they were interleaved, for backwards compat reasons.
566+
if (
567+
deferRenderPhaseUpdateToNextBatch ||
568+
(executionContext & RenderContext) === NoContext
569+
) {
570+
workInProgressRootUpdatedLanes = mergeLanes(
571+
workInProgressRootUpdatedLanes,
572+
lane,
573+
);
574+
}
550575
if (workInProgressRootExitStatus === RootSuspendedWithDelay) {
551576
// The root already suspended with a delay, which means this render
552577
// definitely won't finish. Since we have a new update, let's mark it as

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -394,7 +394,7 @@ describe('ReactIncrementalUpdates', () => {
394394
expect(() =>
395395
expect(Scheduler).toFlushAndYield(
396396
gate(flags =>
397-
flags.new
397+
flags.new && flags.deferRenderPhaseUpdateToNextBatch
398398
? [
399399
'setState updater',
400400
// In the new reconciler, updates inside the render phase are
@@ -427,7 +427,7 @@ describe('ReactIncrementalUpdates', () => {
427427
});
428428
expect(Scheduler).toFlushAndYield(
429429
gate(flags =>
430-
flags.new
430+
flags.new && flags.deferRenderPhaseUpdateToNextBatch
431431
? // In the new reconciler, updates inside the render phase are
432432
// treated as if they came from an event, so the update gets shifted
433433
// to a subsequent render.

packages/shared/ReactFeatureFlags.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,3 +127,11 @@ export const enableModernEventSystem = false;
127127

128128
// Support legacy Primer support on internal FB www
129129
export const enableLegacyFBSupport = false;
130+
131+
// Updates that occur in the render phase are not officially supported. But when
132+
// they do occur, in the new reconciler, we defer them to a subsequent render by
133+
// picking a lane that's not currently rendering. We treat them the same as if
134+
// they came from an interleaved event. In the old reconciler, we use whatever
135+
// expiration time is currently rendering. Remove this flag once we have
136+
// migrated to the new behavior.
137+
export const deferRenderPhaseUpdateToNextBatch = true;

packages/shared/forks/ReactFeatureFlags.native-fb.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ export const enableLegacyFBSupport = false;
4646
export const enableFilterEmptyStringAttributesDOM = false;
4747

4848
export const enableNewReconciler = false;
49+
export const deferRenderPhaseUpdateToNextBatch = true;
4950

5051
// Flow magic to verify the exports of this file match the original version.
5152
// eslint-disable-next-line no-unused-vars

packages/shared/forks/ReactFeatureFlags.native-oss.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ export const enableLegacyFBSupport = false;
4545
export const enableFilterEmptyStringAttributesDOM = false;
4646

4747
export const enableNewReconciler = false;
48+
export const deferRenderPhaseUpdateToNextBatch = true;
4849

4950
// Flow magic to verify the exports of this file match the original version.
5051
// eslint-disable-next-line no-unused-vars

packages/shared/forks/ReactFeatureFlags.test-renderer.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ export const enableLegacyFBSupport = false;
4545
export const enableFilterEmptyStringAttributesDOM = false;
4646

4747
export const enableNewReconciler = false;
48+
export const deferRenderPhaseUpdateToNextBatch = true;
4849

4950
// Flow magic to verify the exports of this file match the original version.
5051
// eslint-disable-next-line no-unused-vars

packages/shared/forks/ReactFeatureFlags.test-renderer.www.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ export const enableLegacyFBSupport = false;
4545
export const enableFilterEmptyStringAttributesDOM = false;
4646

4747
export const enableNewReconciler = false;
48+
export const deferRenderPhaseUpdateToNextBatch = true;
4849

4950
// Flow magic to verify the exports of this file match the original version.
5051
// eslint-disable-next-line no-unused-vars

packages/shared/forks/ReactFeatureFlags.testing.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ export const enableLegacyFBSupport = false;
4545
export const enableFilterEmptyStringAttributesDOM = false;
4646

4747
export const enableNewReconciler = false;
48+
export const deferRenderPhaseUpdateToNextBatch = true;
4849

4950
// Flow magic to verify the exports of this file match the original version.
5051
// eslint-disable-next-line no-unused-vars

packages/shared/forks/ReactFeatureFlags.testing.www.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ export const enableLegacyFBSupport = !__EXPERIMENTAL__;
4545
export const enableFilterEmptyStringAttributesDOM = false;
4646

4747
export const enableNewReconciler = false;
48+
export const deferRenderPhaseUpdateToNextBatch = true;
4849

4950
// Flow magic to verify the exports of this file match the original version.
5051
// eslint-disable-next-line no-unused-vars

packages/shared/forks/ReactFeatureFlags.www-dynamic.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,16 @@ export const enableModernEventSystem = __VARIANT__;
2121
export const enableLegacyFBSupport = __VARIANT__;
2222
export const enableDebugTracing = !__VARIANT__;
2323

24+
// This only has an effect in the new reconciler. But also, the new reconciler
25+
// is only enabled when __VARIANT__ is true. So this is set to the opposite of
26+
// __VARIANT__ so that it's `false` when running against the new reconciler.
27+
// Ideally we would test both against the new reconciler, but until then, we
28+
// should test the value that is used in www. Which is `false`.
29+
//
30+
// Once Lanes has landed in both reconciler forks, we'll get coverage of
31+
// both branches.
32+
export const deferRenderPhaseUpdateToNextBatch = !__VARIANT__;
33+
2434
// These are already tested in both modes using the build type dimension,
2535
// so we don't need to use __VARIANT__ to get extra coverage.
2636
export const debugRenderPhaseSideEffectsForStrictMode = __DEV__;

packages/shared/forks/ReactFeatureFlags.www.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ export const {
2626
enableFilterEmptyStringAttributesDOM,
2727
enableLegacyFBSupport,
2828
enableDebugTracing,
29+
deferRenderPhaseUpdateToNextBatch,
2930
} = dynamicFeatureFlags;
3031

3132
// On WWW, __EXPERIMENTAL__ is used for a new modern build.

0 commit comments

Comments
 (0)