From b053ca95e127fee4976878ef70ccd988f7e91d5d Mon Sep 17 00:00:00 2001 From: Tianyu Yao Date: Wed, 9 Nov 2022 13:07:11 -0800 Subject: [PATCH] Batch default, continuous and sync lane --- .../src/__tests__/ReactDOMFiberAsync-test.js | 27 ++- .../src/ReactFiberLane.new.js | 26 ++- .../src/ReactFiberLane.old.js | 26 ++- .../__tests__/ReactBatching-test.internal.js | 18 +- .../ReactClassSetStateCallback-test.js | 14 +- .../src/__tests__/ReactExpiration-test.js | 4 +- .../src/__tests__/ReactFlushSync-test.js | 12 +- .../src/__tests__/ReactHooks-test.internal.js | 10 +- .../ReactHooksWithNoopRenderer-test.js | 36 ++-- .../src/__tests__/ReactIncremental-test.js | 58 ++++-- .../__tests__/ReactIncrementalUpdates-test.js | 174 +++++++++++------- .../src/__tests__/ReactOffscreen-test.js | 11 +- .../__tests__/ReactOffscreenSuspense-test.js | 10 +- .../src/__tests__/ReactTransition-test.js | 32 +++- .../useMutableSource-test.internal.js | 11 +- .../src/__tests__/useSubscription-test.js | 8 +- 16 files changed, 342 insertions(+), 135 deletions(-) diff --git a/packages/react-dom/src/__tests__/ReactDOMFiberAsync-test.js b/packages/react-dom/src/__tests__/ReactDOMFiberAsync-test.js index a94cbe367a8ae..3360ef2cbd52a 100644 --- a/packages/react-dom/src/__tests__/ReactDOMFiberAsync-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMFiberAsync-test.js @@ -275,17 +275,32 @@ describe('ReactDOMFiberAsync', () => { expect(ops).toEqual([]); }); // Only the active updates have flushed - expect(container.textContent).toEqual('BC'); - expect(ops).toEqual(['BC']); + if (gate(flags => flags.enableSyncDefaultUpdates)) { + expect(container.textContent).toEqual('ABC'); + expect(ops).toEqual(['ABC']); + } else { + expect(container.textContent).toEqual('BC'); + expect(ops).toEqual(['BC']); + } - instance.push('D'); - expect(container.textContent).toEqual('BC'); - expect(ops).toEqual(['BC']); + if (gate(flags => flags.enableSyncDefaultUpdates)) { + instance.push('D'); + expect(container.textContent).toEqual('ABC'); + expect(ops).toEqual(['ABC']); + } else { + instance.push('D'); + expect(container.textContent).toEqual('BC'); + expect(ops).toEqual(['BC']); + } // Flush the async updates Scheduler.unstable_flushAll(); expect(container.textContent).toEqual('ABCD'); - expect(ops).toEqual(['BC', 'ABCD']); + if (gate(flags => flags.enableSyncDefaultUpdates)) { + expect(ops).toEqual(['ABC', 'ABCD']); + } else { + expect(ops).toEqual(['BC', 'ABCD']); + } }); // @gate www diff --git a/packages/react-reconciler/src/ReactFiberLane.new.js b/packages/react-reconciler/src/ReactFiberLane.new.js index cc3781bd7b3ed..de2e119cc3c63 100644 --- a/packages/react-reconciler/src/ReactFiberLane.new.js +++ b/packages/react-reconciler/src/ReactFiberLane.new.js @@ -23,6 +23,7 @@ import { enableUpdaterTracking, allowConcurrentByDefault, enableTransitionTracing, + enableSyncDefaultUpdates, } from 'shared/ReactFeatureFlags'; import {isDevToolsPresent} from './ReactFiberDevToolsHook.new'; import {ConcurrentUpdatesByDefaultMode, NoMode} from './ReactTypeOfMode'; @@ -135,8 +136,28 @@ let nextRetryLane: Lane = RetryLane1; function getHighestPriorityLanes(lanes: Lanes | Lane): Lanes { switch (getHighestPriorityLane(lanes)) { case SyncHydrationLane: + if (enableSyncDefaultUpdates) { + let ret = SyncHydrationLane; + if (lanes & DefaultHydrationLane) { + ret |= DefaultHydrationLane; + } + if (lanes & InputContinuousHydrationLane) { + ret |= InputContinuousHydrationLane; + } + return ret; + } return SyncHydrationLane; case SyncLane: + if (enableSyncDefaultUpdates) { + let ret = SyncLane; + if (lanes & DefaultLane) { + ret |= DefaultLane; + } + if (lanes & InputContinuousLane) { + ret |= InputContinuousLane; + } + return ret; + } return SyncLane; case InputContinuousHydrationLane: return InputContinuousHydrationLane; @@ -251,7 +272,10 @@ export function getNextLanes(root: FiberRoot, wipLanes: Lanes): Lanes { // Default priority updates should not interrupt transition updates. The // only difference between default updates and transition updates is that // default updates do not support refresh transitions. - (nextLane === DefaultLane && (wipLane & TransitionLanes) !== NoLanes) + // Interrupt transtion if default is batched with sync. + (!enableSyncDefaultUpdates && + nextLane === DefaultLane && + (wipLane & TransitionLanes) !== NoLanes) ) { // Keep working on the existing in-progress tree. Do not interrupt. return wipLanes; diff --git a/packages/react-reconciler/src/ReactFiberLane.old.js b/packages/react-reconciler/src/ReactFiberLane.old.js index 110893a036530..3c9ff604a558f 100644 --- a/packages/react-reconciler/src/ReactFiberLane.old.js +++ b/packages/react-reconciler/src/ReactFiberLane.old.js @@ -23,6 +23,7 @@ import { enableUpdaterTracking, allowConcurrentByDefault, enableTransitionTracing, + enableSyncDefaultUpdates, } from 'shared/ReactFeatureFlags'; import {isDevToolsPresent} from './ReactFiberDevToolsHook.old'; import {ConcurrentUpdatesByDefaultMode, NoMode} from './ReactTypeOfMode'; @@ -135,8 +136,28 @@ let nextRetryLane: Lane = RetryLane1; function getHighestPriorityLanes(lanes: Lanes | Lane): Lanes { switch (getHighestPriorityLane(lanes)) { case SyncHydrationLane: + if (enableSyncDefaultUpdates) { + let ret = SyncHydrationLane; + if (lanes & DefaultHydrationLane) { + ret |= DefaultHydrationLane; + } + if (lanes & InputContinuousHydrationLane) { + ret |= InputContinuousHydrationLane; + } + return ret; + } return SyncHydrationLane; case SyncLane: + if (enableSyncDefaultUpdates) { + let ret = SyncLane; + if (lanes & DefaultLane) { + ret |= DefaultLane; + } + if (lanes & InputContinuousLane) { + ret |= InputContinuousLane; + } + return ret; + } return SyncLane; case InputContinuousHydrationLane: return InputContinuousHydrationLane; @@ -251,7 +272,10 @@ export function getNextLanes(root: FiberRoot, wipLanes: Lanes): Lanes { // Default priority updates should not interrupt transition updates. The // only difference between default updates and transition updates is that // default updates do not support refresh transitions. - (nextLane === DefaultLane && (wipLane & TransitionLanes) !== NoLanes) + // Interrupt transtion if default is batched with sync. + (!enableSyncDefaultUpdates && + nextLane === DefaultLane && + (wipLane & TransitionLanes) !== NoLanes) ) { // Keep working on the existing in-progress tree. Do not interrupt. return wipLanes; diff --git a/packages/react-reconciler/src/__tests__/ReactBatching-test.internal.js b/packages/react-reconciler/src/__tests__/ReactBatching-test.internal.js index 7998d2a23177e..6b4771493a194 100644 --- a/packages/react-reconciler/src/__tests__/ReactBatching-test.internal.js +++ b/packages/react-reconciler/src/__tests__/ReactBatching-test.internal.js @@ -157,12 +157,18 @@ describe('ReactBlockingMode', () => { }), ); - // Only the second update should have flushed synchronously - expect(Scheduler).toHaveYielded(['B1']); - expect(root).toMatchRenderedOutput('A0B1'); - // Now flush the first update - expect(Scheduler).toFlushAndYield(['A1']); - expect(root).toMatchRenderedOutput('A1B1'); + if (gate(flags => flags.enableSyncDefaultUpdates)) { + expect(Scheduler).toHaveYielded(['A1', 'B1']); + expect(root).toMatchRenderedOutput('A1B1'); + } else { + // Only the second update should have flushed synchronously + expect(Scheduler).toHaveYielded(['B1']); + expect(root).toMatchRenderedOutput('A0B1'); + + // Now flush the first update + expect(Scheduler).toFlushAndYield(['A1']); + expect(root).toMatchRenderedOutput('A1B1'); + } }); }); diff --git a/packages/react-reconciler/src/__tests__/ReactClassSetStateCallback-test.js b/packages/react-reconciler/src/__tests__/ReactClassSetStateCallback-test.js index 0f97bb5997186..372151466f427 100644 --- a/packages/react-reconciler/src/__tests__/ReactClassSetStateCallback-test.js +++ b/packages/react-reconciler/src/__tests__/ReactClassSetStateCallback-test.js @@ -35,9 +35,17 @@ describe('ReactClassSetStateCallback', () => { expect(Scheduler).toHaveYielded([0]); await act(async () => { - app.setState({step: 1}, () => - Scheduler.unstable_yieldValue('Callback 1'), - ); + if (gate(flags => flags.enableSyncDefaultUpdates)) { + React.startTransition(() => { + app.setState({step: 1}, () => + Scheduler.unstable_yieldValue('Callback 1'), + ); + }); + } else { + app.setState({step: 1}, () => + Scheduler.unstable_yieldValue('Callback 1'), + ); + } ReactNoop.flushSync(() => { app.setState({step: 2}, () => Scheduler.unstable_yieldValue('Callback 2'), diff --git a/packages/react-reconciler/src/__tests__/ReactExpiration-test.js b/packages/react-reconciler/src/__tests__/ReactExpiration-test.js index 24127589dfd90..72ca257fefa8d 100644 --- a/packages/react-reconciler/src/__tests__/ReactExpiration-test.js +++ b/packages/react-reconciler/src/__tests__/ReactExpiration-test.js @@ -339,7 +339,9 @@ describe('ReactExpiration', () => { // Before the update can finish, update again. Even though no time has // advanced, this update should be given a different expiration time than // the currently rendering one. So, C and D should render with 1, not 2. - subscribers.forEach(s => s.setState({text: '2'})); + React.startTransition(() => { + subscribers.forEach(s => s.setState({text: '2'})); + }); expect(Scheduler).toFlushAndYieldThrough([ '1 [C] [render]', '1 [D] [render]', diff --git a/packages/react-reconciler/src/__tests__/ReactFlushSync-test.js b/packages/react-reconciler/src/__tests__/ReactFlushSync-test.js index 07cc3437a5e24..1c1d79e841751 100644 --- a/packages/react-reconciler/src/__tests__/ReactFlushSync-test.js +++ b/packages/react-reconciler/src/__tests__/ReactFlushSync-test.js @@ -54,15 +54,21 @@ describe('ReactFlushSync', () => { // 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']); + expect(Scheduler).toFlushUntilNextPaint( + gate(flags => flags.enableSyncDefaultUpdates) ? ['1, 1'] : ['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']); + if (gate(flags => flags.enableSyncDefaultUpdates)) { + expect(Scheduler).toFlushUntilNextPaint([]); + } else { + // Now flush it. + expect(Scheduler).toFlushUntilNextPaint(['1 / 1']); + } }); expect(root).toMatchRenderedOutput('1, 1'); }); diff --git a/packages/react-reconciler/src/__tests__/ReactHooks-test.internal.js b/packages/react-reconciler/src/__tests__/ReactHooks-test.internal.js index 37fd06f1e0260..618c7cdff082b 100644 --- a/packages/react-reconciler/src/__tests__/ReactHooks-test.internal.js +++ b/packages/react-reconciler/src/__tests__/ReactHooks-test.internal.js @@ -568,9 +568,13 @@ describe('ReactHooks', () => { }); }; - // Update at normal priority - ReactTestRenderer.unstable_batchedUpdates(() => update(n => n * 100)); - + if (gate(flags => flags.enableSyncDefaultUpdates)) { + // Update at transition priority + React.startTransition(() => update(n => n * 100)); + } else { + // Update at normal priority + ReactTestRenderer.unstable_batchedUpdates(() => update(n => n * 100)); + } // The new state is eagerly computed. expect(Scheduler).toHaveYielded(['Compute state (1 -> 100)']); diff --git a/packages/react-reconciler/src/__tests__/ReactHooksWithNoopRenderer-test.js b/packages/react-reconciler/src/__tests__/ReactHooksWithNoopRenderer-test.js index d16b07fc0772f..6cdde2b901632 100644 --- a/packages/react-reconciler/src/__tests__/ReactHooksWithNoopRenderer-test.js +++ b/packages/react-reconciler/src/__tests__/ReactHooksWithNoopRenderer-test.js @@ -815,7 +815,13 @@ describe('ReactHooksWithNoopRenderer', () => { ReactNoop.discreteUpdates(() => { setRow(5); }); - setRow(20); + if (gate(flags => flags.enableSyncDefaultUpdates)) { + React.startTransition(() => { + setRow(20); + }); + } else { + setRow(20); + } }); expect(Scheduler).toHaveYielded(['Up', 'Down']); expect(root).toMatchRenderedOutput(); @@ -955,11 +961,15 @@ describe('ReactHooksWithNoopRenderer', () => { ReactNoop.flushSync(() => { counter.current.dispatch(INCREMENT); }); - expect(Scheduler).toHaveYielded(['Count: 1']); - expect(ReactNoop.getChildren()).toEqual([span('Count: 1')]); - - expect(Scheduler).toFlushAndYield(['Count: 4']); - expect(ReactNoop.getChildren()).toEqual([span('Count: 4')]); + if (gate(flags => flags.enableSyncDefaultUpdates)) { + expect(Scheduler).toHaveYielded(['Count: 4']); + expect(ReactNoop.getChildren()).toEqual([span('Count: 4')]); + } else { + expect(Scheduler).toHaveYielded(['Count: 1']); + expect(ReactNoop.getChildren()).toEqual([span('Count: 1')]); + expect(Scheduler).toFlushAndYield(['Count: 4']); + expect(ReactNoop.getChildren()).toEqual([span('Count: 4')]); + } }); }); @@ -1717,11 +1727,15 @@ describe('ReactHooksWithNoopRenderer', () => { // As a result we, somewhat surprisingly, commit them in the opposite order. // This should be fine because any non-discrete set of work doesn't guarantee order // and easily could've happened slightly later too. - expect(Scheduler).toHaveYielded([ - 'Will set count to 1', - 'Count: 2', - 'Count: 1', - ]); + if (gate(flags => flags.enableSyncDefaultUpdates)) { + expect(Scheduler).toHaveYielded(['Will set count to 1', 'Count: 1']); + } else { + expect(Scheduler).toHaveYielded([ + 'Will set count to 1', + 'Count: 2', + 'Count: 1', + ]); + } expect(ReactNoop.getChildren()).toEqual([span('Count: 1')]); }); diff --git a/packages/react-reconciler/src/__tests__/ReactIncremental-test.js b/packages/react-reconciler/src/__tests__/ReactIncremental-test.js index 8b6226888b2aa..3b047a2973ed7 100644 --- a/packages/react-reconciler/src/__tests__/ReactIncremental-test.js +++ b/packages/react-reconciler/src/__tests__/ReactIncremental-test.js @@ -1910,21 +1910,37 @@ describe('ReactIncremental', () => { , ); - expect(Scheduler).toFlushAndYield([ - 'ShowLocale {"locale":"sv"}', - 'ShowBoth {"locale":"sv"}', - 'Intl {}', - 'ShowLocale {"locale":"en"}', - 'Router {}', - 'Indirection {}', - 'ShowLocale {"locale":"en"}', - 'ShowRoute {"route":"/about"}', - 'ShowNeither {}', - 'Intl {}', - 'ShowBoth {"locale":"ru","route":"/about"}', - 'ShowBoth {"locale":"en","route":"/about"}', - 'ShowBoth {"locale":"en"}', - ]); + if (gate(flags => flags.enableSyncDefaultUpdates)) { + expect(Scheduler).toFlushAndYield([ + 'Intl {}', + 'ShowLocale {"locale":"en"}', + 'Router {}', + 'Indirection {}', + 'ShowLocale {"locale":"en"}', + 'ShowRoute {"route":"/about"}', + 'ShowNeither {}', + 'Intl {}', + 'ShowBoth {"locale":"ru","route":"/about"}', + 'ShowBoth {"locale":"en","route":"/about"}', + 'ShowBoth {"locale":"en"}', + ]); + } else { + expect(Scheduler).toFlushAndYield([ + 'ShowLocale {"locale":"sv"}', + 'ShowBoth {"locale":"sv"}', + 'Intl {}', + 'ShowLocale {"locale":"en"}', + 'Router {}', + 'Indirection {}', + 'ShowLocale {"locale":"en"}', + 'ShowRoute {"route":"/about"}', + 'ShowNeither {}', + 'Intl {}', + 'ShowBoth {"locale":"ru","route":"/about"}', + 'ShowBoth {"locale":"en","route":"/about"}', + 'ShowBoth {"locale":"en"}', + ]); + } }); it('does not leak own context into context provider', () => { @@ -2758,7 +2774,11 @@ describe('ReactIncremental', () => { // Interrupt at same priority ReactNoop.render(); - expect(Scheduler).toFlushAndYield(['Child: 1', 'Parent: 2', 'Child: 2']); + if (gate(flags => flags.enableSyncDefaultUpdates)) { + expect(Scheduler).toFlushAndYield(['Parent: 2', 'Child: 2']); + } else { + expect(Scheduler).toFlushAndYield(['Child: 1', 'Parent: 2', 'Child: 2']); + } }); it('does not interrupt for update at lower priority', () => { @@ -2785,7 +2805,11 @@ describe('ReactIncremental', () => { ReactNoop.expire(2000); ReactNoop.render(); - expect(Scheduler).toFlushAndYield(['Child: 1', 'Parent: 2', 'Child: 2']); + if (gate(flags => flags.enableSyncDefaultUpdates)) { + expect(Scheduler).toFlushAndYield(['Parent: 2', 'Child: 2']); + } else { + expect(Scheduler).toFlushAndYield(['Child: 1', 'Parent: 2', 'Child: 2']); + } }); it('does interrupt for update at higher priority', () => { diff --git a/packages/react-reconciler/src/__tests__/ReactIncrementalUpdates-test.js b/packages/react-reconciler/src/__tests__/ReactIncrementalUpdates-test.js index 454a0399b9a52..552ff16148229 100644 --- a/packages/react-reconciler/src/__tests__/ReactIncrementalUpdates-test.js +++ b/packages/react-reconciler/src/__tests__/ReactIncrementalUpdates-test.js @@ -47,7 +47,7 @@ describe('ReactIncrementalUpdates', () => { state = {}; componentDidMount() { Scheduler.unstable_yieldValue('commit'); - ReactNoop.deferredUpdates(() => { + React.startTransition(() => { // Has low priority this.setState({b: 'b'}); this.setState({c: 'c'}); @@ -111,13 +111,13 @@ describe('ReactIncrementalUpdates', () => { expect(Scheduler).toFlushAndYield(['render', 'componentDidMount']); ReactNoop.flushSync(() => { - ReactNoop.deferredUpdates(() => { + React.startTransition(() => { instance.setState({x: 'x'}); instance.setState({y: 'y'}); }); instance.setState({a: 'a'}); instance.setState({b: 'b'}); - ReactNoop.deferredUpdates(() => { + React.startTransition(() => { instance.updater.enqueueReplaceState(instance, {c: 'c'}); instance.setState({d: 'd'}); }); @@ -190,27 +190,40 @@ describe('ReactIncrementalUpdates', () => { }); // The sync updates should have flushed, but not the async ones - expect(Scheduler).toHaveYielded(['e', 'f']); - expect(ReactNoop.getChildren()).toEqual([span('ef')]); + expect(Scheduler).toHaveYielded(['d', 'e', 'f']); + expect(ReactNoop.getChildren()).toEqual([span('def')]); // Now flush the remaining work. Even though e and f were already processed, // they should be processed again, to ensure that the terminal state // is deterministic. - expect(Scheduler).toFlushAndYield([ - // Since 'g' is in a transition, we'll process 'd' separately first. - // That causes us to process 'd' with 'e' and 'f' rebased. - 'd', - 'e', - 'f', - // Then we'll re-process everything for 'g'. - 'a', - 'b', - 'c', - 'd', - 'e', - 'f', - 'g', - ]); + if (gate(flags => flags.enableSyncDefaultUpdates)) { + expect(Scheduler).toFlushAndYield([ + // Then we'll re-process everything for 'g'. + 'a', + 'b', + 'c', + 'd', + 'e', + 'f', + 'g', + ]); + } else { + expect(Scheduler).toFlushAndYield([ + // Since 'g' is in a transition, we'll process 'd' separately first. + // That causes us to process 'd' with 'e' and 'f' rebased. + 'd', + 'e', + 'f', + // Then we'll re-process everything for 'g'. + 'a', + 'b', + 'c', + 'd', + 'e', + 'f', + 'g', + ]); + } expect(ReactNoop.getChildren()).toEqual([span('abcdefg')]); } else { instance.setState(createUpdate('d')); @@ -292,27 +305,40 @@ describe('ReactIncrementalUpdates', () => { }); // The sync updates should have flushed, but not the async ones. - expect(Scheduler).toHaveYielded(['e', 'f']); + expect(Scheduler).toHaveYielded(['d', 'e', 'f']); expect(ReactNoop.getChildren()).toEqual([span('f')]); // Now flush the remaining work. Even though e and f were already processed, // they should be processed again, to ensure that the terminal state // is deterministic. - expect(Scheduler).toFlushAndYield([ - // Since 'g' is in a transition, we'll process 'd' separately first. - // That causes us to process 'd' with 'e' and 'f' rebased. - 'd', - 'e', - 'f', - // Then we'll re-process everything for 'g'. - 'a', - 'b', - 'c', - 'd', - 'e', - 'f', - 'g', - ]); + if (gate(flags => flags.enableSyncDefaultUpdates)) { + expect(Scheduler).toFlushAndYield([ + // Then we'll re-process everything for 'g'. + 'a', + 'b', + 'c', + 'd', + 'e', + 'f', + 'g', + ]); + } else { + expect(Scheduler).toFlushAndYield([ + // Since 'g' is in a transition, we'll process 'd' separately first. + // That causes us to process 'd' with 'e' and 'f' rebased. + 'd', + 'e', + 'f', + // Then we'll re-process everything for 'g'. + 'a', + 'b', + 'c', + 'd', + 'e', + 'f', + 'g', + ]); + } expect(ReactNoop.getChildren()).toEqual([span('fg')]); } else { instance.setState(createUpdate('d')); @@ -688,21 +714,29 @@ describe('ReactIncrementalUpdates', () => { pushToLog('B'), ); }); - expect(Scheduler).toHaveYielded([ - // A and B are pending. B is higher priority, so we'll render that first. - 'Committed: B', - // Because A comes first in the queue, we're now in rebase mode. B must - // be rebased on top of A. Also, in a layout effect, we received two new - // updates: C and D. C is user-blocking and D is synchronous. - // - // First render the synchronous update. What we're testing here is that - // B *is not dropped* even though it has lower than sync priority. That's - // because we already committed it. However, this render should not - // include C, because that update wasn't already committed. - 'Committed: BD', - 'Committed: BCD', - 'Committed: ABCD', - ]); + if (gate(flags => flags.enableSyncDefaultUpdates)) { + expect(Scheduler).toHaveYielded([ + 'Committed: B', + 'Committed: BCD', + 'Committed: ABCD', + ]); + } else { + expect(Scheduler).toHaveYielded([ + // A and B are pending. B is higher priority, so we'll render that first. + 'Committed: B', + // Because A comes first in the queue, we're now in rebase mode. B must + // be rebased on top of A. Also, in a layout effect, we received two new + // updates: C and D. C is user-blocking and D is synchronous. + // + // First render the synchronous update. What we're testing here is that + // B *is not dropped* even though it has lower than sync priority. That's + // because we already committed it. However, this render should not + // include C, because that update wasn't already committed. + 'Committed: BD', + 'Committed: BCD', + 'Committed: ABCD', + ]); + } expect(root).toMatchRenderedOutput('ABCD'); }); @@ -748,21 +782,29 @@ describe('ReactIncrementalUpdates', () => { pushToLog('B'), ); }); - expect(Scheduler).toHaveYielded([ - // A and B are pending. B is higher priority, so we'll render that first. - 'Committed: B', - // Because A comes first in the queue, we're now in rebase mode. B must - // be rebased on top of A. Also, in a layout effect, we received two new - // updates: C and D. C is user-blocking and D is synchronous. - // - // First render the synchronous update. What we're testing here is that - // B *is not dropped* even though it has lower than sync priority. That's - // because we already committed it. However, this render should not - // include C, because that update wasn't already committed. - 'Committed: BD', - 'Committed: BCD', - 'Committed: ABCD', - ]); + if (gate(flags => flags.enableSyncDefaultUpdates)) { + expect(Scheduler).toHaveYielded([ + 'Committed: B', + 'Committed: BCD', + 'Committed: ABCD', + ]); + } else { + expect(Scheduler).toHaveYielded([ + // A and B are pending. B is higher priority, so we'll render that first. + 'Committed: B', + // Because A comes first in the queue, we're now in rebase mode. B must + // be rebased on top of A. Also, in a layout effect, we received two new + // updates: C and D. C is user-blocking and D is synchronous. + // + // First render the synchronous update. What we're testing here is that + // B *is not dropped* even though it has lower than sync priority. That's + // because we already committed it. However, this render should not + // include C, because that update wasn't already committed. + 'Committed: BD', + 'Committed: BCD', + 'Committed: ABCD', + ]); + } expect(root).toMatchRenderedOutput('ABCD'); }); diff --git a/packages/react-reconciler/src/__tests__/ReactOffscreen-test.js b/packages/react-reconciler/src/__tests__/ReactOffscreen-test.js index d830265610823..7f77ba535517c 100644 --- a/packages/react-reconciler/src/__tests__/ReactOffscreen-test.js +++ b/packages/react-reconciler/src/__tests__/ReactOffscreen-test.js @@ -673,8 +673,15 @@ describe('ReactOffscreen', () => { ); // Before the inner update can finish, we receive another pair of updates. - setOuter(2); - setInner(2); + if (gate(flags => flags.enableSyncDefaultUpdates)) { + React.startTransition(() => { + setOuter(2); + setInner(2); + }); + } else { + setOuter(2); + setInner(2); + } // Also, before either of these new updates are processed, the hidden // tree is revealed at high priority. diff --git a/packages/react-reconciler/src/__tests__/ReactOffscreenSuspense-test.js b/packages/react-reconciler/src/__tests__/ReactOffscreenSuspense-test.js index e1a6ca49fd64c..272979fab6444 100644 --- a/packages/react-reconciler/src/__tests__/ReactOffscreenSuspense-test.js +++ b/packages/react-reconciler/src/__tests__/ReactOffscreenSuspense-test.js @@ -381,7 +381,9 @@ describe('ReactOffscreen', () => { expect(root).toMatchRenderedOutput(); await act(async () => { - setStep(1); + React.startTransition(() => { + setStep(1); + }); ReactNoop.flushSync(() => { setText('B'); }); @@ -513,8 +515,10 @@ describe('ReactOffscreen', () => { // Before the tree commits, schedule a concurrent event. The inner update // is to a tree that's just about to be hidden. - setOuter(2); - setInner(2); + startTransition(() => { + setOuter(2); + setInner(2); + }); // Commit the previous render. jest.runAllTimers(); diff --git a/packages/react-reconciler/src/__tests__/ReactTransition-test.js b/packages/react-reconciler/src/__tests__/ReactTransition-test.js index 560bb527b1b0e..df9a4b48579ff 100644 --- a/packages/react-reconciler/src/__tests__/ReactTransition-test.js +++ b/packages/react-reconciler/src/__tests__/ReactTransition-test.js @@ -934,16 +934,30 @@ describe('ReactTransition', () => { updateNormalPri(); }); - expect(Scheduler).toHaveYielded([ - // Finish transition update. - 'Normal pri: 0', - 'Commit', + if (gate(flags => flags.enableSyncDefaultUpdates)) { + expect(Scheduler).toHaveYielded([ + // Interrupt transition. + 'Transition pri: 0', + 'Normal pri: 1', + 'Commit', - // Normal pri update. - 'Transition pri: 1', - 'Normal pri: 1', - 'Commit', - ]); + // Normal pri update. + 'Transition pri: 1', + 'Normal pri: 1', + 'Commit', + ]); + } else { + expect(Scheduler).toHaveYielded([ + // Finish transition update. + 'Normal pri: 0', + 'Commit', + + // Normal pri update. + 'Transition pri: 1', + 'Normal pri: 1', + 'Commit', + ]); + } expect(root).toMatchRenderedOutput('Transition pri: 1, Normal pri: 1'); }); diff --git a/packages/react-reconciler/src/__tests__/useMutableSource-test.internal.js b/packages/react-reconciler/src/__tests__/useMutableSource-test.internal.js index 6321168d70aa3..81c1b7bc47e9e 100644 --- a/packages/react-reconciler/src/__tests__/useMutableSource-test.internal.js +++ b/packages/react-reconciler/src/__tests__/useMutableSource-test.internal.js @@ -1558,8 +1558,15 @@ describe('useMutableSource', () => { expect(Scheduler).toFlushAndYieldThrough(['a0', 'b0']); // Mutate in an event. This schedules a subscription update on a, which // already mounted, but not b, which hasn't subscribed yet. - mutateA('a1'); - mutateB('b1'); + if (gate(flags => flags.enableSyncDefaultUpdates)) { + React.startTransition(() => { + mutateA('a1'); + mutateB('b1'); + }); + } else { + mutateA('a1'); + mutateB('b1'); + } // Mutate again at lower priority. This will schedule another subscription // update on a, but not b. When b mounts and subscriptions, the value it diff --git a/packages/use-subscription/src/__tests__/useSubscription-test.js b/packages/use-subscription/src/__tests__/useSubscription-test.js index 985312abd1d64..1d481e1d599b4 100644 --- a/packages/use-subscription/src/__tests__/useSubscription-test.js +++ b/packages/use-subscription/src/__tests__/useSubscription-test.js @@ -454,7 +454,13 @@ describe('useSubscription', () => { observableA.next('a-2'); // Update again - renderer.update(); + if (gate(flags => flags.enableSyncDefaultUpdates)) { + React.startTransition(() => { + renderer.update(); + }); + } else { + renderer.update(); + } // Flush everything and ensure that the correct subscribable is used expect(Scheduler).toFlushAndYield([