@samitouri / QOS-React-2 / commits / 4c12339ce3

[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

Josh Story committed Apr 8, 2024 at 09:03 UTC 4c12339ce3fa398050d1026c616ea43d43dcaf4a
18 files changed +248 -84
packages/react-art/src/ReactART.js
+8 -11
@@ -10,9 +10,9 @@ import ReactVersion from 'shared/ReactVersion';
10 import {LegacyRoot, ConcurrentRoot} from 'react-reconciler/src/ReactRootTags';
11 import {
12 createContainer,
13 - updateContainer,
13 + updateContainerSync,
14 injectIntoDevTools,
15 - flushSync,
15 + flushSyncWork,
16 } from 'react-reconciler/src/ReactFiberReconciler';
17 import Transform from 'art/core/transform';
18 import Mode from 'art/modes/current';
@@ -78,9 +78,8 @@ class Surface extends React.Component {
78 );
79 // We synchronously flush updates coming from above so that they commit together
80 // 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();
83 }
84
85 componentDidUpdate(prevProps, prevState) {
@@ -92,9 +91,8 @@ class Surface extends React.Component {
91
92 // We synchronously flush updates coming from above so that they commit together
93 // 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();
96
97 if (this._surface.render) {
98 this._surface.render();
@@ -104,9 +102,8 @@ class Surface extends React.Component {
102 componentWillUnmount() {
103 // We synchronously flush updates coming from above so that they commit together
104 // 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();
107 }
108
109 render() {
packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js
+19
@@ -90,6 +90,7 @@ import {
90 enableScopeAPI,
91 enableTrustedTypesIntegration,
92 enableAsyncActions,
93 + disableLegacyMode,
94 } from 'shared/ReactFeatureFlags';
95 import {
96 HostComponent,
@@ -100,6 +101,7 @@ import {
101 import {listenToAllSupportedEvents} from '../events/DOMPluginEventSystem';
102 import {validateLinkPropsForStyleResource} from '../shared/ReactDOMResourceValidation';
103 import escapeSelectorAttributeValueInsideDoubleQuotes from './escapeSelectorAttributeValueInsideDoubleQuotes';
104 +import {flushSyncWork as flushSyncWorkOnAllRoots} from 'react-reconciler/src/ReactFiberWorkLoop';
105
106 import ReactDOMSharedInternals from 'shared/ReactDOMSharedInternals';
107 const ReactDOMCurrentDispatcher =
@@ -1924,6 +1926,9 @@ function getDocumentFromRoot(root: HoistableRoot): Document {
1926
1927 const previousDispatcher = ReactDOMCurrentDispatcher.current;
1928 ReactDOMCurrentDispatcher.current = {
1929 + flushSyncWork: disableLegacyMode
1930 + ? flushSyncWork
1931 + : previousDispatcher.flushSyncWork,
1932 prefetchDNS,
1933 preconnect,
1934 preload,
@@ -1933,6 +1938,20 @@ ReactDOMCurrentDispatcher.current = {
1938 preinitModuleScript,
1939 };
1940
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 +
1955 // We expect this to get inlined. It is a function mostly to communicate the special nature of
1956 // how we resolve the HoistableRoot for ReactDOM.pre*() methods. Because we support calling
1957 // 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
@@ -13,7 +13,7 @@ import {
13 import {
14 batchedUpdates as batchedUpdatesImpl,
15 discreteUpdates as discreteUpdatesImpl,
16 - flushSync as flushSyncImpl,
16 + flushSyncWork,
17 } from 'react-reconciler/src/ReactFiberReconciler';
18
19 // Used as a way to call batchedUpdates when we don't have a reference to
@@ -36,7 +36,9 @@ function finishEventHandler() {
36 // bails out of the update without touching the DOM.
37 // TODO: Restore state in the microtask, after the discrete updates flush,
38 // 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();
42 restoreStateIfNeeded();
43 }
44 }
packages/react-dom-bindings/src/server/ReactDOMFlightServerHostDispatcher.js
+1
@@ -28,6 +28,7 @@ const ReactDOMCurrentDispatcher =
28
29 const previousDispatcher = ReactDOMCurrentDispatcher.current;
30 ReactDOMCurrentDispatcher.current = {
31 + flushSyncWork: previousDispatcher.flushSyncWork,
32 prefetchDNS,
33 preconnect,
34 preload,
packages/react-dom-bindings/src/server/ReactFizzConfigDOM.js
+1
@@ -88,6 +88,7 @@ const ReactDOMCurrentDispatcher =
88
89 const previousDispatcher = ReactDOMCurrentDispatcher.current;
90 ReactDOMCurrentDispatcher.current = {
91 + flushSyncWork: previousDispatcher.flushSyncWork,
92 prefetchDNS,
93 preconnect,
94 preload,
packages/react-dom/src/ReactDOMSharedInternals.js
+1
@@ -29,6 +29,7 @@ type InternalsType = {
29 function noop() {}
30
31 const DefaultDispatcher: HostDispatcher = {
32 + flushSyncWork: noop,
33 prefetchDNS: noop,
34 preconnect: noop,
35 preload: noop,
packages/react-dom/src/client/ReactDOM.js
+10 -4
@@ -14,6 +14,7 @@ import type {
14 CreateRootOptions,
15 } from './ReactDOMRoot';
16
17 +import {disableLegacyMode} from 'shared/ReactFeatureFlags';
18 import {
19 createRoot as createRootImpl,
20 hydrateRoot as hydrateRootImpl,
@@ -21,9 +22,10 @@ import {
22 } from './ReactDOMRoot';
23 import {createEventHandle} from 'react-dom-bindings/src/client/ReactDOMEventHandle';
24 import {runWithPriority} from 'react-dom-bindings/src/client/ReactDOMUpdatePriority';
25 +import {flushSync as flushSyncIsomorphic} from '../shared/ReactDOMFlushSync';
26
27 import {
26 - flushSync as flushSyncWithoutWarningIfAlreadyRendering,
28 + flushSyncFromReconciler as flushSyncWithoutWarningIfAlreadyRendering,
29 isAlreadyRendering,
30 injectIntoDevTools,
31 findHostInstance,
@@ -123,11 +125,11 @@ function hydrateRoot(
125
126 // Overload the definition to the two valid signatures.
127 // 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;
129 // eslint-disable-next-line no-redeclare
128 -declare function flushSync(): void;
130 +declare function flushSyncFromReconciler(): void;
131 // eslint-disable-next-line no-redeclare
130 -function flushSync<R>(fn: (() => R) | void): R | void {
132 +function flushSyncFromReconciler<R>(fn: (() => R) | void): R | void {
133 if (__DEV__) {
134 if (isAlreadyRendering()) {
135 console.error(
@@ -140,6 +142,10 @@ function flushSync<R>(fn: (() => R) | void): R | void {
142 return flushSyncWithoutWarningIfAlreadyRendering(fn);
143 }
144
145 +const flushSync: typeof flushSyncIsomorphic = disableLegacyMode
146 + ? flushSyncIsomorphic
147 + : flushSyncFromReconciler;
148 +
149 function findDOMNode(
150 componentOrElement: React$Component<any, any>,
151 ): null | Element | Text {
packages/react-dom/src/client/ReactDOMRoot.js
+4 -4
@@ -93,7 +93,8 @@ import {
93 createContainer,
94 createHydrationContainer,
95 updateContainer,
96 - flushSync,
96 + updateContainerSync,
97 + flushSyncWork,
98 isAlreadyRendering,
99 defaultOnUncaughtError,
100 defaultOnCaughtError,
@@ -161,9 +162,8 @@ ReactDOMHydrationRoot.prototype.unmount = ReactDOMRoot.prototype.unmount =
162 );
163 }
164 }
164 - flushSync(() => {
165 - updateContainer(null, root, null, null);
166 - });
165 + updateContainerSync(null, root, null, null);
166 + flushSyncWork();
167 unmarkContainerAsRoot(container);
168 }
169 };
packages/react-dom/src/client/ReactDOMRootFB.js
+12 -15
@@ -49,7 +49,8 @@ import {
49 createHydrationContainer,
50 findHostInstanceWithNoPortals,
51 updateContainer,
52 - flushSync,
52 + updateContainerSync,
53 + flushSyncWork,
54 getPublicRootInstance,
55 findHostInstance,
56 findHostInstanceWithWarning,
@@ -247,7 +248,7 @@ function legacyCreateRootFromDOMContainer(
248 // $FlowFixMe[incompatible-call]
249 listenToAllSupportedEvents(rootContainerElement);
250
250 - flushSync();
251 + flushSyncWork();
252 return root;
253 } else {
254 // First clear any existing content.
@@ -282,9 +283,8 @@ function legacyCreateRootFromDOMContainer(
283 listenToAllSupportedEvents(rootContainerElement);
284
285 // Initial mount should not be batched.
285 - flushSync(() => {
286 - updateContainer(initialChildren, root, parentComponent, callback);
287 - });
286 + updateContainerSync(initialChildren, root, parentComponent, callback);
287 + flushSyncWork();
288
289 return root;
290 }
@@ -485,6 +485,8 @@ export function unmountComponentAtNode(container: Container): boolean {
485 }
486
487 if (container._reactRootContainer) {
488 + const root = container._reactRootContainer;
489 +
490 if (__DEV__) {
491 const rootEl = getReactRootElementInContainer(container);
492 const renderedByDifferentReact = rootEl && !getInstanceFromNode(rootEl);
@@ -496,16 +498,11 @@ export function unmountComponentAtNode(container: Container): boolean {
498 }
499 }
500
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);
506 return true;
507 } else {
508 if (__DEV__) {
packages/react-dom/src/shared/ReactDOMFlushSync.js new
+67
@@ -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
@@ -82,6 +82,7 @@ export type PreinitModuleScriptOptions = {
82 };
83
84 export type HostDispatcher = {
85 + flushSyncWork: () => boolean | void,
86 prefetchDNS: (href: string) => void,
87 preconnect: (href: string, crossOrigin?: ?CrossOriginEnum) => void,
88 preload: (href: string, as: string, options?: ?PreloadImplOptions) => void,
packages/react-noop-renderer/src/createReactNoop.js
+24 -1
@@ -29,6 +29,7 @@ import isArray from 'shared/isArray';
29 import {checkPropStringCoercion} from 'shared/CheckStringCoercion';
30 import {
31 NoEventPriority,
32 + DiscreteEventPriority,
33 DefaultEventPriority,
34 IdleEventPriority,
35 ConcurrentRoot,
@@ -40,6 +41,9 @@ import {
41 disableStringRefs,
42 } from 'shared/ReactFeatureFlags';
43
44 +import ReactSharedInternals from 'shared/ReactSharedInternals';
45 +const ReactCurrentBatchConfig = ReactSharedInternals.ReactCurrentBatchConfig;
46 +
47 type Container = {
48 rootID: string,
49 children: Array<Instance | TextInstance>,
@@ -943,7 +947,25 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
947 );
948 }
949 }
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 + }
969 }
970
971 function onRecoverableError(error) {
@@ -1081,6 +1103,7 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
1103 getChildrenAsJSX() {
1104 return getChildrenAsJSX(container);
1105 },
1106 + legacy: true,
1107 };
1108 },
1109
packages/react-reconciler/src/ReactFiberHotReloading.js
+10 -13
@@ -15,12 +15,12 @@ import type {Instance} from './ReactFiberConfig';
15 import type {ReactNodeList} from 'shared/ReactTypes';
16
17 import {
18 - flushSync,
18 + flushSyncWork,
19 scheduleUpdateOnFiber,
20 flushPassiveEffects,
21 } from './ReactFiberWorkLoop';
22 import {enqueueConcurrentRenderForLane} from './ReactFiberConcurrentUpdates';
23 -import {updateContainer} from './ReactFiberReconciler';
23 +import {updateContainerSync} from './ReactFiberReconciler';
24 import {emptyContextObject} from './ReactFiberContext';
25 import {SyncLane} from './ReactFiberLane';
26 import {
@@ -241,13 +241,12 @@ export const scheduleRefresh: ScheduleRefresh = (
241 }
242 const {staleFamilies, updatedFamilies} = update;
243 flushPassiveEffects();
244 - flushSync(() => {
245 - scheduleFibersWithFamiliesRecursively(
246 - root.current,
247 - updatedFamilies,
248 - staleFamilies,
249 - );
250 - });
244 + scheduleFibersWithFamiliesRecursively(
245 + root.current,
246 + updatedFamilies,
247 + staleFamilies,
248 + );
249 + flushSyncWork();
250 }
251 };
252
@@ -262,10 +261,8 @@ export const scheduleRoot: ScheduleRoot = (
261 // Just ignore. We'll delete this with _renderSubtree code path later.
262 return;
263 }
265 - flushPassiveEffects();
266 - flushSync(() => {
267 - updateContainer(element, root, null, null);
268 - });
264 + updateContainerSync(element, root, null, null);
265 + flushSyncWork();
266 }
267 };
268
packages/react-reconciler/src/ReactFiberReconciler.js
+55 -15
@@ -25,6 +25,7 @@ import type {ReactNodeList, ReactFormState} from 'shared/ReactTypes';
25 import type {Lane} from './ReactFiberLane';
26 import type {SuspenseState} from './ReactFiberSuspenseComponent';
27
28 +import {LegacyRoot} from './ReactRootTags';
29 import {
30 findCurrentHostFiber,
31 findCurrentHostFiberWithNoPortals,
@@ -61,7 +62,8 @@ import {
62 scheduleInitialHydrationOnRoot,
63 flushRoot,
64 batchedUpdates,
64 - flushSync,
65 + flushSyncFromReconciler,
66 + flushSyncWork,
67 isAlreadyRendering,
68 deferredUpdates,
69 discreteUpdates,
@@ -357,11 +359,51 @@ export function updateContainer(
359 parentComponent: ?React$Component<any, any>,
360 callback: ?Function,
361 ): Lane {
362 + const current = container.current;
363 + const lane = requestUpdateLane(current);
364 + updateContainerImpl(
365 + current,
366 + lane,
367 + element,
368 + container,
369 + parentComponent,
370 + callback,
371 + );
372 + return lane;
373 +}
374 +
375 +export function updateContainerSync(
376 + element: ReactNodeList,
377 + container: OpaqueRoot,
378 + parentComponent: ?React$Component<any, any>,
379 + callback: ?Function,
380 +): Lane {
381 + if (container.tag === LegacyRoot) {
382 + flushPassiveEffects();
383 + }
384 + const current = container.current;
385 + updateContainerImpl(
386 + current,
387 + SyncLane,
388 + element,
389 + container,
390 + parentComponent,
391 + callback,
392 + );
393 + return SyncLane;
394 +}
395 +
396 +function updateContainerImpl(
397 + rootFiber: Fiber,
398 + lane: Lane,
399 + element: ReactNodeList,
400 + container: OpaqueRoot,
401 + parentComponent: ?React$Component<any, any>,
402 + callback: ?Function,
403 +): void {
404 if (__DEV__) {
405 onScheduleRoot(container, element);
406 }
363 - const current = container.current;
364 - const lane = requestUpdateLane(current);
407
408 if (enableSchedulingProfiler) {
409 markRenderScheduled(lane);
@@ -410,20 +452,19 @@ export function updateContainer(
452 update.callback = callback;
453 }
454
413 - const root = enqueueUpdate(current, update, lane);
455 + const root = enqueueUpdate(rootFiber, update, lane);
456 if (root !== null) {
415 - scheduleUpdateOnFiber(root, current, lane);
416 - entangleTransitions(root, current, lane);
457 + scheduleUpdateOnFiber(root, rootFiber, lane);
458 + entangleTransitions(root, rootFiber, lane);
459 }
418 -
419 - return lane;
460 }
461
462 export {
463 batchedUpdates,
464 deferredUpdates,
465 discreteUpdates,
426 - flushSync,
466 + flushSyncFromReconciler,
467 + flushSyncWork,
468 isAlreadyRendering,
469 flushPassiveEffects,
470 };
@@ -456,12 +497,11 @@ export function attemptSynchronousHydration(fiber: Fiber): void {
497 break;
498 }
499 case SuspenseComponent: {
459 - flushSync(() => {
460 - const root = enqueueConcurrentRenderForLane(fiber, SyncLane);
461 - if (root !== null) {
462 - scheduleUpdateOnFiber(root, fiber, SyncLane);
463 - }
464 - });
500 + const root = enqueueConcurrentRenderForLane(fiber, SyncLane);
501 + if (root !== null) {
502 + scheduleUpdateOnFiber(root, fiber, SyncLane);
503 + }
504 + flushSyncWork();
505 // If we're still blocked after this, we need to increase
506 // the priority of any promises resolving within this
507 // boundary so that they next attempt also has higher pri.
packages/react-reconciler/src/ReactFiberWorkLoop.js
+25 -14
@@ -9,6 +9,7 @@
9
10 import {REACT_STRICT_MODE_TYPE} from 'shared/ReactSymbols';
11
12 +import type {BatchConfig} from 'react/src/ReactCurrentBatchConfig';
13 import type {Wakeable, Thenable} from 'shared/ReactTypes';
14 import type {Fiber, FiberRoot} from './ReactInternalTypes';
15 import type {Lanes, Lane} from './ReactFiberLane';
@@ -281,13 +282,12 @@ import {logUncaughtError} from './ReactFiberErrorLogger';
282
283 const PossiblyWeakMap = typeof WeakMap === 'function' ? WeakMap : Map;
284
284 -const {
285 - ReactCurrentDispatcher,
286 - ReactCurrentCache,
287 - ReactCurrentOwner,
288 - ReactCurrentBatchConfig,
289 - ReactCurrentActQueue,
290 -} = ReactSharedInternals;
285 +const ReactCurrentDispatcher = ReactSharedInternals.ReactCurrentDispatcher;
286 +const ReactCurrentCache = ReactSharedInternals.ReactCurrentCache;
287 +const ReactCurrentOwner = ReactSharedInternals.ReactCurrentOwner;
288 +const ReactCurrentBatchConfig: BatchConfig =
289 + ReactSharedInternals.ReactCurrentBatchConfig;
290 +const ReactCurrentActQueue = ReactSharedInternals.ReactCurrentActQueue;
291
292 type ExecutionContext = number;
293
@@ -625,12 +625,11 @@ export function requestUpdateLane(fiber: Fiber): Lane {
625 const transition = requestCurrentTransition();
626 if (transition !== null) {
627 if (__DEV__) {
628 - const batchConfigTransition = ReactCurrentBatchConfig.transition;
629 - if (!batchConfigTransition._updatedFibers) {
630 - batchConfigTransition._updatedFibers = new Set();
628 + if (!transition._updatedFibers) {
629 + transition._updatedFibers = new Set();
630 }
631
633 - batchConfigTransition._updatedFibers.add(fiber);
632 + transition._updatedFibers.add(fiber);
633 }
634
635 const actionScopeLane = peekEntangledActionLane();
@@ -776,6 +775,8 @@ export function scheduleUpdateOnFiber(
775 transition.startTime = now();
776 }
777
778 + // $FlowFixMe[prop-missing]: The BatchConfigTransition and Transition types are incompatible but was previously untyped and thus uncaught
779 + // $FlowFixMe[incompatible-call]: "
780 addTransitionToLanesMap(root, transition, lane);
781 }
782 }
@@ -1494,11 +1495,11 @@ export function discreteUpdates<A, B, C, D, R>(
1495 // Overload the definition to the two valid signatures.
1496 // Warning, this opts-out of checking the function body.
1497 // eslint-disable-next-line no-unused-vars
1497 -declare function flushSync<R>(fn: () => R): R;
1498 +declare function flushSyncFromReconciler<R>(fn: () => R): R;
1499 // eslint-disable-next-line no-redeclare
1499 -declare function flushSync(void): void;
1500 +declare function flushSyncFromReconciler(void): void;
1501 // eslint-disable-next-line no-redeclare
1501 -export function flushSync<R>(fn: (() => R) | void): R | void {
1502 +export function flushSyncFromReconciler<R>(fn: (() => R) | void): R | void {
1503 // In legacy mode, we flush pending passive effects at the beginning of the
1504 // next event, not at the end of the previous one.
1505 if (
@@ -1538,6 +1539,16 @@ export function flushSync<R>(fn: (() => R) | void): R | void {
1539 }
1540 }
1541
1542 +// If called outside of a render or commit will flush all sync work on all roots
1543 +// Returns whether the the call was during a render or not
1544 +export function flushSyncWork(): boolean {
1545 + if ((executionContext & (RenderContext | CommitContext)) === NoContext) {
1546 + flushSyncWorkOnAllRoots();
1547 + return false;
1548 + }
1549 + return true;
1550 +}
1551 +
1552 export function isAlreadyRendering(): boolean {
1553 // Used by the renderer to print a warning if certain APIs are called from
1554 // the wrong context.
packages/react-test-renderer/src/ReactTestRenderer.js
+3 -3
@@ -20,7 +20,7 @@ import {
20 getPublicRootInstance,
21 createContainer,
22 updateContainer,
23 - flushSync,
23 + flushSyncFromReconciler,
24 injectIntoDevTools,
25 batchedUpdates,
26 defaultOnUncaughtError,
@@ -468,7 +468,7 @@ function create(
468 update(newElement: React$Element<any>): any,
469 unmount(): void,
470 getInstance(): React$Component<any, any> | PublicInstance | null,
471 - unstable_flushSync: typeof flushSync,
471 + unstable_flushSync: typeof flushSyncFromReconciler,
472 } {
473 if (__DEV__) {
474 if (
@@ -597,7 +597,7 @@ function create(
597 return getPublicRootInstance(root);
598 },
599
600 - unstable_flushSync: flushSync,
600 + unstable_flushSync: flushSyncFromReconciler,
601 };
602
603 Object.defineProperty(
packages/react/src/ReactCurrentBatchConfig.js
+1 -1
@@ -9,7 +9,7 @@
9
10 import type {BatchConfigTransition} from 'react-reconciler/src/ReactFiberTracingMarkerComponent';
11
12 -type BatchConfig = {
12 +export type BatchConfig = {
13 transition: BatchConfigTransition | null,
14 };
15 /**
scripts/error-codes/codes.json
+2 -1
@@ -505,5 +505,6 @@
505 "517": "Symbols cannot be passed to a Server Function without a temporary reference set. Pass a TemporaryReferenceSet to the options.%s",
506 "518": "Saw multiple hydration diff roots in a pass. This is a bug in React.",
507 "519": "Hydration Mismatch Exception: This is not a real error, and should not leak into userspace. If you're seeing this, it's likely a bug in React.",
508 - "520": "There was an error during concurrent rendering but React was able to recover by instead synchronously rendering the entire root."
508 + "520": "There was an error during concurrent rendering but React was able to recover by instead synchronously rendering the entire root.",
509 + "521": "flushSyncWork should not be called from builds that support legacy mode. This is a bug in React."
510 }