diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.new.js b/packages/react-reconciler/src/ReactFiberWorkLoop.new.js index 3ec3120b853ac..a7dc1af53e56b 100644 --- a/packages/react-reconciler/src/ReactFiberWorkLoop.new.js +++ b/packages/react-reconciler/src/ReactFiberWorkLoop.new.js @@ -1161,16 +1161,6 @@ export function unbatchedUpdates(fn: (a: A) => R, a: A): R { export function flushSync(fn: A => R, a: A): R { const prevExecutionContext = executionContext; - if ((prevExecutionContext & (RenderContext | CommitContext)) !== NoContext) { - if (__DEV__) { - 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.', - ); - } - return fn(a); - } executionContext |= BatchedContext; const previousPriority = getCurrentUpdatePriority(); @@ -1187,7 +1177,17 @@ export function flushSync(fn: A => R, a: A): R { // Flush the immediate callbacks that were scheduled during this batch. // Note that this will happen even if batchedUpdates is higher up // the stack. - flushSyncCallbackQueue(); + if ((executionContext & (RenderContext | CommitContext)) === NoContext) { + flushSyncCallbackQueue(); + } else { + if (__DEV__) { + 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.', + ); + } + } } } diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.old.js b/packages/react-reconciler/src/ReactFiberWorkLoop.old.js index f45aee1d8c048..c5482e05dfce5 100644 --- a/packages/react-reconciler/src/ReactFiberWorkLoop.old.js +++ b/packages/react-reconciler/src/ReactFiberWorkLoop.old.js @@ -1161,16 +1161,6 @@ export function unbatchedUpdates(fn: (a: A) => R, a: A): R { export function flushSync(fn: A => R, a: A): R { const prevExecutionContext = executionContext; - if ((prevExecutionContext & (RenderContext | CommitContext)) !== NoContext) { - if (__DEV__) { - 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.', - ); - } - return fn(a); - } executionContext |= BatchedContext; const previousPriority = getCurrentUpdatePriority(); @@ -1187,7 +1177,17 @@ export function flushSync(fn: A => R, a: A): R { // Flush the immediate callbacks that were scheduled during this batch. // Note that this will happen even if batchedUpdates is higher up // the stack. - flushSyncCallbackQueue(); + if ((executionContext & (RenderContext | CommitContext)) === NoContext) { + flushSyncCallbackQueue(); + } else { + if (__DEV__) { + 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.', + ); + } + } } } diff --git a/packages/react-reconciler/src/__tests__/ReactFlushSync-test.js b/packages/react-reconciler/src/__tests__/ReactFlushSync-test.js new file mode 100644 index 0000000000000..3c96f5ee65914 --- /dev/null +++ b/packages/react-reconciler/src/__tests__/ReactFlushSync-test.js @@ -0,0 +1,57 @@ +let React; +let ReactNoop; +let Scheduler; +let useState; +let useEffect; + +describe('ReactFlushSync', () => { + beforeEach(() => { + jest.resetModules(); + + React = require('react'); + ReactNoop = require('react-noop-renderer'); + Scheduler = require('scheduler'); + useState = React.useState; + useEffect = React.useEffect; + }); + + function Text({text}) { + Scheduler.unstable_yieldValue(text); + return text; + } + + test('changes priority of updates in useEffect', async () => { + function App() { + const [syncState, setSyncState] = useState(0); + const [state, setState] = useState(0); + useEffect(() => { + if (syncState !== 1) { + setState(1); + ReactNoop.flushSync(() => setSyncState(1)); + } + }, [syncState, state]); + return ; + } + + const root = ReactNoop.createRoot(); + await ReactNoop.act(async () => { + root.render(); + // This will yield right before the passive effect fires + expect(Scheduler).toFlushUntilNextPaint(['0, 0']); + + // The passive effect will schedule a sync update and a normal update. + // They should commit in two separate batches. First the sync one. + expect(() => + expect(Scheduler).toFlushUntilNextPaint(['1, 0']), + ).toErrorDev('flushSync was called from inside a lifecycle method'); + + // The remaining update is not sync + ReactNoop.flushSync(); + expect(Scheduler).toHaveYielded([]); + + // Now flush it. + expect(Scheduler).toFlushUntilNextPaint(['1, 1']); + }); + expect(root).toMatchRenderedOutput('1, 1'); + }); +});