diff --git a/packages/react-dom/index.classic.fb.js b/packages/react-dom/index.classic.fb.js index 33133eafbf316..5d4cb1e11f822 100644 --- a/packages/react-dom/index.classic.fb.js +++ b/packages/react-dom/index.classic.fb.js @@ -23,7 +23,6 @@ export { findDOMNode, flushSync, unmountComponentAtNode, - unstable_batchedUpdates, unstable_createEventHandle, unstable_renderSubtreeIntoContainer, unstable_runWithPriority, // DO NOT USE: Temporarily exposed to migrate off of Scheduler.runWithPriority. @@ -38,6 +37,11 @@ export { version, } from './src/client/ReactDOM'; -export {createRoot, hydrateRoot, render} from './src/client/ReactDOMRootFB'; +export { + createRoot, + hydrateRoot, + render, + unstable_batchedUpdates, +} from './src/client/ReactDOMRootFB'; export {Internals as __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED}; diff --git a/packages/react-dom/src/__tests__/ReactLegacyMount-test.js b/packages/react-dom/src/__tests__/ReactLegacyMount-test.js index 6835e9a018a8f..bd0f3442bc4cb 100644 --- a/packages/react-dom/src/__tests__/ReactLegacyMount-test.js +++ b/packages/react-dom/src/__tests__/ReactLegacyMount-test.js @@ -248,7 +248,7 @@ describe('ReactMount', () => { expect(calls).toBe(5); }); - // @gate !disableLegacyMode + // @gate !disableLegacyMode && classic it('initial mount of legacy root is sync inside batchedUpdates, as if it were wrapped in flushSync', () => { const container1 = document.createElement('div'); const container2 = document.createElement('div'); diff --git a/packages/react-dom/src/__tests__/ReactLegacyUpdates-test.js b/packages/react-dom/src/__tests__/ReactLegacyUpdates-test.js index 68d9edf05b6bd..942191372a3c0 100644 --- a/packages/react-dom/src/__tests__/ReactLegacyUpdates-test.js +++ b/packages/react-dom/src/__tests__/ReactLegacyUpdates-test.js @@ -32,7 +32,7 @@ describe('ReactLegacyUpdates', () => { assertLog = InternalTestUtils.assertLog; }); - // @gate !disableLegacyMode + // @gate !disableLegacyMode && classic it('should batch state when updating state twice', () => { let updateCount = 0; @@ -63,7 +63,7 @@ describe('ReactLegacyUpdates', () => { expect(updateCount).toBe(1); }); - // @gate !disableLegacyMode + // @gate !disableLegacyMode && classic it('should batch state when updating two different state keys', () => { let updateCount = 0; @@ -97,7 +97,7 @@ describe('ReactLegacyUpdates', () => { expect(updateCount).toBe(1); }); - // @gate !disableLegacyMode + // @gate !disableLegacyMode && classic it('should batch state and props together', () => { let updateCount = 0; @@ -131,7 +131,7 @@ describe('ReactLegacyUpdates', () => { expect(updateCount).toBe(1); }); - // @gate !disableLegacyMode + // @gate !disableLegacyMode && classic it('should batch parent/child state updates together', () => { let parentUpdateCount = 0; @@ -187,7 +187,7 @@ describe('ReactLegacyUpdates', () => { expect(childUpdateCount).toBe(1); }); - // @gate !disableLegacyMode + // @gate !disableLegacyMode && classic it('should batch child/parent state updates together', () => { let parentUpdateCount = 0; @@ -245,7 +245,7 @@ describe('ReactLegacyUpdates', () => { expect(childUpdateCount).toBe(1); }); - // @gate !disableLegacyMode + // @gate !disableLegacyMode && classic it('should support chained state updates', () => { let updateCount = 0; @@ -286,7 +286,7 @@ describe('ReactLegacyUpdates', () => { expect(updateCount).toBe(2); }); - // @gate !disableLegacyMode + // @gate !disableLegacyMode && classic it('should batch forceUpdate together', () => { let shouldUpdateCount = 0; let updateCount = 0; @@ -548,7 +548,7 @@ describe('ReactLegacyUpdates', () => { ); }); - // @gate !disableLegacyMode + // @gate !disableLegacyMode && classic it('should queue mount-ready handlers across different roots', () => { // We'll define two components A and B, then update both of them. When A's // componentDidUpdate handlers is called, B's DOM should already have been @@ -849,7 +849,7 @@ describe('ReactLegacyUpdates', () => { expect(callbackCount).toBe(1); }); - // @gate !disableLegacyMode + // @gate !disableLegacyMode && classic it('does not call render after a component as been deleted', () => { let renderCount = 0; let componentB = null; diff --git a/packages/react-dom/src/client/ReactDOM.js b/packages/react-dom/src/client/ReactDOM.js index 5f48120f1f68e..99d2b62f2f845 100644 --- a/packages/react-dom/src/client/ReactDOM.js +++ b/packages/react-dom/src/client/ReactDOM.js @@ -32,7 +32,6 @@ import { import {createEventHandle} from 'react-dom-bindings/src/client/ReactDOMEventHandle'; import { - batchedUpdates, flushSync as flushSyncWithoutWarningIfAlreadyRendering, isAlreadyRendering, injectIntoDevTools, @@ -167,9 +166,16 @@ function flushSync(fn: (() => R) | void): R | void { // Expose findDOMNode on internals Internals.findDOMNode = findDOMNode; +function unstable_batchedUpdates(fn: (a: A) => R, a: A): R { + // batchedUpdates was a legacy mode feature that is a no-op outside of + // legacy mode. In 19, we made it an actual no-op, but we're keeping it + // for now since there may be libraries that still include it. + return fn(a); +} + export { createPortal, - batchedUpdates as unstable_batchedUpdates, + unstable_batchedUpdates, flushSync, ReactVersion as version, // Disabled behind disableLegacyReactDOMAPIs @@ -196,7 +202,7 @@ Internals.Events = [ getFiberCurrentPropsFromNode, enqueueStateRestore, restoreStateIfNeeded, - batchedUpdates, + unstable_batchedUpdates, ]; const foundDevTools = injectIntoDevTools({ diff --git a/packages/react-dom/src/client/ReactDOMRootFB.js b/packages/react-dom/src/client/ReactDOMRootFB.js index 4f2f0094de2b4..138f781e6caa7 100644 --- a/packages/react-dom/src/client/ReactDOMRootFB.js +++ b/packages/react-dom/src/client/ReactDOMRootFB.js @@ -42,6 +42,7 @@ import { } from 'react-dom-bindings/src/client/HTMLNodeType'; import { + batchedUpdates, createContainer, createHydrationContainer, findHostInstanceWithNoPortals, @@ -416,3 +417,5 @@ export function unstable_renderSubtreeIntoContainer( callback, ); } + +export {batchedUpdates as unstable_batchedUpdates}; diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.js b/packages/react-reconciler/src/ReactFiberWorkLoop.js index fb0b710dbb81b..f9b18aff86485 100644 --- a/packages/react-reconciler/src/ReactFiberWorkLoop.js +++ b/packages/react-reconciler/src/ReactFiberWorkLoop.js @@ -39,6 +39,7 @@ import { disableLegacyContext, alwaysThrottleRetries, enableInfiniteRenderLoopDetection, + disableLegacyMode, } from 'shared/ReactFeatureFlags'; import ReactSharedInternals from 'shared/ReactSharedInternals'; import is from 'shared/objectIs'; @@ -1456,21 +1457,27 @@ export function deferredUpdates(fn: () => A): A { } export function batchedUpdates(fn: A => R, a: A): R { - const prevExecutionContext = executionContext; - executionContext |= BatchedContext; - try { + if (disableLegacyMode) { + // batchedUpdates is a no-op now, but there's still some internal react-dom + // code calling it, that we can't remove until we remove legacy mode. return fn(a); - } finally { - executionContext = prevExecutionContext; - // If there were legacy sync updates, flush them at the end of the outer - // most batchedUpdates-like method. - if ( - executionContext === NoContext && - // Treat `act` as if it's inside `batchedUpdates`, even in legacy mode. - !(__DEV__ && ReactCurrentActQueue.isBatchingLegacy) - ) { - resetRenderTimer(); - flushSyncWorkOnLegacyRootsOnly(); + } else { + const prevExecutionContext = executionContext; + executionContext |= BatchedContext; + try { + return fn(a); + } finally { + executionContext = prevExecutionContext; + // If there were legacy sync updates, flush them at the end of the outer + // most batchedUpdates-like method. + if ( + executionContext === NoContext && + // Treat `act` as if it's inside `batchedUpdates`, even in legacy mode. + !(__DEV__ && ReactCurrentActQueue.isBatchingLegacy) + ) { + resetRenderTimer(); + flushSyncWorkOnLegacyRootsOnly(); + } } } }