Skip to content

Commit 4c12339

Browse files
authored
[DOM] move flushSync out of the reconciler (#28500)
This PR moves `flushSync` out of the reconciler. there is still an internal implementation that is used when these semantics are needed for React methods such as `unmount` on roots. This new isomorphic `flushSync` is only used in builds that no longer support legacy mode. Additionally all the internal uses of flushSync in the reconciler have been replaced with more direct methods. There is a new `updateContainerSync` method which updates a container but forces it to the Sync lane and flushes passive effects if necessary. This combined with flushSyncWork can be used to replace flushSync for all instances of internal usage. We still maintain the original flushSync implementation as `flushSyncFromReconciler` because it will be used as the flushSync implementation for FB builds. This is because it has special legacy mode handling that the new isomorphic implementation does not need to consider. It will be removed from production OSS builds by closure though
1 parent 8e1462e commit 4c12339

18 files changed

+248
-84
lines changed

packages/react-art/src/ReactART.js

+8-11
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,9 @@ import ReactVersion from 'shared/ReactVersion';
1010
import {LegacyRoot, ConcurrentRoot} from 'react-reconciler/src/ReactRootTags';
1111
import {
1212
createContainer,
13-
updateContainer,
13+
updateContainerSync,
1414
injectIntoDevTools,
15-
flushSync,
15+
flushSyncWork,
1616
} from 'react-reconciler/src/ReactFiberReconciler';
1717
import Transform from 'art/core/transform';
1818
import Mode from 'art/modes/current';
@@ -78,9 +78,8 @@ class Surface extends React.Component {
7878
);
7979
// We synchronously flush updates coming from above so that they commit together
8080
// and so that refs resolve before the parent life cycles.
81-
flushSync(() => {
82-
updateContainer(this.props.children, this._mountNode, this);
83-
});
81+
updateContainerSync(this.props.children, this._mountNode, this);
82+
flushSyncWork();
8483
}
8584

8685
componentDidUpdate(prevProps, prevState) {
@@ -92,9 +91,8 @@ class Surface extends React.Component {
9291

9392
// We synchronously flush updates coming from above so that they commit together
9493
// and so that refs resolve before the parent life cycles.
95-
flushSync(() => {
96-
updateContainer(this.props.children, this._mountNode, this);
97-
});
94+
updateContainerSync(this.props.children, this._mountNode, this);
95+
flushSyncWork();
9896

9997
if (this._surface.render) {
10098
this._surface.render();
@@ -104,9 +102,8 @@ class Surface extends React.Component {
104102
componentWillUnmount() {
105103
// We synchronously flush updates coming from above so that they commit together
106104
// and so that refs resolve before the parent life cycles.
107-
flushSync(() => {
108-
updateContainer(null, this._mountNode, this);
109-
});
105+
updateContainerSync(null, this._mountNode, this);
106+
flushSyncWork();
110107
}
111108

112109
render() {

packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js

+19
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ import {
9090
enableScopeAPI,
9191
enableTrustedTypesIntegration,
9292
enableAsyncActions,
93+
disableLegacyMode,
9394
} from 'shared/ReactFeatureFlags';
9495
import {
9596
HostComponent,
@@ -100,6 +101,7 @@ import {
100101
import {listenToAllSupportedEvents} from '../events/DOMPluginEventSystem';
101102
import {validateLinkPropsForStyleResource} from '../shared/ReactDOMResourceValidation';
102103
import escapeSelectorAttributeValueInsideDoubleQuotes from './escapeSelectorAttributeValueInsideDoubleQuotes';
104+
import {flushSyncWork as flushSyncWorkOnAllRoots} from 'react-reconciler/src/ReactFiberWorkLoop';
103105

104106
import ReactDOMSharedInternals from 'shared/ReactDOMSharedInternals';
105107
const ReactDOMCurrentDispatcher =
@@ -1924,6 +1926,9 @@ function getDocumentFromRoot(root: HoistableRoot): Document {
19241926

19251927
const previousDispatcher = ReactDOMCurrentDispatcher.current;
19261928
ReactDOMCurrentDispatcher.current = {
1929+
flushSyncWork: disableLegacyMode
1930+
? flushSyncWork
1931+
: previousDispatcher.flushSyncWork,
19271932
prefetchDNS,
19281933
preconnect,
19291934
preload,
@@ -1933,6 +1938,20 @@ ReactDOMCurrentDispatcher.current = {
19331938
preinitModuleScript,
19341939
};
19351940

1941+
function flushSyncWork() {
1942+
if (disableLegacyMode) {
1943+
const previousWasRendering = previousDispatcher.flushSyncWork();
1944+
const wasRendering = flushSyncWorkOnAllRoots();
1945+
// Since multiple dispatchers can flush sync work during a single flushSync call
1946+
// we need to return true if any of them were rendering.
1947+
return previousWasRendering || wasRendering;
1948+
} else {
1949+
throw new Error(
1950+
'flushSyncWork should not be called from builds that support legacy mode. This is a bug in React.',
1951+
);
1952+
}
1953+
}
1954+
19361955
// We expect this to get inlined. It is a function mostly to communicate the special nature of
19371956
// how we resolve the HoistableRoot for ReactDOM.pre*() methods. Because we support calling
19381957
// these methods outside of render there is no way to know which Document or ShadowRoot is 'scoped'

packages/react-dom-bindings/src/events/ReactDOMUpdateBatching.js

+4-2
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import {
1313
import {
1414
batchedUpdates as batchedUpdatesImpl,
1515
discreteUpdates as discreteUpdatesImpl,
16-
flushSync as flushSyncImpl,
16+
flushSyncWork,
1717
} from 'react-reconciler/src/ReactFiberReconciler';
1818

1919
// Used as a way to call batchedUpdates when we don't have a reference to
@@ -36,7 +36,9 @@ function finishEventHandler() {
3636
// bails out of the update without touching the DOM.
3737
// TODO: Restore state in the microtask, after the discrete updates flush,
3838
// instead of early flushing them here.
39-
flushSyncImpl();
39+
// @TODO Should move to flushSyncWork once legacy mode is removed but since this flushSync
40+
// flushes passive effects we can't do this yet.
41+
flushSyncWork();
4042
restoreStateIfNeeded();
4143
}
4244
}

packages/react-dom-bindings/src/server/ReactDOMFlightServerHostDispatcher.js

+1
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ const ReactDOMCurrentDispatcher =
2828

2929
const previousDispatcher = ReactDOMCurrentDispatcher.current;
3030
ReactDOMCurrentDispatcher.current = {
31+
flushSyncWork: previousDispatcher.flushSyncWork,
3132
prefetchDNS,
3233
preconnect,
3334
preload,

packages/react-dom-bindings/src/server/ReactFizzConfigDOM.js

+1
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ const ReactDOMCurrentDispatcher =
8888

8989
const previousDispatcher = ReactDOMCurrentDispatcher.current;
9090
ReactDOMCurrentDispatcher.current = {
91+
flushSyncWork: previousDispatcher.flushSyncWork,
9192
prefetchDNS,
9293
preconnect,
9394
preload,

packages/react-dom/src/ReactDOMSharedInternals.js

+1
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ type InternalsType = {
2929
function noop() {}
3030

3131
const DefaultDispatcher: HostDispatcher = {
32+
flushSyncWork: noop,
3233
prefetchDNS: noop,
3334
preconnect: noop,
3435
preload: noop,

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

+10-4
Original file line numberDiff line numberDiff line change
@@ -14,16 +14,18 @@ import type {
1414
CreateRootOptions,
1515
} from './ReactDOMRoot';
1616

17+
import {disableLegacyMode} from 'shared/ReactFeatureFlags';
1718
import {
1819
createRoot as createRootImpl,
1920
hydrateRoot as hydrateRootImpl,
2021
isValidContainer,
2122
} from './ReactDOMRoot';
2223
import {createEventHandle} from 'react-dom-bindings/src/client/ReactDOMEventHandle';
2324
import {runWithPriority} from 'react-dom-bindings/src/client/ReactDOMUpdatePriority';
25+
import {flushSync as flushSyncIsomorphic} from '../shared/ReactDOMFlushSync';
2426

2527
import {
26-
flushSync as flushSyncWithoutWarningIfAlreadyRendering,
28+
flushSyncFromReconciler as flushSyncWithoutWarningIfAlreadyRendering,
2729
isAlreadyRendering,
2830
injectIntoDevTools,
2931
findHostInstance,
@@ -123,11 +125,11 @@ function hydrateRoot(
123125

124126
// Overload the definition to the two valid signatures.
125127
// Warning, this opts-out of checking the function body.
126-
declare function flushSync<R>(fn: () => R): R;
128+
declare function flushSyncFromReconciler<R>(fn: () => R): R;
127129
// eslint-disable-next-line no-redeclare
128-
declare function flushSync(): void;
130+
declare function flushSyncFromReconciler(): void;
129131
// eslint-disable-next-line no-redeclare
130-
function flushSync<R>(fn: (() => R) | void): R | void {
132+
function flushSyncFromReconciler<R>(fn: (() => R) | void): R | void {
131133
if (__DEV__) {
132134
if (isAlreadyRendering()) {
133135
console.error(
@@ -140,6 +142,10 @@ function flushSync<R>(fn: (() => R) | void): R | void {
140142
return flushSyncWithoutWarningIfAlreadyRendering(fn);
141143
}
142144

145+
const flushSync: typeof flushSyncIsomorphic = disableLegacyMode
146+
? flushSyncIsomorphic
147+
: flushSyncFromReconciler;
148+
143149
function findDOMNode(
144150
componentOrElement: React$Component<any, any>,
145151
): null | Element | Text {

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

+4-4
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,8 @@ import {
9393
createContainer,
9494
createHydrationContainer,
9595
updateContainer,
96-
flushSync,
96+
updateContainerSync,
97+
flushSyncWork,
9798
isAlreadyRendering,
9899
defaultOnUncaughtError,
99100
defaultOnCaughtError,
@@ -161,9 +162,8 @@ ReactDOMHydrationRoot.prototype.unmount = ReactDOMRoot.prototype.unmount =
161162
);
162163
}
163164
}
164-
flushSync(() => {
165-
updateContainer(null, root, null, null);
166-
});
165+
updateContainerSync(null, root, null, null);
166+
flushSyncWork();
167167
unmarkContainerAsRoot(container);
168168
}
169169
};

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

+12-15
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,8 @@ import {
4949
createHydrationContainer,
5050
findHostInstanceWithNoPortals,
5151
updateContainer,
52-
flushSync,
52+
updateContainerSync,
53+
flushSyncWork,
5354
getPublicRootInstance,
5455
findHostInstance,
5556
findHostInstanceWithWarning,
@@ -247,7 +248,7 @@ function legacyCreateRootFromDOMContainer(
247248
// $FlowFixMe[incompatible-call]
248249
listenToAllSupportedEvents(rootContainerElement);
249250

250-
flushSync();
251+
flushSyncWork();
251252
return root;
252253
} else {
253254
// First clear any existing content.
@@ -282,9 +283,8 @@ function legacyCreateRootFromDOMContainer(
282283
listenToAllSupportedEvents(rootContainerElement);
283284

284285
// Initial mount should not be batched.
285-
flushSync(() => {
286-
updateContainer(initialChildren, root, parentComponent, callback);
287-
});
286+
updateContainerSync(initialChildren, root, parentComponent, callback);
287+
flushSyncWork();
288288

289289
return root;
290290
}
@@ -485,6 +485,8 @@ export function unmountComponentAtNode(container: Container): boolean {
485485
}
486486

487487
if (container._reactRootContainer) {
488+
const root = container._reactRootContainer;
489+
488490
if (__DEV__) {
489491
const rootEl = getReactRootElementInContainer(container);
490492
const renderedByDifferentReact = rootEl && !getInstanceFromNode(rootEl);
@@ -496,16 +498,11 @@ export function unmountComponentAtNode(container: Container): boolean {
496498
}
497499
}
498500

499-
// Unmount should not be batched.
500-
flushSync(() => {
501-
legacyRenderSubtreeIntoContainer(null, null, container, false, () => {
502-
// $FlowFixMe[incompatible-type] This should probably use `delete container._reactRootContainer`
503-
container._reactRootContainer = null;
504-
unmarkContainerAsRoot(container);
505-
});
506-
});
507-
// If you call unmountComponentAtNode twice in quick succession, you'll
508-
// get `true` twice. That's probably fine?
501+
updateContainerSync(null, root, null, null);
502+
flushSyncWork();
503+
// $FlowFixMe[incompatible-type] This should probably use `delete container._reactRootContainer`
504+
container._reactRootContainer = null;
505+
unmarkContainerAsRoot(container);
509506
return true;
510507
} else {
511508
if (__DEV__) {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
/**
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @flow
8+
*/
9+
10+
import type {BatchConfig} from 'react/src/ReactCurrentBatchConfig';
11+
12+
import {disableLegacyMode} from 'shared/ReactFeatureFlags';
13+
import {DiscreteEventPriority} from 'react-reconciler/src/ReactEventPriorities';
14+
15+
import ReactSharedInternals from 'shared/ReactSharedInternals';
16+
const ReactCurrentBatchConfig: BatchConfig =
17+
ReactSharedInternals.ReactCurrentBatchConfig;
18+
19+
import ReactDOMSharedInternals from 'shared/ReactDOMSharedInternals';
20+
const ReactDOMCurrentDispatcher =
21+
ReactDOMSharedInternals.ReactDOMCurrentDispatcher;
22+
23+
declare function flushSyncImpl<R>(fn: () => R): R;
24+
declare function flushSyncImpl(void): void;
25+
function flushSyncImpl<R>(fn: (() => R) | void): R | void {
26+
const previousTransition = ReactCurrentBatchConfig.transition;
27+
const previousUpdatePriority =
28+
ReactDOMSharedInternals.up; /* ReactDOMCurrentUpdatePriority */
29+
30+
try {
31+
ReactCurrentBatchConfig.transition = null;
32+
ReactDOMSharedInternals.up /* ReactDOMCurrentUpdatePriority */ =
33+
DiscreteEventPriority;
34+
if (fn) {
35+
return fn();
36+
} else {
37+
return undefined;
38+
}
39+
} finally {
40+
ReactCurrentBatchConfig.transition = previousTransition;
41+
ReactDOMSharedInternals.up /* ReactDOMCurrentUpdatePriority */ =
42+
previousUpdatePriority;
43+
const wasInRender = ReactDOMCurrentDispatcher.current.flushSyncWork();
44+
if (__DEV__) {
45+
if (wasInRender) {
46+
console.error(
47+
'flushSync was called from inside a lifecycle method. React cannot ' +
48+
'flush when React is already rendering. Consider moving this call to ' +
49+
'a scheduler task or micro task.',
50+
);
51+
}
52+
}
53+
}
54+
}
55+
56+
declare function flushSyncErrorInBuildsThatSupportLegacyMode<R>(fn: () => R): R;
57+
declare function flushSyncErrorInBuildsThatSupportLegacyMode(void): void;
58+
function flushSyncErrorInBuildsThatSupportLegacyMode() {
59+
// eslint-disable-next-line react-internal/prod-error-codes
60+
throw new Error(
61+
'Expected this build of React to not support legacy mode but it does. This is a bug in React.',
62+
);
63+
}
64+
65+
export const flushSync: typeof flushSyncImpl = disableLegacyMode
66+
? flushSyncImpl
67+
: flushSyncErrorInBuildsThatSupportLegacyMode;

packages/react-dom/src/shared/ReactDOMTypes.js

+1
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ export type PreinitModuleScriptOptions = {
8282
};
8383

8484
export type HostDispatcher = {
85+
flushSyncWork: () => boolean | void,
8586
prefetchDNS: (href: string) => void,
8687
preconnect: (href: string, crossOrigin?: ?CrossOriginEnum) => void,
8788
preload: (href: string, as: string, options?: ?PreloadImplOptions) => void,

packages/react-noop-renderer/src/createReactNoop.js

+24-1
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import isArray from 'shared/isArray';
2929
import {checkPropStringCoercion} from 'shared/CheckStringCoercion';
3030
import {
3131
NoEventPriority,
32+
DiscreteEventPriority,
3233
DefaultEventPriority,
3334
IdleEventPriority,
3435
ConcurrentRoot,
@@ -40,6 +41,9 @@ import {
4041
disableStringRefs,
4142
} from 'shared/ReactFeatureFlags';
4243

44+
import ReactSharedInternals from 'shared/ReactSharedInternals';
45+
const ReactCurrentBatchConfig = ReactSharedInternals.ReactCurrentBatchConfig;
46+
4347
type Container = {
4448
rootID: string,
4549
children: Array<Instance | TextInstance>,
@@ -943,7 +947,25 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
943947
);
944948
}
945949
}
946-
return NoopRenderer.flushSync(fn);
950+
if (disableLegacyMode) {
951+
const previousTransition = ReactCurrentBatchConfig.transition;
952+
const preivousEventPriority = currentEventPriority;
953+
try {
954+
ReactCurrentBatchConfig.transition = null;
955+
currentEventPriority = DiscreteEventPriority;
956+
if (fn) {
957+
return fn();
958+
} else {
959+
return undefined;
960+
}
961+
} finally {
962+
ReactCurrentBatchConfig.transition = previousTransition;
963+
currentEventPriority = preivousEventPriority;
964+
NoopRenderer.flushSyncWork();
965+
}
966+
} else {
967+
return NoopRenderer.flushSyncFromReconciler(fn);
968+
}
947969
}
948970

949971
function onRecoverableError(error) {
@@ -1081,6 +1103,7 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
10811103
getChildrenAsJSX() {
10821104
return getChildrenAsJSX(container);
10831105
},
1106+
legacy: true,
10841107
};
10851108
},
10861109

0 commit comments

Comments
 (0)