Skip to content

Commit 9e9dac6

Browse files
authored
Add unstable_concurrentUpdatesByDefault (#21227)
1 parent 86f3385 commit 9e9dac6

24 files changed

+159
-38
lines changed

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

+35
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ let React = require('react');
1313
let ReactDOM = require('react-dom');
1414
let ReactDOMServer = require('react-dom/server');
1515
let Scheduler = require('scheduler');
16+
let act;
1617

1718
describe('ReactDOMRoot', () => {
1819
let container;
@@ -24,6 +25,7 @@ describe('ReactDOMRoot', () => {
2425
ReactDOM = require('react-dom');
2526
ReactDOMServer = require('react-dom/server');
2627
Scheduler = require('scheduler');
28+
act = require('react-dom/test-utils').unstable_concurrentAct;
2729
});
2830

2931
if (!__EXPERIMENTAL__) {
@@ -316,4 +318,37 @@ describe('ReactDOMRoot', () => {
316318
{withoutStack: true},
317319
);
318320
});
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+
});
319354
});

packages/react-dom/src/client/ReactDOMRoot.js

+11
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ export type RootOptions = {
2828
...
2929
},
3030
unstable_strictModeLevel?: number,
31+
unstable_concurrentUpdatesByDefault?: boolean,
3132
...
3233
};
3334

@@ -52,6 +53,7 @@ import {
5253
} from 'react-reconciler/src/ReactFiberReconciler';
5354
import invariant from 'shared/invariant';
5455
import {ConcurrentRoot, LegacyRoot} from 'react-reconciler/src/ReactRootTags';
56+
import {allowConcurrentByDefault} from 'shared/ReactFeatureFlags';
5557

5658
function ReactDOMRoot(container: Container, options: void | RootOptions) {
5759
this._internalRoot = createRootImpl(container, ConcurrentRoot, options);
@@ -126,12 +128,21 @@ function createRootImpl(
126128
? options.unstable_strictModeLevel
127129
: null;
128130

131+
let concurrentUpdatesByDefaultOverride = null;
132+
if (allowConcurrentByDefault) {
133+
concurrentUpdatesByDefaultOverride =
134+
options != null && options.unstable_concurrentUpdatesByDefault != null
135+
? options.unstable_concurrentUpdatesByDefault
136+
: null;
137+
}
138+
129139
const root = createContainer(
130140
container,
131141
tag,
132142
hydrate,
133143
hydrationCallbacks,
134144
strictModeLevelOverride,
145+
concurrentUpdatesByDefaultOverride,
135146
);
136147
markContainerAsRoot(root.current, container);
137148

packages/react-native-renderer/src/ReactFabric.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -207,7 +207,7 @@ function render(
207207
if (!root) {
208208
// TODO (bvaughn): If we decide to keep the wrapper component,
209209
// 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);
211211
roots.set(containerTag, root);
212212
}
213213
updateContainer(element, root, null, callback);

packages/react-native-renderer/src/ReactNativeRenderer.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -203,7 +203,7 @@ function render(
203203
if (!root) {
204204
// TODO (bvaughn): If we decide to keep the wrapper component,
205205
// 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);
207207
roots.set(containerTag, root);
208208
}
209209
updateContainer(element, root, null, callback);

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

+13
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ import {
2424
enableStrictEffects,
2525
enableProfilerTimer,
2626
enableScopeAPI,
27+
enableSyncDefaultUpdates,
28+
allowConcurrentByDefault,
2729
} from 'shared/ReactFeatureFlags';
2830
import {NoFlags, Placement, StaticMask} from './ReactFiberFlags';
2931
import {ConcurrentRoot} from './ReactRootTags';
@@ -68,6 +70,7 @@ import {
6870
ProfileMode,
6971
StrictLegacyMode,
7072
StrictEffectsMode,
73+
ConcurrentUpdatesByDefaultMode,
7174
} from './ReactTypeOfMode';
7275
import {
7376
REACT_FORWARD_REF_TYPE,
@@ -420,6 +423,7 @@ export function resetWorkInProgress(workInProgress: Fiber, renderLanes: Lanes) {
420423
export function createHostRootFiber(
421424
tag: RootTag,
422425
strictModeLevelOverride: null | number,
426+
concurrentUpdatesByDefaultOverride: null | boolean,
423427
): Fiber {
424428
let mode;
425429
if (tag === ConcurrentRoot) {
@@ -440,6 +444,15 @@ export function createHostRootFiber(
440444
mode |= StrictLegacyMode;
441445
}
442446
}
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+
}
443456
} else {
444457
mode = NoMode;
445458
}

packages/react-reconciler/src/ReactFiber.old.js

+13
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ import {
2424
enableStrictEffects,
2525
enableProfilerTimer,
2626
enableScopeAPI,
27+
enableSyncDefaultUpdates,
28+
allowConcurrentByDefault,
2729
} from 'shared/ReactFeatureFlags';
2830
import {NoFlags, Placement, StaticMask} from './ReactFiberFlags';
2931
import {ConcurrentRoot} from './ReactRootTags';
@@ -68,6 +70,7 @@ import {
6870
ProfileMode,
6971
StrictLegacyMode,
7072
StrictEffectsMode,
73+
ConcurrentUpdatesByDefaultMode,
7174
} from './ReactTypeOfMode';
7275
import {
7376
REACT_FORWARD_REF_TYPE,
@@ -420,6 +423,7 @@ export function resetWorkInProgress(workInProgress: Fiber, renderLanes: Lanes) {
420423
export function createHostRootFiber(
421424
tag: RootTag,
422425
strictModeLevelOverride: null | number,
426+
concurrentUpdatesByDefaultOverride: null | boolean,
423427
): Fiber {
424428
let mode;
425429
if (tag === ConcurrentRoot) {
@@ -440,6 +444,15 @@ export function createHostRootFiber(
440444
mode |= StrictLegacyMode;
441445
}
442446
}
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+
}
443456
} else {
444457
mode = NoMode;
445458
}

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

+20-14
Original file line numberDiff line numberDiff line change
@@ -39,9 +39,10 @@ import {
3939
enableCache,
4040
enableSchedulingProfiler,
4141
enableUpdaterTracking,
42-
enableSyncDefaultUpdates,
42+
allowConcurrentByDefault,
4343
} from 'shared/ReactFeatureFlags';
4444
import {isDevToolsPresent} from './ReactFiberDevToolsHook.new';
45+
import {ConcurrentUpdatesByDefaultMode, NoMode} from './ReactTypeOfMode';
4546

4647
export const SyncLanePriority: LanePriority = 12;
4748

@@ -318,11 +319,12 @@ export function getNextLanes(root: FiberRoot, wipLanes: Lanes): Lanes {
318319
}
319320

320321
if (
321-
// TODO: Check for root override, once that lands
322-
enableSyncDefaultUpdates &&
323-
(nextLanes & InputContinuousLane) !== NoLanes
322+
allowConcurrentByDefault &&
323+
(root.current.mode & ConcurrentUpdatesByDefaultMode) !== NoMode
324324
) {
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
326328
// and default updates, so they render in the same batch. The only reason
327329
// they use separate lanes is because continuous updates should interrupt
328330
// transitions, but default updates should not.
@@ -527,17 +529,21 @@ export function shouldTimeSlice(root: FiberRoot, lanes: Lanes) {
527529
// finish rendering without yielding execution.
528530
return false;
529531
}
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.
539538
return true;
540539
}
540+
541+
const SyncDefaultLanes =
542+
InputContinuousHydrationLane |
543+
InputContinuousLane |
544+
DefaultHydrationLane |
545+
DefaultLane;
546+
return (lanes & SyncDefaultLanes) === NoLanes;
541547
}
542548

543549
export function isTransitionLane(lane: Lane) {

packages/react-reconciler/src/ReactFiberLane.old.js

+20-14
Original file line numberDiff line numberDiff line change
@@ -39,9 +39,10 @@ import {
3939
enableCache,
4040
enableSchedulingProfiler,
4141
enableUpdaterTracking,
42-
enableSyncDefaultUpdates,
42+
allowConcurrentByDefault,
4343
} from 'shared/ReactFeatureFlags';
4444
import {isDevToolsPresent} from './ReactFiberDevToolsHook.old';
45+
import {ConcurrentUpdatesByDefaultMode, NoMode} from './ReactTypeOfMode';
4546

4647
export const SyncLanePriority: LanePriority = 12;
4748

@@ -318,11 +319,12 @@ export function getNextLanes(root: FiberRoot, wipLanes: Lanes): Lanes {
318319
}
319320

320321
if (
321-
// TODO: Check for root override, once that lands
322-
enableSyncDefaultUpdates &&
323-
(nextLanes & InputContinuousLane) !== NoLanes
322+
allowConcurrentByDefault &&
323+
(root.current.mode & ConcurrentUpdatesByDefaultMode) !== NoMode
324324
) {
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
326328
// and default updates, so they render in the same batch. The only reason
327329
// they use separate lanes is because continuous updates should interrupt
328330
// transitions, but default updates should not.
@@ -527,17 +529,21 @@ export function shouldTimeSlice(root: FiberRoot, lanes: Lanes) {
527529
// finish rendering without yielding execution.
528530
return false;
529531
}
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.
539538
return true;
540539
}
540+
541+
const SyncDefaultLanes =
542+
InputContinuousHydrationLane |
543+
InputContinuousLane |
544+
DefaultHydrationLane |
545+
DefaultLane;
546+
return (lanes & SyncDefaultLanes) === NoLanes;
541547
}
542548

543549
export function isTransitionLane(lane: Lane) {

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

+2
Original file line numberDiff line numberDiff line change
@@ -249,13 +249,15 @@ export function createContainer(
249249
hydrate: boolean,
250250
hydrationCallbacks: null | SuspenseHydrationCallbacks,
251251
strictModeLevelOverride: null | number,
252+
concurrentUpdatesByDefaultOverride: null | boolean,
252253
): OpaqueRoot {
253254
return createFiberRoot(
254255
containerInfo,
255256
tag,
256257
hydrate,
257258
hydrationCallbacks,
258259
strictModeLevelOverride,
260+
concurrentUpdatesByDefaultOverride,
259261
);
260262
}
261263

packages/react-reconciler/src/ReactFiberReconciler.old.js

+2
Original file line numberDiff line numberDiff line change
@@ -249,13 +249,15 @@ export function createContainer(
249249
hydrate: boolean,
250250
hydrationCallbacks: null | SuspenseHydrationCallbacks,
251251
strictModeLevelOverride: null | number,
252+
concurrentUpdatesByDefaultOverride: null | boolean,
252253
): OpaqueRoot {
253254
return createFiberRoot(
254255
containerInfo,
255256
tag,
256257
hydrate,
257258
hydrationCallbacks,
258259
strictModeLevelOverride,
260+
concurrentUpdatesByDefaultOverride,
259261
);
260262
}
261263

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

+6-1
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@ export function createFiberRoot(
9999
hydrate: boolean,
100100
hydrationCallbacks: null | SuspenseHydrationCallbacks,
101101
strictModeLevelOverride: null | number,
102+
concurrentUpdatesByDefaultOverride: null | boolean,
102103
): FiberRoot {
103104
const root: FiberRoot = (new FiberRootNode(containerInfo, tag, hydrate): any);
104105
if (enableSuspenseCallback) {
@@ -107,7 +108,11 @@ export function createFiberRoot(
107108

108109
// Cyclic construction. This cheats the type system right now because
109110
// stateNode is any.
110-
const uninitializedFiber = createHostRootFiber(tag, strictModeLevelOverride);
111+
const uninitializedFiber = createHostRootFiber(
112+
tag,
113+
strictModeLevelOverride,
114+
concurrentUpdatesByDefaultOverride,
115+
);
111116
root.current = uninitializedFiber;
112117
uninitializedFiber.stateNode = root;
113118

packages/react-reconciler/src/ReactFiberRoot.old.js

+6-1
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@ export function createFiberRoot(
9999
hydrate: boolean,
100100
hydrationCallbacks: null | SuspenseHydrationCallbacks,
101101
strictModeLevelOverride: null | number,
102+
concurrentUpdatesByDefaultOverride: null | boolean,
102103
): FiberRoot {
103104
const root: FiberRoot = (new FiberRootNode(containerInfo, tag, hydrate): any);
104105
if (enableSuspenseCallback) {
@@ -107,7 +108,11 @@ export function createFiberRoot(
107108

108109
// Cyclic construction. This cheats the type system right now because
109110
// stateNode is any.
110-
const uninitializedFiber = createHostRootFiber(tag, strictModeLevelOverride);
111+
const uninitializedFiber = createHostRootFiber(
112+
tag,
113+
strictModeLevelOverride,
114+
concurrentUpdatesByDefaultOverride,
115+
);
111116
root.current = uninitializedFiber;
112117
uninitializedFiber.stateNode = root;
113118

0 commit comments

Comments
 (0)