From 4c12339ce3fa398050d1026c616ea43d43dcaf4a Mon Sep 17 00:00:00 2001 From: Josh Story Date: Mon, 8 Apr 2024 09:03:20 -0700 Subject: [PATCH] [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 --- packages/react-art/src/ReactART.js | 19 +++-- .../src/client/ReactFiberConfigDOM.js | 19 +++++ .../src/events/ReactDOMUpdateBatching.js | 6 +- .../ReactDOMFlightServerHostDispatcher.js | 1 + .../src/server/ReactFizzConfigDOM.js | 1 + .../react-dom/src/ReactDOMSharedInternals.js | 1 + packages/react-dom/src/client/ReactDOM.js | 14 ++-- packages/react-dom/src/client/ReactDOMRoot.js | 8 +-- .../react-dom/src/client/ReactDOMRootFB.js | 27 ++++--- .../react-dom/src/shared/ReactDOMFlushSync.js | 67 ++++++++++++++++++ .../react-dom/src/shared/ReactDOMTypes.js | 1 + .../src/createReactNoop.js | 25 ++++++- .../src/ReactFiberHotReloading.js | 23 +++--- .../src/ReactFiberReconciler.js | 70 +++++++++++++++---- .../src/ReactFiberWorkLoop.js | 39 +++++++---- .../src/ReactTestRenderer.js | 6 +- packages/react/src/ReactCurrentBatchConfig.js | 2 +- scripts/error-codes/codes.json | 3 +- 18 files changed, 248 insertions(+), 84 deletions(-) create mode 100644 packages/react-dom/src/shared/ReactDOMFlushSync.js diff --git a/packages/react-art/src/ReactART.js b/packages/react-art/src/ReactART.js index bf044d34b0441..4defd43e4868e 100644 --- a/packages/react-art/src/ReactART.js +++ b/packages/react-art/src/ReactART.js @@ -10,9 +10,9 @@ import ReactVersion from 'shared/ReactVersion'; import {LegacyRoot, ConcurrentRoot} from 'react-reconciler/src/ReactRootTags'; import { createContainer, - updateContainer, + updateContainerSync, injectIntoDevTools, - flushSync, + flushSyncWork, } from 'react-reconciler/src/ReactFiberReconciler'; import Transform from 'art/core/transform'; import Mode from 'art/modes/current'; @@ -78,9 +78,8 @@ class Surface extends React.Component { ); // We synchronously flush updates coming from above so that they commit together // and so that refs resolve before the parent life cycles. - flushSync(() => { - updateContainer(this.props.children, this._mountNode, this); - }); + updateContainerSync(this.props.children, this._mountNode, this); + flushSyncWork(); } componentDidUpdate(prevProps, prevState) { @@ -92,9 +91,8 @@ class Surface extends React.Component { // We synchronously flush updates coming from above so that they commit together // and so that refs resolve before the parent life cycles. - flushSync(() => { - updateContainer(this.props.children, this._mountNode, this); - }); + updateContainerSync(this.props.children, this._mountNode, this); + flushSyncWork(); if (this._surface.render) { this._surface.render(); @@ -104,9 +102,8 @@ class Surface extends React.Component { componentWillUnmount() { // We synchronously flush updates coming from above so that they commit together // and so that refs resolve before the parent life cycles. - flushSync(() => { - updateContainer(null, this._mountNode, this); - }); + updateContainerSync(null, this._mountNode, this); + flushSyncWork(); } render() { diff --git a/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js b/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js index 504172309f9c5..dd089a17fdd11 100644 --- a/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js +++ b/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js @@ -90,6 +90,7 @@ import { enableScopeAPI, enableTrustedTypesIntegration, enableAsyncActions, + disableLegacyMode, } from 'shared/ReactFeatureFlags'; import { HostComponent, @@ -100,6 +101,7 @@ import { import {listenToAllSupportedEvents} from '../events/DOMPluginEventSystem'; import {validateLinkPropsForStyleResource} from '../shared/ReactDOMResourceValidation'; import escapeSelectorAttributeValueInsideDoubleQuotes from './escapeSelectorAttributeValueInsideDoubleQuotes'; +import {flushSyncWork as flushSyncWorkOnAllRoots} from 'react-reconciler/src/ReactFiberWorkLoop'; import ReactDOMSharedInternals from 'shared/ReactDOMSharedInternals'; const ReactDOMCurrentDispatcher = @@ -1924,6 +1926,9 @@ function getDocumentFromRoot(root: HoistableRoot): Document { const previousDispatcher = ReactDOMCurrentDispatcher.current; ReactDOMCurrentDispatcher.current = { + flushSyncWork: disableLegacyMode + ? flushSyncWork + : previousDispatcher.flushSyncWork, prefetchDNS, preconnect, preload, @@ -1933,6 +1938,20 @@ ReactDOMCurrentDispatcher.current = { preinitModuleScript, }; +function flushSyncWork() { + if (disableLegacyMode) { + const previousWasRendering = previousDispatcher.flushSyncWork(); + const wasRendering = flushSyncWorkOnAllRoots(); + // Since multiple dispatchers can flush sync work during a single flushSync call + // we need to return true if any of them were rendering. + return previousWasRendering || wasRendering; + } else { + throw new Error( + 'flushSyncWork should not be called from builds that support legacy mode. This is a bug in React.', + ); + } +} + // We expect this to get inlined. It is a function mostly to communicate the special nature of // how we resolve the HoistableRoot for ReactDOM.pre*() methods. Because we support calling // these methods outside of render there is no way to know which Document or ShadowRoot is 'scoped' diff --git a/packages/react-dom-bindings/src/events/ReactDOMUpdateBatching.js b/packages/react-dom-bindings/src/events/ReactDOMUpdateBatching.js index 0e80a3e7504b6..a2995577491e7 100644 --- a/packages/react-dom-bindings/src/events/ReactDOMUpdateBatching.js +++ b/packages/react-dom-bindings/src/events/ReactDOMUpdateBatching.js @@ -13,7 +13,7 @@ import { import { batchedUpdates as batchedUpdatesImpl, discreteUpdates as discreteUpdatesImpl, - flushSync as flushSyncImpl, + flushSyncWork, } from 'react-reconciler/src/ReactFiberReconciler'; // Used as a way to call batchedUpdates when we don't have a reference to @@ -36,7 +36,9 @@ function finishEventHandler() { // bails out of the update without touching the DOM. // TODO: Restore state in the microtask, after the discrete updates flush, // instead of early flushing them here. - flushSyncImpl(); + // @TODO Should move to flushSyncWork once legacy mode is removed but since this flushSync + // flushes passive effects we can't do this yet. + flushSyncWork(); restoreStateIfNeeded(); } } diff --git a/packages/react-dom-bindings/src/server/ReactDOMFlightServerHostDispatcher.js b/packages/react-dom-bindings/src/server/ReactDOMFlightServerHostDispatcher.js index 64efccddedefa..be58583396d1a 100644 --- a/packages/react-dom-bindings/src/server/ReactDOMFlightServerHostDispatcher.js +++ b/packages/react-dom-bindings/src/server/ReactDOMFlightServerHostDispatcher.js @@ -28,6 +28,7 @@ const ReactDOMCurrentDispatcher = const previousDispatcher = ReactDOMCurrentDispatcher.current; ReactDOMCurrentDispatcher.current = { + flushSyncWork: previousDispatcher.flushSyncWork, prefetchDNS, preconnect, preload, diff --git a/packages/react-dom-bindings/src/server/ReactFizzConfigDOM.js b/packages/react-dom-bindings/src/server/ReactFizzConfigDOM.js index 475e934a60491..92f3756ab800d 100644 --- a/packages/react-dom-bindings/src/server/ReactFizzConfigDOM.js +++ b/packages/react-dom-bindings/src/server/ReactFizzConfigDOM.js @@ -88,6 +88,7 @@ const ReactDOMCurrentDispatcher = const previousDispatcher = ReactDOMCurrentDispatcher.current; ReactDOMCurrentDispatcher.current = { + flushSyncWork: previousDispatcher.flushSyncWork, prefetchDNS, preconnect, preload, diff --git a/packages/react-dom/src/ReactDOMSharedInternals.js b/packages/react-dom/src/ReactDOMSharedInternals.js index f2dd23c26660f..39faf9464a57c 100644 --- a/packages/react-dom/src/ReactDOMSharedInternals.js +++ b/packages/react-dom/src/ReactDOMSharedInternals.js @@ -29,6 +29,7 @@ type InternalsType = { function noop() {} const DefaultDispatcher: HostDispatcher = { + flushSyncWork: noop, prefetchDNS: noop, preconnect: noop, preload: noop, diff --git a/packages/react-dom/src/client/ReactDOM.js b/packages/react-dom/src/client/ReactDOM.js index 04b59d618aeb1..b294418f7a511 100644 --- a/packages/react-dom/src/client/ReactDOM.js +++ b/packages/react-dom/src/client/ReactDOM.js @@ -14,6 +14,7 @@ import type { CreateRootOptions, } from './ReactDOMRoot'; +import {disableLegacyMode} from 'shared/ReactFeatureFlags'; import { createRoot as createRootImpl, hydrateRoot as hydrateRootImpl, @@ -21,9 +22,10 @@ import { } from './ReactDOMRoot'; import {createEventHandle} from 'react-dom-bindings/src/client/ReactDOMEventHandle'; import {runWithPriority} from 'react-dom-bindings/src/client/ReactDOMUpdatePriority'; +import {flushSync as flushSyncIsomorphic} from '../shared/ReactDOMFlushSync'; import { - flushSync as flushSyncWithoutWarningIfAlreadyRendering, + flushSyncFromReconciler as flushSyncWithoutWarningIfAlreadyRendering, isAlreadyRendering, injectIntoDevTools, findHostInstance, @@ -123,11 +125,11 @@ function hydrateRoot( // Overload the definition to the two valid signatures. // Warning, this opts-out of checking the function body. -declare function flushSync(fn: () => R): R; +declare function flushSyncFromReconciler(fn: () => R): R; // eslint-disable-next-line no-redeclare -declare function flushSync(): void; +declare function flushSyncFromReconciler(): void; // eslint-disable-next-line no-redeclare -function flushSync(fn: (() => R) | void): R | void { +function flushSyncFromReconciler(fn: (() => R) | void): R | void { if (__DEV__) { if (isAlreadyRendering()) { console.error( @@ -140,6 +142,10 @@ function flushSync(fn: (() => R) | void): R | void { return flushSyncWithoutWarningIfAlreadyRendering(fn); } +const flushSync: typeof flushSyncIsomorphic = disableLegacyMode + ? flushSyncIsomorphic + : flushSyncFromReconciler; + function findDOMNode( componentOrElement: React$Component, ): null | Element | Text { diff --git a/packages/react-dom/src/client/ReactDOMRoot.js b/packages/react-dom/src/client/ReactDOMRoot.js index 9d24b0ba9ef51..ef573f5f790cd 100644 --- a/packages/react-dom/src/client/ReactDOMRoot.js +++ b/packages/react-dom/src/client/ReactDOMRoot.js @@ -93,7 +93,8 @@ import { createContainer, createHydrationContainer, updateContainer, - flushSync, + updateContainerSync, + flushSyncWork, isAlreadyRendering, defaultOnUncaughtError, defaultOnCaughtError, @@ -161,9 +162,8 @@ ReactDOMHydrationRoot.prototype.unmount = ReactDOMRoot.prototype.unmount = ); } } - flushSync(() => { - updateContainer(null, root, null, null); - }); + updateContainerSync(null, root, null, null); + flushSyncWork(); unmarkContainerAsRoot(container); } }; diff --git a/packages/react-dom/src/client/ReactDOMRootFB.js b/packages/react-dom/src/client/ReactDOMRootFB.js index 4bf5b43f6e51d..2d6f5187be229 100644 --- a/packages/react-dom/src/client/ReactDOMRootFB.js +++ b/packages/react-dom/src/client/ReactDOMRootFB.js @@ -49,7 +49,8 @@ import { createHydrationContainer, findHostInstanceWithNoPortals, updateContainer, - flushSync, + updateContainerSync, + flushSyncWork, getPublicRootInstance, findHostInstance, findHostInstanceWithWarning, @@ -247,7 +248,7 @@ function legacyCreateRootFromDOMContainer( // $FlowFixMe[incompatible-call] listenToAllSupportedEvents(rootContainerElement); - flushSync(); + flushSyncWork(); return root; } else { // First clear any existing content. @@ -282,9 +283,8 @@ function legacyCreateRootFromDOMContainer( listenToAllSupportedEvents(rootContainerElement); // Initial mount should not be batched. - flushSync(() => { - updateContainer(initialChildren, root, parentComponent, callback); - }); + updateContainerSync(initialChildren, root, parentComponent, callback); + flushSyncWork(); return root; } @@ -485,6 +485,8 @@ export function unmountComponentAtNode(container: Container): boolean { } if (container._reactRootContainer) { + const root = container._reactRootContainer; + if (__DEV__) { const rootEl = getReactRootElementInContainer(container); const renderedByDifferentReact = rootEl && !getInstanceFromNode(rootEl); @@ -496,16 +498,11 @@ export function unmountComponentAtNode(container: Container): boolean { } } - // Unmount should not be batched. - flushSync(() => { - legacyRenderSubtreeIntoContainer(null, null, container, false, () => { - // $FlowFixMe[incompatible-type] This should probably use `delete container._reactRootContainer` - container._reactRootContainer = null; - unmarkContainerAsRoot(container); - }); - }); - // If you call unmountComponentAtNode twice in quick succession, you'll - // get `true` twice. That's probably fine? + updateContainerSync(null, root, null, null); + flushSyncWork(); + // $FlowFixMe[incompatible-type] This should probably use `delete container._reactRootContainer` + container._reactRootContainer = null; + unmarkContainerAsRoot(container); return true; } else { if (__DEV__) { diff --git a/packages/react-dom/src/shared/ReactDOMFlushSync.js b/packages/react-dom/src/shared/ReactDOMFlushSync.js new file mode 100644 index 0000000000000..a746a9090f60b --- /dev/null +++ b/packages/react-dom/src/shared/ReactDOMFlushSync.js @@ -0,0 +1,67 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +import type {BatchConfig} from 'react/src/ReactCurrentBatchConfig'; + +import {disableLegacyMode} from 'shared/ReactFeatureFlags'; +import {DiscreteEventPriority} from 'react-reconciler/src/ReactEventPriorities'; + +import ReactSharedInternals from 'shared/ReactSharedInternals'; +const ReactCurrentBatchConfig: BatchConfig = + ReactSharedInternals.ReactCurrentBatchConfig; + +import ReactDOMSharedInternals from 'shared/ReactDOMSharedInternals'; +const ReactDOMCurrentDispatcher = + ReactDOMSharedInternals.ReactDOMCurrentDispatcher; + +declare function flushSyncImpl(fn: () => R): R; +declare function flushSyncImpl(void): void; +function flushSyncImpl(fn: (() => R) | void): R | void { + const previousTransition = ReactCurrentBatchConfig.transition; + const previousUpdatePriority = + ReactDOMSharedInternals.up; /* ReactDOMCurrentUpdatePriority */ + + try { + ReactCurrentBatchConfig.transition = null; + ReactDOMSharedInternals.up /* ReactDOMCurrentUpdatePriority */ = + DiscreteEventPriority; + if (fn) { + return fn(); + } else { + return undefined; + } + } finally { + ReactCurrentBatchConfig.transition = previousTransition; + ReactDOMSharedInternals.up /* ReactDOMCurrentUpdatePriority */ = + previousUpdatePriority; + const wasInRender = ReactDOMCurrentDispatcher.current.flushSyncWork(); + if (__DEV__) { + if (wasInRender) { + console.error( + 'flushSync was called from inside a lifecycle method. React cannot ' + + 'flush when React is already rendering. Consider moving this call to ' + + 'a scheduler task or micro task.', + ); + } + } + } +} + +declare function flushSyncErrorInBuildsThatSupportLegacyMode(fn: () => R): R; +declare function flushSyncErrorInBuildsThatSupportLegacyMode(void): void; +function flushSyncErrorInBuildsThatSupportLegacyMode() { + // eslint-disable-next-line react-internal/prod-error-codes + throw new Error( + 'Expected this build of React to not support legacy mode but it does. This is a bug in React.', + ); +} + +export const flushSync: typeof flushSyncImpl = disableLegacyMode + ? flushSyncImpl + : flushSyncErrorInBuildsThatSupportLegacyMode; diff --git a/packages/react-dom/src/shared/ReactDOMTypes.js b/packages/react-dom/src/shared/ReactDOMTypes.js index 0e6d76c6e919a..515825cad29be 100644 --- a/packages/react-dom/src/shared/ReactDOMTypes.js +++ b/packages/react-dom/src/shared/ReactDOMTypes.js @@ -82,6 +82,7 @@ export type PreinitModuleScriptOptions = { }; export type HostDispatcher = { + flushSyncWork: () => boolean | void, prefetchDNS: (href: string) => void, preconnect: (href: string, crossOrigin?: ?CrossOriginEnum) => void, preload: (href: string, as: string, options?: ?PreloadImplOptions) => void, diff --git a/packages/react-noop-renderer/src/createReactNoop.js b/packages/react-noop-renderer/src/createReactNoop.js index 86c04f961d788..9bb0a5fc1ca26 100644 --- a/packages/react-noop-renderer/src/createReactNoop.js +++ b/packages/react-noop-renderer/src/createReactNoop.js @@ -29,6 +29,7 @@ import isArray from 'shared/isArray'; import {checkPropStringCoercion} from 'shared/CheckStringCoercion'; import { NoEventPriority, + DiscreteEventPriority, DefaultEventPriority, IdleEventPriority, ConcurrentRoot, @@ -40,6 +41,9 @@ import { disableStringRefs, } from 'shared/ReactFeatureFlags'; +import ReactSharedInternals from 'shared/ReactSharedInternals'; +const ReactCurrentBatchConfig = ReactSharedInternals.ReactCurrentBatchConfig; + type Container = { rootID: string, children: Array, @@ -943,7 +947,25 @@ function createReactNoop(reconciler: Function, useMutation: boolean) { ); } } - return NoopRenderer.flushSync(fn); + if (disableLegacyMode) { + const previousTransition = ReactCurrentBatchConfig.transition; + const preivousEventPriority = currentEventPriority; + try { + ReactCurrentBatchConfig.transition = null; + currentEventPriority = DiscreteEventPriority; + if (fn) { + return fn(); + } else { + return undefined; + } + } finally { + ReactCurrentBatchConfig.transition = previousTransition; + currentEventPriority = preivousEventPriority; + NoopRenderer.flushSyncWork(); + } + } else { + return NoopRenderer.flushSyncFromReconciler(fn); + } } function onRecoverableError(error) { @@ -1081,6 +1103,7 @@ function createReactNoop(reconciler: Function, useMutation: boolean) { getChildrenAsJSX() { return getChildrenAsJSX(container); }, + legacy: true, }; }, diff --git a/packages/react-reconciler/src/ReactFiberHotReloading.js b/packages/react-reconciler/src/ReactFiberHotReloading.js index 6f58d5608b8cf..538dfe6449721 100644 --- a/packages/react-reconciler/src/ReactFiberHotReloading.js +++ b/packages/react-reconciler/src/ReactFiberHotReloading.js @@ -15,12 +15,12 @@ import type {Instance} from './ReactFiberConfig'; import type {ReactNodeList} from 'shared/ReactTypes'; import { - flushSync, + flushSyncWork, scheduleUpdateOnFiber, flushPassiveEffects, } from './ReactFiberWorkLoop'; import {enqueueConcurrentRenderForLane} from './ReactFiberConcurrentUpdates'; -import {updateContainer} from './ReactFiberReconciler'; +import {updateContainerSync} from './ReactFiberReconciler'; import {emptyContextObject} from './ReactFiberContext'; import {SyncLane} from './ReactFiberLane'; import { @@ -241,13 +241,12 @@ export const scheduleRefresh: ScheduleRefresh = ( } const {staleFamilies, updatedFamilies} = update; flushPassiveEffects(); - flushSync(() => { - scheduleFibersWithFamiliesRecursively( - root.current, - updatedFamilies, - staleFamilies, - ); - }); + scheduleFibersWithFamiliesRecursively( + root.current, + updatedFamilies, + staleFamilies, + ); + flushSyncWork(); } }; @@ -262,10 +261,8 @@ export const scheduleRoot: ScheduleRoot = ( // Just ignore. We'll delete this with _renderSubtree code path later. return; } - flushPassiveEffects(); - flushSync(() => { - updateContainer(element, root, null, null); - }); + updateContainerSync(element, root, null, null); + flushSyncWork(); } }; diff --git a/packages/react-reconciler/src/ReactFiberReconciler.js b/packages/react-reconciler/src/ReactFiberReconciler.js index 06100010d0ee3..fc2eb5bef39e0 100644 --- a/packages/react-reconciler/src/ReactFiberReconciler.js +++ b/packages/react-reconciler/src/ReactFiberReconciler.js @@ -25,6 +25,7 @@ import type {ReactNodeList, ReactFormState} from 'shared/ReactTypes'; import type {Lane} from './ReactFiberLane'; import type {SuspenseState} from './ReactFiberSuspenseComponent'; +import {LegacyRoot} from './ReactRootTags'; import { findCurrentHostFiber, findCurrentHostFiberWithNoPortals, @@ -61,7 +62,8 @@ import { scheduleInitialHydrationOnRoot, flushRoot, batchedUpdates, - flushSync, + flushSyncFromReconciler, + flushSyncWork, isAlreadyRendering, deferredUpdates, discreteUpdates, @@ -357,11 +359,51 @@ export function updateContainer( parentComponent: ?React$Component, callback: ?Function, ): Lane { + const current = container.current; + const lane = requestUpdateLane(current); + updateContainerImpl( + current, + lane, + element, + container, + parentComponent, + callback, + ); + return lane; +} + +export function updateContainerSync( + element: ReactNodeList, + container: OpaqueRoot, + parentComponent: ?React$Component, + callback: ?Function, +): Lane { + if (container.tag === LegacyRoot) { + flushPassiveEffects(); + } + const current = container.current; + updateContainerImpl( + current, + SyncLane, + element, + container, + parentComponent, + callback, + ); + return SyncLane; +} + +function updateContainerImpl( + rootFiber: Fiber, + lane: Lane, + element: ReactNodeList, + container: OpaqueRoot, + parentComponent: ?React$Component, + callback: ?Function, +): void { if (__DEV__) { onScheduleRoot(container, element); } - const current = container.current; - const lane = requestUpdateLane(current); if (enableSchedulingProfiler) { markRenderScheduled(lane); @@ -410,20 +452,19 @@ export function updateContainer( update.callback = callback; } - const root = enqueueUpdate(current, update, lane); + const root = enqueueUpdate(rootFiber, update, lane); if (root !== null) { - scheduleUpdateOnFiber(root, current, lane); - entangleTransitions(root, current, lane); + scheduleUpdateOnFiber(root, rootFiber, lane); + entangleTransitions(root, rootFiber, lane); } - - return lane; } export { batchedUpdates, deferredUpdates, discreteUpdates, - flushSync, + flushSyncFromReconciler, + flushSyncWork, isAlreadyRendering, flushPassiveEffects, }; @@ -456,12 +497,11 @@ export function attemptSynchronousHydration(fiber: Fiber): void { break; } case SuspenseComponent: { - flushSync(() => { - const root = enqueueConcurrentRenderForLane(fiber, SyncLane); - if (root !== null) { - scheduleUpdateOnFiber(root, fiber, SyncLane); - } - }); + const root = enqueueConcurrentRenderForLane(fiber, SyncLane); + if (root !== null) { + scheduleUpdateOnFiber(root, fiber, SyncLane); + } + flushSyncWork(); // If we're still blocked after this, we need to increase // the priority of any promises resolving within this // boundary so that they next attempt also has higher pri. diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.js b/packages/react-reconciler/src/ReactFiberWorkLoop.js index 626d5a606fe16..e3f1396bc7f1d 100644 --- a/packages/react-reconciler/src/ReactFiberWorkLoop.js +++ b/packages/react-reconciler/src/ReactFiberWorkLoop.js @@ -9,6 +9,7 @@ import {REACT_STRICT_MODE_TYPE} from 'shared/ReactSymbols'; +import type {BatchConfig} from 'react/src/ReactCurrentBatchConfig'; import type {Wakeable, Thenable} from 'shared/ReactTypes'; import type {Fiber, FiberRoot} from './ReactInternalTypes'; import type {Lanes, Lane} from './ReactFiberLane'; @@ -281,13 +282,12 @@ import {logUncaughtError} from './ReactFiberErrorLogger'; const PossiblyWeakMap = typeof WeakMap === 'function' ? WeakMap : Map; -const { - ReactCurrentDispatcher, - ReactCurrentCache, - ReactCurrentOwner, - ReactCurrentBatchConfig, - ReactCurrentActQueue, -} = ReactSharedInternals; +const ReactCurrentDispatcher = ReactSharedInternals.ReactCurrentDispatcher; +const ReactCurrentCache = ReactSharedInternals.ReactCurrentCache; +const ReactCurrentOwner = ReactSharedInternals.ReactCurrentOwner; +const ReactCurrentBatchConfig: BatchConfig = + ReactSharedInternals.ReactCurrentBatchConfig; +const ReactCurrentActQueue = ReactSharedInternals.ReactCurrentActQueue; type ExecutionContext = number; @@ -625,12 +625,11 @@ export function requestUpdateLane(fiber: Fiber): Lane { const transition = requestCurrentTransition(); if (transition !== null) { if (__DEV__) { - const batchConfigTransition = ReactCurrentBatchConfig.transition; - if (!batchConfigTransition._updatedFibers) { - batchConfigTransition._updatedFibers = new Set(); + if (!transition._updatedFibers) { + transition._updatedFibers = new Set(); } - batchConfigTransition._updatedFibers.add(fiber); + transition._updatedFibers.add(fiber); } const actionScopeLane = peekEntangledActionLane(); @@ -776,6 +775,8 @@ export function scheduleUpdateOnFiber( transition.startTime = now(); } + // $FlowFixMe[prop-missing]: The BatchConfigTransition and Transition types are incompatible but was previously untyped and thus uncaught + // $FlowFixMe[incompatible-call]: " addTransitionToLanesMap(root, transition, lane); } } @@ -1494,11 +1495,11 @@ export function discreteUpdates( // Overload the definition to the two valid signatures. // Warning, this opts-out of checking the function body. // eslint-disable-next-line no-unused-vars -declare function flushSync(fn: () => R): R; +declare function flushSyncFromReconciler(fn: () => R): R; // eslint-disable-next-line no-redeclare -declare function flushSync(void): void; +declare function flushSyncFromReconciler(void): void; // eslint-disable-next-line no-redeclare -export function flushSync(fn: (() => R) | void): R | void { +export function flushSyncFromReconciler(fn: (() => R) | void): R | void { // In legacy mode, we flush pending passive effects at the beginning of the // next event, not at the end of the previous one. if ( @@ -1538,6 +1539,16 @@ export function flushSync(fn: (() => R) | void): R | void { } } +// If called outside of a render or commit will flush all sync work on all roots +// Returns whether the the call was during a render or not +export function flushSyncWork(): boolean { + if ((executionContext & (RenderContext | CommitContext)) === NoContext) { + flushSyncWorkOnAllRoots(); + return false; + } + return true; +} + export function isAlreadyRendering(): boolean { // Used by the renderer to print a warning if certain APIs are called from // the wrong context. diff --git a/packages/react-test-renderer/src/ReactTestRenderer.js b/packages/react-test-renderer/src/ReactTestRenderer.js index 7368eada24ad5..3d2f77a47e962 100644 --- a/packages/react-test-renderer/src/ReactTestRenderer.js +++ b/packages/react-test-renderer/src/ReactTestRenderer.js @@ -20,7 +20,7 @@ import { getPublicRootInstance, createContainer, updateContainer, - flushSync, + flushSyncFromReconciler, injectIntoDevTools, batchedUpdates, defaultOnUncaughtError, @@ -468,7 +468,7 @@ function create( update(newElement: React$Element): any, unmount(): void, getInstance(): React$Component | PublicInstance | null, - unstable_flushSync: typeof flushSync, + unstable_flushSync: typeof flushSyncFromReconciler, } { if (__DEV__) { if ( @@ -597,7 +597,7 @@ function create( return getPublicRootInstance(root); }, - unstable_flushSync: flushSync, + unstable_flushSync: flushSyncFromReconciler, }; Object.defineProperty( diff --git a/packages/react/src/ReactCurrentBatchConfig.js b/packages/react/src/ReactCurrentBatchConfig.js index 67d59a6a012d6..debd21e4fa200 100644 --- a/packages/react/src/ReactCurrentBatchConfig.js +++ b/packages/react/src/ReactCurrentBatchConfig.js @@ -9,7 +9,7 @@ import type {BatchConfigTransition} from 'react-reconciler/src/ReactFiberTracingMarkerComponent'; -type BatchConfig = { +export type BatchConfig = { transition: BatchConfigTransition | null, }; /** diff --git a/scripts/error-codes/codes.json b/scripts/error-codes/codes.json index b44eda5795c34..799ce858f3e19 100644 --- a/scripts/error-codes/codes.json +++ b/scripts/error-codes/codes.json @@ -505,5 +505,6 @@ "517": "Symbols cannot be passed to a Server Function without a temporary reference set. Pass a TemporaryReferenceSet to the options.%s", "518": "Saw multiple hydration diff roots in a pass. This is a bug in React.", "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.", - "520": "There was an error during concurrent rendering but React was able to recover by instead synchronously rendering the entire root." + "520": "There was an error during concurrent rendering but React was able to recover by instead synchronously rendering the entire root.", + "521": "flushSyncWork should not be called from builds that support legacy mode. This is a bug in React." }