Skip to content

Commit 7e746d5

Browse files
committed
Implements flushSync in react-dom without relying on the reconciler. This will only be available in builds that no longer support legacy mode because the reconciler flushSync has special logic for legacy mode which is not necessary for concurrent roots.
This is one option for how we might recover existing flushSync priorities with an external flushSync implementation. To reduce hidden class checks we need to read the batch config once which requires inlining the requestCurrentTransition implementation. Moves the legacy implementation of flushSync to the fb entrypoint The cost of the separate file is not really warranted. Will use feature flag to scope build specific implementations Moved error to fiber config. The reconciler implementation should be DCE'd in builds that still support legacy mode Exposes an updateContainerSync implementation so we can avoid depending on flushSyncFromReconciler in hot reloading Removes flushSyncFromReconciler from ReactDOMRoot Removes flushSyncFromReconciler from ReactDOMUpdateBatching Removes flushSyncFromReconciler use from ReactFiberReconciler Removes flushSyncFromReconciler from ReactART Rather than use event priority or lane type semantics for the batch config it now uses a boolean. There really is only a binary of sync or not sync so we don't need to express this concept as something overly specific to Fiber. simplifies the during render case to in dev to avoid a bit of extra work. There is no harm in returning true in prod even if there is nothing to react to it.
1 parent 5d28591 commit 7e746d5

File tree

18 files changed

+240
-73
lines changed

18 files changed

+240
-73
lines changed

packages/react-art/src/ReactART.js

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,10 @@ 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,
16+
flushPassiveEffects,
1617
} from 'react-reconciler/src/ReactFiberReconciler';
1718
import Transform from 'art/core/transform';
1819
import Mode from 'art/modes/current';
@@ -78,9 +79,8 @@ class Surface extends React.Component {
7879
);
7980
// We synchronously flush updates coming from above so that they commit together
8081
// and so that refs resolve before the parent life cycles.
81-
flushSync(() => {
82-
updateContainer(this.props.children, this._mountNode, this);
83-
});
82+
updateContainerSync(this.props.children, this._mountNode, this);
83+
flushSyncWork();
8484
}
8585

8686
componentDidUpdate(prevProps, prevState) {
@@ -92,9 +92,8 @@ class Surface extends React.Component {
9292

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

9998
if (this._surface.render) {
10099
this._surface.render();
@@ -104,9 +103,8 @@ class Surface extends React.Component {
104103
componentWillUnmount() {
105104
// We synchronously flush updates coming from above so that they commit together
106105
// and so that refs resolve before the parent life cycles.
107-
flushSync(() => {
108-
updateContainer(null, this._mountNode, this);
109-
});
106+
updateContainerSync(null, this._mountNode, this);
107+
flushSyncWork();
110108
}
111109

112110
render() {

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

Lines changed: 19 additions & 0 deletions
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

Lines changed: 4 additions & 2 deletions
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

Lines changed: 1 addition & 0 deletions
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

Lines changed: 1 addition & 0 deletions
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

Lines changed: 1 addition & 0 deletions
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

Lines changed: 10 additions & 4 deletions
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

Lines changed: 4 additions & 4 deletions
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

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ import {
4949
createHydrationContainer,
5050
findHostInstanceWithNoPortals,
5151
updateContainer,
52-
flushSync,
52+
flushSyncFromReconciler,
5353
getPublicRootInstance,
5454
findHostInstance,
5555
findHostInstanceWithWarning,
@@ -247,7 +247,7 @@ function legacyCreateRootFromDOMContainer(
247247
// $FlowFixMe[incompatible-call]
248248
listenToAllSupportedEvents(rootContainerElement);
249249

250-
flushSync();
250+
flushSyncFromReconciler();
251251
return root;
252252
} else {
253253
// First clear any existing content.
@@ -282,7 +282,7 @@ function legacyCreateRootFromDOMContainer(
282282
listenToAllSupportedEvents(rootContainerElement);
283283

284284
// Initial mount should not be batched.
285-
flushSync(() => {
285+
flushSyncFromReconciler(() => {
286286
updateContainer(initialChildren, root, parentComponent, callback);
287287
});
288288

@@ -497,7 +497,7 @@ export function unmountComponentAtNode(container: Container): boolean {
497497
}
498498

499499
// Unmount should not be batched.
500-
flushSync(() => {
500+
flushSyncFromReconciler(() => {
501501
legacyRenderSubtreeIntoContainer(null, null, container, false, () => {
502502
// $FlowFixMe[incompatible-type] This should probably use `delete container._reactRootContainer`
503503
container._reactRootContainer = null;
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
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+
const ReactDOMCurrentUpdatePriority =
23+
ReactDOMSharedInternals.ReactDOMCurrentUpdatePriority;
24+
25+
declare function flushSyncImpl<R>(fn: () => R): R;
26+
declare function flushSyncImpl(void): void;
27+
function flushSyncImpl<R>(fn: (() => R) | void): R | void {
28+
const previousTransition = ReactCurrentBatchConfig.transition;
29+
const previousUpdatePriority = ReactDOMCurrentUpdatePriority.current;
30+
31+
try {
32+
ReactCurrentBatchConfig.transition = null;
33+
ReactDOMCurrentUpdatePriority.current = DiscreteEventPriority;
34+
if (fn) {
35+
return fn();
36+
} else {
37+
return undefined;
38+
}
39+
} finally {
40+
ReactCurrentBatchConfig.transition = previousTransition;
41+
ReactDOMCurrentUpdatePriority.current = previousUpdatePriority;
42+
const wasInRender = ReactDOMCurrentDispatcher.current.flushSyncWork();
43+
if (__DEV__) {
44+
if (wasInRender) {
45+
console.error(
46+
'flushSync was called from inside a lifecycle method. React cannot ' +
47+
'flush when React is already rendering. Consider moving this call to ' +
48+
'a scheduler task or micro task.',
49+
);
50+
}
51+
}
52+
}
53+
}
54+
55+
declare function flushSyncErrorInBuildsThatSupportLegacyMode<R>(fn: () => R): R;
56+
declare function flushSyncErrorInBuildsThatSupportLegacyMode(void): void;
57+
function flushSyncErrorInBuildsThatSupportLegacyMode() {
58+
// eslint-disable-next-line react-internal/prod-error-codes
59+
throw new Error(
60+
'Expected this build of React to not support legacy mode but it does. This is a bug in React.',
61+
);
62+
}
63+
64+
export const flushSync: typeof flushSyncImpl = disableLegacyMode
65+
? flushSyncImpl
66+
: flushSyncErrorInBuildsThatSupportLegacyMode;

0 commit comments

Comments
 (0)