Add unstable_concurrentUpdatesByDefault (#21227)
Ricky committed
Apr 28, 2021 at 16:09 UTC
9e9dac650535406b25979758a630a78b7c68a22c
24 files changed
+159
-38
packages/react-dom/src/__tests__/ReactDOMRoot-test.js
+35
@@ -13,6 +13,7 @@ let React = require('react');
13
let ReactDOM = require('react-dom');
14
let ReactDOMServer = require('react-dom/server');
15
let Scheduler = require('scheduler');
16
+let act;
17
18
describe('ReactDOMRoot', () => {
19
let container;
@@ -24,6 +25,7 @@ describe('ReactDOMRoot', () => {
25
ReactDOM = require('react-dom');
26
ReactDOMServer = require('react-dom/server');
27
Scheduler = require('scheduler');
28
+ act = require('react-dom/test-utils').unstable_concurrentAct;
29
});
30
31
if (!__EXPERIMENTAL__) {
@@ -316,4 +318,37 @@ describe('ReactDOMRoot', () => {
318
{withoutStack: true},
319
);
320
});
321
+
322
+ // @gate experimental
323
+ it('opts-in to concurrent default updates', async () => {
324
+ const root = ReactDOM.unstable_createRoot(container, {
325
+ unstable_concurrentUpdatesByDefault: true,
326
+ });
327
+
328
+ function Foo({value}) {
329
+ Scheduler.unstable_yieldValue(value);
330
+ return <div>{value}</div>;
331
+ }
332
+
333
+ await act(async () => {
334
+ root.render(<Foo value="a" />);
335
+ });
336
+
337
+ expect(container.textContent).toEqual('a');
338
+
339
+ await act(async () => {
340
+ root.render(<Foo value="b" />);
341
+
342
+ expect(Scheduler).toHaveYielded(['a']);
343
+ expect(container.textContent).toEqual('a');
344
+
345
+ expect(Scheduler).toFlushAndYieldThrough(['b']);
346
+ if (gate(flags => flags.allowConcurrentByDefault)) {
347
+ expect(container.textContent).toEqual('a');
348
+ } else {
349
+ expect(container.textContent).toEqual('b');
350
+ }
351
+ });
352
+ expect(container.textContent).toEqual('b');
353
+ });
354
});
packages/react-dom/src/client/ReactDOMRoot.js
+11
@@ -28,6 +28,7 @@ export type RootOptions = {
28
...
29
},
30
unstable_strictModeLevel?: number,
31
+ unstable_concurrentUpdatesByDefault?: boolean,
32
...
33
};
34
@@ -52,6 +53,7 @@ import {
53
} from 'react-reconciler/src/ReactFiberReconciler';
54
import invariant from 'shared/invariant';
55
import {ConcurrentRoot, LegacyRoot} from 'react-reconciler/src/ReactRootTags';
56
+import {allowConcurrentByDefault} from 'shared/ReactFeatureFlags';
57
58
function ReactDOMRoot(container: Container, options: void | RootOptions) {
59
this._internalRoot = createRootImpl(container, ConcurrentRoot, options);
@@ -126,12 +128,21 @@ function createRootImpl(
128
? options.unstable_strictModeLevel
129
: null;
130
131
+ let concurrentUpdatesByDefaultOverride = null;
132
+ if (allowConcurrentByDefault) {
133
+ concurrentUpdatesByDefaultOverride =
134
+ options != null && options.unstable_concurrentUpdatesByDefault != null
135
+ ? options.unstable_concurrentUpdatesByDefault
136
+ : null;
137
+ }
138
+
139
const root = createContainer(
140
container,
141
tag,
142
hydrate,
143
hydrationCallbacks,
144
strictModeLevelOverride,
145
+ concurrentUpdatesByDefaultOverride,
146
);
147
markContainerAsRoot(root.current, container);
148
packages/react-native-renderer/src/ReactFabric.js
+1
-1
@@ -207,7 +207,7 @@ function render(
207
if (!root) {
208
// TODO (bvaughn): If we decide to keep the wrapper component,
209
// We could create a wrapper for containerTag as well to reduce special casing.
210
- root = createContainer(containerTag, LegacyRoot, false, null, null);
210
+ root = createContainer(containerTag, LegacyRoot, false, null, null, null);
211
roots.set(containerTag, root);
212
}
213
updateContainer(element, root, null, callback);
packages/react-native-renderer/src/ReactNativeRenderer.js
+1
-1
@@ -203,7 +203,7 @@ function render(
203
if (!root) {
204
// TODO (bvaughn): If we decide to keep the wrapper component,
205
// We could create a wrapper for containerTag as well to reduce special casing.
206
- root = createContainer(containerTag, LegacyRoot, false, null, null);
206
+ root = createContainer(containerTag, LegacyRoot, false, null, null, null);
207
roots.set(containerTag, root);
208
}
209
updateContainer(element, root, null, callback);
packages/react-reconciler/src/ReactFiber.new.js
+13
@@ -24,6 +24,8 @@ import {
24
enableStrictEffects,
25
enableProfilerTimer,
26
enableScopeAPI,
27
+ enableSyncDefaultUpdates,
28
+ allowConcurrentByDefault,
29
} from 'shared/ReactFeatureFlags';
30
import {NoFlags, Placement, StaticMask} from './ReactFiberFlags';
31
import {ConcurrentRoot} from './ReactRootTags';
@@ -68,6 +70,7 @@ import {
70
ProfileMode,
71
StrictLegacyMode,
72
StrictEffectsMode,
73
+ ConcurrentUpdatesByDefaultMode,
74
} from './ReactTypeOfMode';
75
import {
76
REACT_FORWARD_REF_TYPE,
@@ -420,6 +423,7 @@ export function resetWorkInProgress(workInProgress: Fiber, renderLanes: Lanes) {
423
export function createHostRootFiber(
424
tag: RootTag,
425
strictModeLevelOverride: null | number,
426
+ concurrentUpdatesByDefaultOverride: null | boolean,
427
): Fiber {
428
let mode;
429
if (tag === ConcurrentRoot) {
@@ -440,6 +444,15 @@ export function createHostRootFiber(
444
mode |= StrictLegacyMode;
445
}
446
}
447
+ if (
448
+ // We only use this flag for our repo tests to check both behaviors.
449
+ // TODO: Flip this flag and rename it something like "forceConcurrentByDefaultForTesting"
450
+ !enableSyncDefaultUpdates ||
451
+ // Only for internal experiments.
452
+ (allowConcurrentByDefault && concurrentUpdatesByDefaultOverride)
453
+ ) {
454
+ mode |= ConcurrentUpdatesByDefaultMode;
455
+ }
456
} else {
457
mode = NoMode;
458
}
packages/react-reconciler/src/ReactFiber.old.js
+13
@@ -24,6 +24,8 @@ import {
24
enableStrictEffects,
25
enableProfilerTimer,
26
enableScopeAPI,
27
+ enableSyncDefaultUpdates,
28
+ allowConcurrentByDefault,
29
} from 'shared/ReactFeatureFlags';
30
import {NoFlags, Placement, StaticMask} from './ReactFiberFlags';
31
import {ConcurrentRoot} from './ReactRootTags';
@@ -68,6 +70,7 @@ import {
70
ProfileMode,
71
StrictLegacyMode,
72
StrictEffectsMode,
73
+ ConcurrentUpdatesByDefaultMode,
74
} from './ReactTypeOfMode';
75
import {
76
REACT_FORWARD_REF_TYPE,
@@ -420,6 +423,7 @@ export function resetWorkInProgress(workInProgress: Fiber, renderLanes: Lanes) {
423
export function createHostRootFiber(
424
tag: RootTag,
425
strictModeLevelOverride: null | number,
426
+ concurrentUpdatesByDefaultOverride: null | boolean,
427
): Fiber {
428
let mode;
429
if (tag === ConcurrentRoot) {
@@ -440,6 +444,15 @@ export function createHostRootFiber(
444
mode |= StrictLegacyMode;
445
}
446
}
447
+ if (
448
+ // We only use this flag for our repo tests to check both behaviors.
449
+ // TODO: Flip this flag and rename it something like "forceConcurrentByDefaultForTesting"
450
+ !enableSyncDefaultUpdates ||
451
+ // Only for internal experiments.
452
+ (allowConcurrentByDefault && concurrentUpdatesByDefaultOverride)
453
+ ) {
454
+ mode |= ConcurrentUpdatesByDefaultMode;
455
+ }
456
} else {
457
mode = NoMode;
458
}
packages/react-reconciler/src/ReactFiberLane.new.js
+20
-14
@@ -39,9 +39,10 @@ import {
39
enableCache,
40
enableSchedulingProfiler,
41
enableUpdaterTracking,
42
- enableSyncDefaultUpdates,
42
+ allowConcurrentByDefault,
43
} from 'shared/ReactFeatureFlags';
44
import {isDevToolsPresent} from './ReactFiberDevToolsHook.new';
45
+import {ConcurrentUpdatesByDefaultMode, NoMode} from './ReactTypeOfMode';
46
47
export const SyncLanePriority: LanePriority = 12;
48
@@ -318,11 +319,12 @@ export function getNextLanes(root: FiberRoot, wipLanes: Lanes): Lanes {
319
}
320
321
if (
321
- // TODO: Check for root override, once that lands
322
- enableSyncDefaultUpdates &&
323
- (nextLanes & InputContinuousLane) !== NoLanes
322
+ allowConcurrentByDefault &&
323
+ (root.current.mode & ConcurrentUpdatesByDefaultMode) !== NoMode
324
) {
325
- // When updates are sync by default, we entangle continous priority updates
325
+ // Do nothing, use the lanes as they were assigned.
326
+ } else if ((nextLanes & InputContinuousLane) !== NoLanes) {
327
+ // When updates are sync by default, we entangle continuous priority updates
328
// and default updates, so they render in the same batch. The only reason
329
// they use separate lanes is because continuous updates should interrupt
330
// transitions, but default updates should not.
@@ -527,17 +529,21 @@ export function shouldTimeSlice(root: FiberRoot, lanes: Lanes) {
529
// finish rendering without yielding execution.
530
return false;
531
}
530
- if (enableSyncDefaultUpdates) {
531
- const SyncDefaultLanes =
532
- InputContinuousHydrationLane |
533
- InputContinuousLane |
534
- DefaultHydrationLane |
535
- DefaultLane;
536
- // TODO: Check for root override, once that lands
537
- return (lanes & SyncDefaultLanes) === NoLanes;
538
- } else {
532
+
533
+ if (
534
+ allowConcurrentByDefault &&
535
+ (root.current.mode & ConcurrentUpdatesByDefaultMode) !== NoMode
536
+ ) {
537
+ // Concurrent updates by default always use time slicing.
538
return true;
539
}
540
+
541
+ const SyncDefaultLanes =
542
+ InputContinuousHydrationLane |
543
+ InputContinuousLane |
544
+ DefaultHydrationLane |
545
+ DefaultLane;
546
+ return (lanes & SyncDefaultLanes) === NoLanes;
547
}
548
549
export function isTransitionLane(lane: Lane) {
packages/react-reconciler/src/ReactFiberLane.old.js
+20
-14
@@ -39,9 +39,10 @@ import {
39
enableCache,
40
enableSchedulingProfiler,
41
enableUpdaterTracking,
42
- enableSyncDefaultUpdates,
42
+ allowConcurrentByDefault,
43
} from 'shared/ReactFeatureFlags';
44
import {isDevToolsPresent} from './ReactFiberDevToolsHook.old';
45
+import {ConcurrentUpdatesByDefaultMode, NoMode} from './ReactTypeOfMode';
46
47
export const SyncLanePriority: LanePriority = 12;
48
@@ -318,11 +319,12 @@ export function getNextLanes(root: FiberRoot, wipLanes: Lanes): Lanes {
319
}
320
321
if (
321
- // TODO: Check for root override, once that lands
322
- enableSyncDefaultUpdates &&
323
- (nextLanes & InputContinuousLane) !== NoLanes
322
+ allowConcurrentByDefault &&
323
+ (root.current.mode & ConcurrentUpdatesByDefaultMode) !== NoMode
324
) {
325
- // When updates are sync by default, we entangle continous priority updates
325
+ // Do nothing, use the lanes as they were assigned.
326
+ } else if ((nextLanes & InputContinuousLane) !== NoLanes) {
327
+ // When updates are sync by default, we entangle continuous priority updates
328
// and default updates, so they render in the same batch. The only reason
329
// they use separate lanes is because continuous updates should interrupt
330
// transitions, but default updates should not.
@@ -527,17 +529,21 @@ export function shouldTimeSlice(root: FiberRoot, lanes: Lanes) {
529
// finish rendering without yielding execution.
530
return false;
531
}
530
- if (enableSyncDefaultUpdates) {
531
- const SyncDefaultLanes =
532
- InputContinuousHydrationLane |
533
- InputContinuousLane |
534
- DefaultHydrationLane |
535
- DefaultLane;
536
- // TODO: Check for root override, once that lands
537
- return (lanes & SyncDefaultLanes) === NoLanes;
538
- } else {
532
+
533
+ if (
534
+ allowConcurrentByDefault &&
535
+ (root.current.mode & ConcurrentUpdatesByDefaultMode) !== NoMode
536
+ ) {
537
+ // Concurrent updates by default always use time slicing.
538
return true;
539
}
540
+
541
+ const SyncDefaultLanes =
542
+ InputContinuousHydrationLane |
543
+ InputContinuousLane |
544
+ DefaultHydrationLane |
545
+ DefaultLane;
546
+ return (lanes & SyncDefaultLanes) === NoLanes;
547
}
548
549
export function isTransitionLane(lane: Lane) {
packages/react-reconciler/src/ReactFiberReconciler.new.js
+2
@@ -249,6 +249,7 @@ export function createContainer(
249
hydrate: boolean,
250
hydrationCallbacks: null | SuspenseHydrationCallbacks,
251
strictModeLevelOverride: null | number,
252
+ concurrentUpdatesByDefaultOverride: null | boolean,
253
): OpaqueRoot {
254
return createFiberRoot(
255
containerInfo,
@@ -256,6 +257,7 @@ export function createContainer(
257
hydrate,
258
hydrationCallbacks,
259
strictModeLevelOverride,
260
+ concurrentUpdatesByDefaultOverride,
261
);
262
}
263
packages/react-reconciler/src/ReactFiberReconciler.old.js
+2
@@ -249,6 +249,7 @@ export function createContainer(
249
hydrate: boolean,
250
hydrationCallbacks: null | SuspenseHydrationCallbacks,
251
strictModeLevelOverride: null | number,
252
+ concurrentUpdatesByDefaultOverride: null | boolean,
253
): OpaqueRoot {
254
return createFiberRoot(
255
containerInfo,
@@ -256,6 +257,7 @@ export function createContainer(
257
hydrate,
258
hydrationCallbacks,
259
strictModeLevelOverride,
260
+ concurrentUpdatesByDefaultOverride,
261
);
262
}
263
packages/react-reconciler/src/ReactFiberRoot.new.js
+6
-1
@@ -99,6 +99,7 @@ export function createFiberRoot(
99
hydrate: boolean,
100
hydrationCallbacks: null | SuspenseHydrationCallbacks,
101
strictModeLevelOverride: null | number,
102
+ concurrentUpdatesByDefaultOverride: null | boolean,
103
): FiberRoot {
104
const root: FiberRoot = (new FiberRootNode(containerInfo, tag, hydrate): any);
105
if (enableSuspenseCallback) {
@@ -107,7 +108,11 @@ export function createFiberRoot(
108
109
// Cyclic construction. This cheats the type system right now because
110
// stateNode is any.
110
- const uninitializedFiber = createHostRootFiber(tag, strictModeLevelOverride);
111
+ const uninitializedFiber = createHostRootFiber(
112
+ tag,
113
+ strictModeLevelOverride,
114
+ concurrentUpdatesByDefaultOverride,
115
+ );
116
root.current = uninitializedFiber;
117
uninitializedFiber.stateNode = root;
118
packages/react-reconciler/src/ReactFiberRoot.old.js
+6
-1
@@ -99,6 +99,7 @@ export function createFiberRoot(
99
hydrate: boolean,
100
hydrationCallbacks: null | SuspenseHydrationCallbacks,
101
strictModeLevelOverride: null | number,
102
+ concurrentUpdatesByDefaultOverride: null | boolean,
103
): FiberRoot {
104
const root: FiberRoot = (new FiberRootNode(containerInfo, tag, hydrate): any);
105
if (enableSuspenseCallback) {
@@ -107,7 +108,11 @@ export function createFiberRoot(
108
109
// Cyclic construction. This cheats the type system right now because
110
// stateNode is any.
110
- const uninitializedFiber = createHostRootFiber(tag, strictModeLevelOverride);
111
+ const uninitializedFiber = createHostRootFiber(
112
+ tag,
113
+ strictModeLevelOverride,
114
+ concurrentUpdatesByDefaultOverride,
115
+ );
116
root.current = uninitializedFiber;
117
uninitializedFiber.stateNode = root;
118
packages/react-reconciler/src/ReactTypeOfMode.js
+7
-6
@@ -9,10 +9,11 @@
9
10
export type TypeOfMode = number;
11
12
-export const NoMode = /* */ 0b000000;
12
+export const NoMode = /* */ 0b000000;
13
// TODO: Remove ConcurrentMode by reading from the root tag instead
14
-export const ConcurrentMode = /* */ 0b000001;
15
-export const ProfileMode = /* */ 0b000010;
16
-export const DebugTracingMode = /* */ 0b000100;
17
-export const StrictLegacyMode = /* */ 0b001000;
18
-export const StrictEffectsMode = /* */ 0b010000;
14
+export const ConcurrentMode = /* */ 0b000001;
15
+export const ProfileMode = /* */ 0b000010;
16
+export const DebugTracingMode = /* */ 0b000100;
17
+export const StrictLegacyMode = /* */ 0b001000;
18
+export const StrictEffectsMode = /* */ 0b010000;
19
+export const ConcurrentUpdatesByDefaultMode = /* */ 0b100000;
packages/react-test-renderer/src/ReactTestRenderer.js
+10
@@ -51,6 +51,7 @@ import enqueueTask from 'shared/enqueueTask';
51
52
import {getPublicInstance} from './ReactTestHostConfig';
53
import {ConcurrentRoot, LegacyRoot} from 'react-reconciler/src/ReactRootTags';
54
+import {allowConcurrentByDefault} from 'shared/ReactFeatureFlags';
55
56
const {IsSomeRendererActing} = ReactSharedInternals;
57
@@ -58,6 +59,7 @@ type TestRendererOptions = {
59
createNodeMock: (element: React$Element<any>) => any,
60
unstable_isConcurrent: boolean,
61
unstable_strictModeLevel: number,
62
+ unstable_concurrentUpdatesByDefault: boolean,
63
...
64
};
65
@@ -436,6 +438,7 @@ function create(element: React$Element<any>, options: TestRendererOptions) {
438
let createNodeMock = defaultTestOptions.createNodeMock;
439
let isConcurrent = false;
440
let strictModeLevel = null;
441
+ let concurrentUpdatesByDefault = null;
442
if (typeof options === 'object' && options !== null) {
443
if (typeof options.createNodeMock === 'function') {
444
createNodeMock = options.createNodeMock;
@@ -446,6 +449,12 @@ function create(element: React$Element<any>, options: TestRendererOptions) {
449
if (options.unstable_strictModeLevel !== undefined) {
450
strictModeLevel = options.unstable_strictModeLevel;
451
}
452
+ if (allowConcurrentByDefault) {
453
+ if (options.unstable_concurrentUpdatesByDefault !== undefined) {
454
+ concurrentUpdatesByDefault =
455
+ options.unstable_concurrentUpdatesByDefault;
456
+ }
457
+ }
458
}
459
let container = {
460
children: [],
@@ -458,6 +467,7 @@ function create(element: React$Element<any>, options: TestRendererOptions) {
467
false,
468
null,
469
strictModeLevel,
470
+ concurrentUpdatesByDefault,
471
);
472
invariant(root != null, 'something went wrong');
473
updateContainer(element, root, null, null);
packages/shared/ReactFeatureFlags.js
+2
@@ -169,3 +169,5 @@ export const disableSchedulerTimeoutInWorkLoop = false;
169
export const enableLazyContextPropagation = false;
170
171
export const enableSyncDefaultUpdates = true;
172
+
173
+export const allowConcurrentByDefault = false;
packages/shared/forks/ReactFeatureFlags.native-fb.js
+1
@@ -60,6 +60,7 @@ export const enableRecursiveCommitTraversal = false;
60
export const disableSchedulerTimeoutInWorkLoop = false;
61
export const enableLazyContextPropagation = false;
62
export const enableSyncDefaultUpdates = false;
63
+export const allowConcurrentByDefault = false;
64
65
// Flow magic to verify the exports of this file match the original version.
66
// eslint-disable-next-line no-unused-vars
packages/shared/forks/ReactFeatureFlags.native-oss.js
+1
@@ -59,6 +59,7 @@ export const enableRecursiveCommitTraversal = false;
59
export const disableSchedulerTimeoutInWorkLoop = false;
60
export const enableLazyContextPropagation = false;
61
export const enableSyncDefaultUpdates = true;
62
+export const allowConcurrentByDefault = false;
63
64
// Flow magic to verify the exports of this file match the original version.
65
// eslint-disable-next-line no-unused-vars
packages/shared/forks/ReactFeatureFlags.test-renderer.js
+1
@@ -59,6 +59,7 @@ export const enableRecursiveCommitTraversal = false;
59
export const disableSchedulerTimeoutInWorkLoop = false;
60
export const enableLazyContextPropagation = false;
61
export const enableSyncDefaultUpdates = true;
62
+export const allowConcurrentByDefault = false;
63
64
// Flow magic to verify the exports of this file match the original version.
65
// eslint-disable-next-line no-unused-vars
packages/shared/forks/ReactFeatureFlags.test-renderer.native.js
+1
@@ -59,6 +59,7 @@ export const enableRecursiveCommitTraversal = false;
59
export const disableSchedulerTimeoutInWorkLoop = false;
60
export const enableLazyContextPropagation = false;
61
export const enableSyncDefaultUpdates = false;
62
+export const allowConcurrentByDefault = false;
63
64
// Flow magic to verify the exports of this file match the original version.
65
// eslint-disable-next-line no-unused-vars
packages/shared/forks/ReactFeatureFlags.test-renderer.www.js
+1
@@ -59,6 +59,7 @@ export const enableRecursiveCommitTraversal = false;
59
export const disableSchedulerTimeoutInWorkLoop = false;
60
export const enableLazyContextPropagation = false;
61
export const enableSyncDefaultUpdates = false;
62
+export const allowConcurrentByDefault = true;
63
64
// Flow magic to verify the exports of this file match the original version.
65
// eslint-disable-next-line no-unused-vars
packages/shared/forks/ReactFeatureFlags.testing.js
+1
@@ -59,6 +59,7 @@ export const enableRecursiveCommitTraversal = false;
59
export const disableSchedulerTimeoutInWorkLoop = false;
60
export const enableLazyContextPropagation = false;
61
export const enableSyncDefaultUpdates = true;
62
+export const allowConcurrentByDefault = false;
63
64
// Flow magic to verify the exports of this file match the original version.
65
// eslint-disable-next-line no-unused-vars
packages/shared/forks/ReactFeatureFlags.testing.www.js
+1
@@ -59,6 +59,7 @@ export const enableRecursiveCommitTraversal = false;
59
export const disableSchedulerTimeoutInWorkLoop = false;
60
export const enableLazyContextPropagation = false;
61
export const enableSyncDefaultUpdates = true;
62
+export const allowConcurrentByDefault = true;
63
64
// Flow magic to verify the exports of this file match the original version.
65
// eslint-disable-next-line no-unused-vars
packages/shared/forks/ReactFeatureFlags.www-dynamic.js
+1
@@ -60,3 +60,4 @@ export const enableProfilerNestedUpdateScheduledHook = __VARIANT__;
60
export const disableSchedulerTimeoutInWorkLoop = __VARIANT__;
61
export const enableLazyContextPropagation = __VARIANT__;
62
export const enableSyncDefaultUpdates = __VARIANT__;
63
+export const allowConcurrentByDefault = true;
packages/shared/forks/ReactFeatureFlags.www.js
+2
@@ -93,6 +93,8 @@ export const enableNewReconciler = __VARIANT__;
93
94
export const enableRecursiveCommitTraversal = false;
95
96
+export const allowConcurrentByDefault = true;
97
+
98
// Flow magic to verify the exports of this file match the original version.
99
// eslint-disable-next-line no-unused-vars
100
type Check<_X, Y: _X, X: Y = _X> = null;