diff --git a/packages/react-dom/src/events/ReactDOMEventListener.js b/packages/react-dom/src/events/ReactDOMEventListener.js index 3f00dcf305b37..f09d6ee864481 100644 --- a/packages/react-dom/src/events/ReactDOMEventListener.js +++ b/packages/react-dom/src/events/ReactDOMEventListener.js @@ -57,6 +57,9 @@ import { getCurrentUpdatePriority, setCurrentUpdatePriority, } from 'react-reconciler/src/ReactEventPriorities'; +import ReactSharedInternals from 'shared/ReactSharedInternals'; + +const {ReactCurrentBatchConfig} = ReactSharedInternals; // TODO: can we stop exporting these? export let _enabled = true; @@ -141,11 +144,14 @@ function dispatchContinuousEvent( nativeEvent, ) { const previousPriority = getCurrentUpdatePriority(); + const prevTransition = ReactCurrentBatchConfig.transition; + ReactCurrentBatchConfig.transition = 0; try { setCurrentUpdatePriority(ContinuousEventPriority); dispatchEvent(domEventName, eventSystemFlags, container, nativeEvent); } finally { setCurrentUpdatePriority(previousPriority); + ReactCurrentBatchConfig.transition = prevTransition; } } diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.new.js b/packages/react-reconciler/src/ReactFiberWorkLoop.new.js index cc41445830b40..b747392a61638 100644 --- a/packages/react-reconciler/src/ReactFiberWorkLoop.new.js +++ b/packages/react-reconciler/src/ReactFiberWorkLoop.new.js @@ -240,6 +240,7 @@ const ceil = Math.ceil; const { ReactCurrentDispatcher, ReactCurrentOwner, + ReactCurrentBatchConfig, IsSomeRendererActing, } = ReactSharedInternals; @@ -1072,11 +1073,14 @@ export function flushDiscreteUpdates() { export function deferredUpdates(fn: () => A): A { const previousPriority = getCurrentUpdatePriority(); + const prevTransition = ReactCurrentBatchConfig.transition; try { + ReactCurrentBatchConfig.transition = 0; setCurrentUpdatePriority(DefaultEventPriority); return fn(); } finally { setCurrentUpdatePriority(previousPriority); + ReactCurrentBatchConfig.transition = prevTransition; } } @@ -1118,11 +1122,14 @@ export function discreteUpdates( d: D, ): R { const previousPriority = getCurrentUpdatePriority(); + const prevTransition = ReactCurrentBatchConfig.transition; try { + ReactCurrentBatchConfig.transition = 0; setCurrentUpdatePriority(DiscreteEventPriority); return fn(a, b, c, d); } finally { setCurrentUpdatePriority(previousPriority); + ReactCurrentBatchConfig.transition = prevTransition; if (executionContext === NoContext) { // Flush the immediate callbacks that were scheduled during this batch resetRenderTimer(); @@ -1151,8 +1158,10 @@ export function flushSync(fn: A => R, a: A): R { const prevExecutionContext = executionContext; executionContext |= BatchedContext; + const prevTransition = ReactCurrentBatchConfig.transition; const previousPriority = getCurrentUpdatePriority(); try { + ReactCurrentBatchConfig.transition = 0; setCurrentUpdatePriority(DiscreteEventPriority); if (fn) { return fn(a); @@ -1161,6 +1170,7 @@ export function flushSync(fn: A => R, a: A): R { } } finally { setCurrentUpdatePriority(previousPriority); + ReactCurrentBatchConfig.transition = prevTransition; executionContext = prevExecutionContext; // Flush the immediate callbacks that were scheduled during this batch. // Note that this will happen even if batchedUpdates is higher up @@ -1182,12 +1192,15 @@ export function flushSync(fn: A => R, a: A): R { export function flushControlled(fn: () => mixed): void { const prevExecutionContext = executionContext; executionContext |= BatchedContext; + const prevTransition = ReactCurrentBatchConfig.transition; const previousPriority = getCurrentUpdatePriority(); try { + ReactCurrentBatchConfig.transition = 0; setCurrentUpdatePriority(DiscreteEventPriority); fn(); } finally { setCurrentUpdatePriority(previousPriority); + ReactCurrentBatchConfig.transition = prevTransition; executionContext = prevExecutionContext; if (executionContext === NoContext) { @@ -1681,10 +1694,13 @@ function commitRoot(root) { // TODO: This no longer makes any sense. We already wrap the mutation and // layout phases. Should be able to remove. const previousUpdateLanePriority = getCurrentUpdatePriority(); + const prevTransition = ReactCurrentBatchConfig.transition; try { + ReactCurrentBatchConfig.transition = 0; setCurrentUpdatePriority(DiscreteEventPriority); commitRootImpl(root, previousUpdateLanePriority); } finally { + ReactCurrentBatchConfig.transition = prevTransition; setCurrentUpdatePriority(previousUpdateLanePriority); } @@ -1797,6 +1813,8 @@ function commitRootImpl(root, renderPriorityLevel) { NoFlags; if (subtreeHasEffects || rootHasEffect) { + const prevTransition = ReactCurrentBatchConfig.transition; + ReactCurrentBatchConfig.transition = 0; const previousPriority = getCurrentUpdatePriority(); setCurrentUpdatePriority(DiscreteEventPriority); @@ -1882,6 +1900,7 @@ function commitRootImpl(root, renderPriorityLevel) { // Reset the priority to the previous non-sync value. setCurrentUpdatePriority(previousPriority); + ReactCurrentBatchConfig.transition = prevTransition; } else { // No effects. root.current = finishedWork; @@ -2022,12 +2041,15 @@ export function flushPassiveEffects(): boolean { if (rootWithPendingPassiveEffects !== null) { const renderPriority = lanesToEventPriority(pendingPassiveEffectsLanes); const priority = lowerEventPriority(DefaultEventPriority, renderPriority); + const prevTransition = ReactCurrentBatchConfig.transition; const previousPriority = getCurrentUpdatePriority(); try { + ReactCurrentBatchConfig.transition = 0; setCurrentUpdatePriority(priority); return flushPassiveEffectsImpl(); } finally { setCurrentUpdatePriority(previousPriority); + ReactCurrentBatchConfig.transition = prevTransition; } } return false; diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.old.js b/packages/react-reconciler/src/ReactFiberWorkLoop.old.js index 4420e51aa1e2c..35250f1d6d526 100644 --- a/packages/react-reconciler/src/ReactFiberWorkLoop.old.js +++ b/packages/react-reconciler/src/ReactFiberWorkLoop.old.js @@ -240,6 +240,7 @@ const ceil = Math.ceil; const { ReactCurrentDispatcher, ReactCurrentOwner, + ReactCurrentBatchConfig, IsSomeRendererActing, } = ReactSharedInternals; @@ -1072,11 +1073,14 @@ export function flushDiscreteUpdates() { export function deferredUpdates(fn: () => A): A { const previousPriority = getCurrentUpdatePriority(); + const prevTransition = ReactCurrentBatchConfig.transition; try { + ReactCurrentBatchConfig.transition = 0; setCurrentUpdatePriority(DefaultEventPriority); return fn(); } finally { setCurrentUpdatePriority(previousPriority); + ReactCurrentBatchConfig.transition = prevTransition; } } @@ -1118,11 +1122,14 @@ export function discreteUpdates( d: D, ): R { const previousPriority = getCurrentUpdatePriority(); + const prevTransition = ReactCurrentBatchConfig.transition; try { + ReactCurrentBatchConfig.transition = 0; setCurrentUpdatePriority(DiscreteEventPriority); return fn(a, b, c, d); } finally { setCurrentUpdatePriority(previousPriority); + ReactCurrentBatchConfig.transition = prevTransition; if (executionContext === NoContext) { // Flush the immediate callbacks that were scheduled during this batch resetRenderTimer(); @@ -1151,8 +1158,10 @@ export function flushSync(fn: A => R, a: A): R { const prevExecutionContext = executionContext; executionContext |= BatchedContext; + const prevTransition = ReactCurrentBatchConfig.transition; const previousPriority = getCurrentUpdatePriority(); try { + ReactCurrentBatchConfig.transition = 0; setCurrentUpdatePriority(DiscreteEventPriority); if (fn) { return fn(a); @@ -1161,6 +1170,7 @@ export function flushSync(fn: A => R, a: A): R { } } finally { setCurrentUpdatePriority(previousPriority); + ReactCurrentBatchConfig.transition = prevTransition; executionContext = prevExecutionContext; // Flush the immediate callbacks that were scheduled during this batch. // Note that this will happen even if batchedUpdates is higher up @@ -1182,12 +1192,15 @@ export function flushSync(fn: A => R, a: A): R { export function flushControlled(fn: () => mixed): void { const prevExecutionContext = executionContext; executionContext |= BatchedContext; + const prevTransition = ReactCurrentBatchConfig.transition; const previousPriority = getCurrentUpdatePriority(); try { + ReactCurrentBatchConfig.transition = 0; setCurrentUpdatePriority(DiscreteEventPriority); fn(); } finally { setCurrentUpdatePriority(previousPriority); + ReactCurrentBatchConfig.transition = prevTransition; executionContext = prevExecutionContext; if (executionContext === NoContext) { @@ -1681,10 +1694,13 @@ function commitRoot(root) { // TODO: This no longer makes any sense. We already wrap the mutation and // layout phases. Should be able to remove. const previousUpdateLanePriority = getCurrentUpdatePriority(); + const prevTransition = ReactCurrentBatchConfig.transition; try { + ReactCurrentBatchConfig.transition = 0; setCurrentUpdatePriority(DiscreteEventPriority); commitRootImpl(root, previousUpdateLanePriority); } finally { + ReactCurrentBatchConfig.transition = prevTransition; setCurrentUpdatePriority(previousUpdateLanePriority); } @@ -1797,6 +1813,8 @@ function commitRootImpl(root, renderPriorityLevel) { NoFlags; if (subtreeHasEffects || rootHasEffect) { + const prevTransition = ReactCurrentBatchConfig.transition; + ReactCurrentBatchConfig.transition = 0; const previousPriority = getCurrentUpdatePriority(); setCurrentUpdatePriority(DiscreteEventPriority); @@ -1882,6 +1900,7 @@ function commitRootImpl(root, renderPriorityLevel) { // Reset the priority to the previous non-sync value. setCurrentUpdatePriority(previousPriority); + ReactCurrentBatchConfig.transition = prevTransition; } else { // No effects. root.current = finishedWork; @@ -2022,12 +2041,15 @@ export function flushPassiveEffects(): boolean { if (rootWithPendingPassiveEffects !== null) { const renderPriority = lanesToEventPriority(pendingPassiveEffectsLanes); const priority = lowerEventPriority(DefaultEventPriority, renderPriority); + const prevTransition = ReactCurrentBatchConfig.transition; const previousPriority = getCurrentUpdatePriority(); try { + ReactCurrentBatchConfig.transition = 0; setCurrentUpdatePriority(priority); return flushPassiveEffectsImpl(); } finally { setCurrentUpdatePriority(previousPriority); + ReactCurrentBatchConfig.transition = prevTransition; } } return false; diff --git a/packages/react-reconciler/src/__tests__/ReactFlushSync-test.js b/packages/react-reconciler/src/__tests__/ReactFlushSync-test.js index 3c96f5ee65914..bc7499a3d43a7 100644 --- a/packages/react-reconciler/src/__tests__/ReactFlushSync-test.js +++ b/packages/react-reconciler/src/__tests__/ReactFlushSync-test.js @@ -3,6 +3,7 @@ let ReactNoop; let Scheduler; let useState; let useEffect; +let startTransition; describe('ReactFlushSync', () => { beforeEach(() => { @@ -13,6 +14,7 @@ describe('ReactFlushSync', () => { Scheduler = require('scheduler'); useState = React.useState; useEffect = React.useEffect; + startTransition = React.unstable_startTransition; }); function Text({text}) { @@ -54,4 +56,45 @@ describe('ReactFlushSync', () => { }); expect(root).toMatchRenderedOutput('1, 1'); }); + + // @gate experimental + test('nested with startTransition', async () => { + let setSyncState; + let setState; + function App() { + const [syncState, _setSyncState] = useState(0); + const [state, _setState] = useState(0); + setSyncState = _setSyncState; + setState = _setState; + return ; + } + + const root = ReactNoop.createRoot(); + await ReactNoop.act(async () => { + root.render(); + }); + expect(Scheduler).toHaveYielded(['0, 0']); + expect(root).toMatchRenderedOutput('0, 0'); + + await ReactNoop.act(async () => { + ReactNoop.flushSync(() => { + startTransition(() => { + // This should be async even though flushSync is on the stack, because + // startTransition is closer. + setState(1); + ReactNoop.flushSync(() => { + // This should be async even though startTransition is on the stack, + // because flushSync is closer. + setSyncState(1); + }); + }); + }); + // Only the sync update should have flushed + expect(Scheduler).toHaveYielded(['1, 0']); + expect(root).toMatchRenderedOutput('1, 0'); + }); + // Now the async update has flushed, too. + expect(Scheduler).toHaveYielded(['1, 1']); + expect(root).toMatchRenderedOutput('1, 1'); + }); });