@samitouri / QOS-React / commits / 9e9dac6505

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;