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." }