From aea7c2aab18f2c288a1fcab0cf7a14560b5db35e Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Mon, 3 May 2021 13:34:23 -0500 Subject: [PATCH] Re-land "Support nesting of startTransition and flushSync (alt) (#21149)" This re-lands commit faa1e127f1ba755da846bc6ce299cdefaf97721f. --- .../src/events/ReactDOMEventListener.js | 6 +++ .../src/ReactFiberWorkLoop.new.js | 22 ++++++++++ .../src/ReactFiberWorkLoop.old.js | 22 ++++++++++ .../src/__tests__/ReactFlushSync-test.js | 43 +++++++++++++++++++ 4 files changed, 93 insertions(+) diff --git a/packages/react-dom/src/events/ReactDOMEventListener.js b/packages/react-dom/src/events/ReactDOMEventListener.js index 590d8c2d9f5b3..81b3a1b85dce4 100644 --- a/packages/react-dom/src/events/ReactDOMEventListener.js +++ b/packages/react-dom/src/events/ReactDOMEventListener.js @@ -49,6 +49,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; @@ -125,11 +128,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 99f3e014148f5..0b15fb13a2351 100644 --- a/packages/react-reconciler/src/ReactFiberWorkLoop.new.js +++ b/packages/react-reconciler/src/ReactFiberWorkLoop.new.js @@ -245,6 +245,7 @@ const ceil = Math.ceil; const { ReactCurrentDispatcher, ReactCurrentOwner, + ReactCurrentBatchConfig, IsSomeRendererActing, } = ReactSharedInternals; @@ -1062,11 +1063,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; } } @@ -1110,11 +1114,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) { resetRenderTimer(); } @@ -1144,8 +1151,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); @@ -1154,6 +1163,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 @@ -1175,12 +1185,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) { @@ -1675,10 +1688,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); } @@ -1800,6 +1816,8 @@ function commitRootImpl(root, renderPriorityLevel) { NoFlags; if (subtreeHasEffects || rootHasEffect) { + const prevTransition = ReactCurrentBatchConfig.transition; + ReactCurrentBatchConfig.transition = 0; const previousPriority = getCurrentUpdatePriority(); setCurrentUpdatePriority(DiscreteEventPriority); @@ -1881,6 +1899,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; @@ -2017,12 +2036,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 e7328b989083b..7b03e4cc743df 100644 --- a/packages/react-reconciler/src/ReactFiberWorkLoop.old.js +++ b/packages/react-reconciler/src/ReactFiberWorkLoop.old.js @@ -245,6 +245,7 @@ const ceil = Math.ceil; const { ReactCurrentDispatcher, ReactCurrentOwner, + ReactCurrentBatchConfig, IsSomeRendererActing, } = ReactSharedInternals; @@ -1062,11 +1063,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; } } @@ -1110,11 +1114,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) { resetRenderTimer(); } @@ -1144,8 +1151,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); @@ -1154,6 +1163,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 @@ -1175,12 +1185,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) { @@ -1675,10 +1688,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); } @@ -1800,6 +1816,8 @@ function commitRootImpl(root, renderPriorityLevel) { NoFlags; if (subtreeHasEffects || rootHasEffect) { + const prevTransition = ReactCurrentBatchConfig.transition; + ReactCurrentBatchConfig.transition = 0; const previousPriority = getCurrentUpdatePriority(); setCurrentUpdatePriority(DiscreteEventPriority); @@ -1881,6 +1899,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; @@ -2017,12 +2036,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 327b4a87ca678..a3df1effd7b35 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}) { @@ -62,6 +64,47 @@ 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'); + }); + test('flushes passive effects synchronously when they are the result of a sync render', async () => { function App() { useEffect(() => {