From 691096c95d1019f57e0da2c9a060c5e094b7c586 Mon Sep 17 00:00:00 2001 From: Brian Vaughn Date: Tue, 18 Feb 2020 14:19:43 -0800 Subject: [PATCH] Split recent passive effects changes into 2 flags (#18030) * Split recent passive effects changes into 2 flags Separate flags can now be used to opt passive effects into: 1) Deferring destroy functions on unmount to subsequent passive effects flush 2) Running all destroy functions (for all fibers) before create functions This allows us to test the less risky feature (2) separately from the more risky one. * deferPassiveEffectCleanupDuringUnmount is ignored unless runAllPassiveEffectDestroysBeforeCreates is true --- .../src/ReactFiberCommitWork.js | 10 +- .../src/ReactFiberWorkLoop.js | 8 +- ...eactHooksWithNoopRenderer-test.internal.js | 5132 ++++++++-------- ...tSuspenseWithNoopRenderer-test.internal.js | 5333 +++++++++-------- packages/shared/ReactFeatureFlags.js | 9 + .../forks/ReactFeatureFlags.native-fb.js | 1 + .../forks/ReactFeatureFlags.native-oss.js | 1 + .../forks/ReactFeatureFlags.persistent.js | 1 + .../forks/ReactFeatureFlags.test-renderer.js | 1 + .../ReactFeatureFlags.test-renderer.www.js | 1 + .../shared/forks/ReactFeatureFlags.testing.js | 1 + .../shared/forks/ReactFeatureFlags.www.js | 3 +- 12 files changed, 5400 insertions(+), 5101 deletions(-) diff --git a/packages/react-reconciler/src/ReactFiberCommitWork.js b/packages/react-reconciler/src/ReactFiberCommitWork.js index 9d9d220f33d52..ba682cc93f53c 100644 --- a/packages/react-reconciler/src/ReactFiberCommitWork.js +++ b/packages/react-reconciler/src/ReactFiberCommitWork.js @@ -34,6 +34,7 @@ import { enableFundamentalAPI, enableSuspenseCallback, enableScopeAPI, + runAllPassiveEffectDestroysBeforeCreates, } from 'shared/ReactFeatureFlags'; import { FunctionComponent, @@ -398,7 +399,7 @@ function commitHookEffectListMount(tag: number, finishedWork: Fiber) { } function schedulePassiveEffects(finishedWork: Fiber) { - if (deferPassiveEffectCleanupDuringUnmount) { + if (runAllPassiveEffectDestroysBeforeCreates) { const updateQueue: FunctionComponentUpdateQueue | null = (finishedWork.updateQueue: any); let lastEffect = updateQueue !== null ? updateQueue.lastEffect : null; if (lastEffect !== null) { @@ -456,7 +457,7 @@ function commitLifeCycles( // by a create function in another component during the same commit. commitHookEffectListMount(HookLayout | HookHasEffect, finishedWork); - if (deferPassiveEffectCleanupDuringUnmount) { + if (runAllPassiveEffectDestroysBeforeCreates) { schedulePassiveEffects(finishedWork); } return; @@ -795,7 +796,10 @@ function commitUnmount( if (lastEffect !== null) { const firstEffect = lastEffect.next; - if (deferPassiveEffectCleanupDuringUnmount) { + if ( + deferPassiveEffectCleanupDuringUnmount && + runAllPassiveEffectDestroysBeforeCreates + ) { let effect = firstEffect; do { const {destroy, tag} = effect; diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.js b/packages/react-reconciler/src/ReactFiberWorkLoop.js index f647ccb88b2ce..116bb2e99cf60 100644 --- a/packages/react-reconciler/src/ReactFiberWorkLoop.js +++ b/packages/react-reconciler/src/ReactFiberWorkLoop.js @@ -18,7 +18,7 @@ import type {Effect as HookEffect} from './ReactFiberHooks'; import { warnAboutDeprecatedLifecycles, - deferPassiveEffectCleanupDuringUnmount, + runAllPassiveEffectDestroysBeforeCreates, enableUserTimingAPI, enableSuspenseServerRenderer, replayFailedUnitOfWorkWithInvokeGuardedCallback, @@ -2174,7 +2174,7 @@ export function enqueuePendingPassiveHookEffectMount( fiber: Fiber, effect: HookEffect, ): void { - if (deferPassiveEffectCleanupDuringUnmount) { + if (runAllPassiveEffectDestroysBeforeCreates) { pendingPassiveHookEffectsMount.push(effect, fiber); if (!rootDoesHavePassiveEffects) { rootDoesHavePassiveEffects = true; @@ -2190,7 +2190,7 @@ export function enqueuePendingPassiveHookEffectUnmount( fiber: Fiber, effect: HookEffect, ): void { - if (deferPassiveEffectCleanupDuringUnmount) { + if (runAllPassiveEffectDestroysBeforeCreates) { pendingPassiveHookEffectsUnmount.push(effect, fiber); if (!rootDoesHavePassiveEffects) { rootDoesHavePassiveEffects = true; @@ -2224,7 +2224,7 @@ function flushPassiveEffectsImpl() { executionContext |= CommitContext; const prevInteractions = pushInteractions(root); - if (deferPassiveEffectCleanupDuringUnmount) { + if (runAllPassiveEffectDestroysBeforeCreates) { // It's important that ALL pending passive effect destroy functions are called // before ANY passive effect create functions are called. // Otherwise effects in sibling components might interfere with each other. diff --git a/packages/react-reconciler/src/__tests__/ReactHooksWithNoopRenderer-test.internal.js b/packages/react-reconciler/src/__tests__/ReactHooksWithNoopRenderer-test.internal.js index 438a748fb75a4..6e50579addd31 100644 --- a/packages/react-reconciler/src/__tests__/ReactHooksWithNoopRenderer-test.internal.js +++ b/packages/react-reconciler/src/__tests__/ReactHooksWithNoopRenderer-test.internal.js @@ -34,2759 +34,2953 @@ let forwardRef; let memo; let act; -describe('ReactHooksWithNoopRenderer', () => { - beforeEach(() => { - jest.resetModules(); - jest.useFakeTimers(); - - ReactFeatureFlags = require('shared/ReactFeatureFlags'); - ReactFeatureFlags.debugRenderPhaseSideEffectsForStrictMode = false; - ReactFeatureFlags.enableSchedulerTracing = true; - ReactFeatureFlags.flushSuspenseFallbacksInTests = false; - ReactFeatureFlags.deferPassiveEffectCleanupDuringUnmount = true; - React = require('react'); - ReactNoop = require('react-noop-renderer'); - Scheduler = require('scheduler'); - SchedulerTracing = require('scheduler/tracing'); - ReactCache = require('react-cache'); - useState = React.useState; - useReducer = React.useReducer; - useEffect = React.useEffect; - useLayoutEffect = React.useLayoutEffect; - useCallback = React.useCallback; - useMemo = React.useMemo; - useRef = React.useRef; - useImperativeHandle = React.useImperativeHandle; - forwardRef = React.forwardRef; - memo = React.memo; - useTransition = React.useTransition; - useDeferredValue = React.useDeferredValue; - Suspense = React.Suspense; - act = ReactNoop.act; - - TextResource = ReactCache.unstable_createResource( - ([text, ms = 0]) => { - return new Promise((resolve, reject) => - setTimeout(() => { - Scheduler.unstable_yieldValue(`Promise resolved [${text}]`); - resolve(text); - }, ms), +function loadModules({ + deferPassiveEffectCleanupDuringUnmount, + runAllPassiveEffectDestroysBeforeCreates, +}) { + ReactFeatureFlags = require('shared/ReactFeatureFlags'); + ReactFeatureFlags.debugRenderPhaseSideEffectsForStrictMode = false; + ReactFeatureFlags.enableSchedulerTracing = true; + ReactFeatureFlags.flushSuspenseFallbacksInTests = false; + ReactFeatureFlags.deferPassiveEffectCleanupDuringUnmount = deferPassiveEffectCleanupDuringUnmount; + ReactFeatureFlags.runAllPassiveEffectDestroysBeforeCreates = runAllPassiveEffectDestroysBeforeCreates; + React = require('react'); + ReactNoop = require('react-noop-renderer'); + Scheduler = require('scheduler'); + SchedulerTracing = require('scheduler/tracing'); + ReactCache = require('react-cache'); + useState = React.useState; + useReducer = React.useReducer; + useEffect = React.useEffect; + useLayoutEffect = React.useLayoutEffect; + useCallback = React.useCallback; + useMemo = React.useMemo; + useRef = React.useRef; + useImperativeHandle = React.useImperativeHandle; + forwardRef = React.forwardRef; + memo = React.memo; + useTransition = React.useTransition; + useDeferredValue = React.useDeferredValue; + Suspense = React.Suspense; + act = ReactNoop.act; +} + +[true, false].forEach(deferPassiveEffectCleanupDuringUnmount => { + [true, false].forEach(runAllPassiveEffectDestroysBeforeCreates => { + describe(`ReactHooksWithNoopRenderer deferPassiveEffectCleanupDuringUnmount:${deferPassiveEffectCleanupDuringUnmount} runAllPassiveEffectDestroysBeforeCreates:${runAllPassiveEffectDestroysBeforeCreates}`, () => { + beforeEach(() => { + jest.resetModules(); + jest.useFakeTimers(); + + loadModules({ + deferPassiveEffectCleanupDuringUnmount, + runAllPassiveEffectDestroysBeforeCreates, + }); + + TextResource = ReactCache.unstable_createResource( + ([text, ms = 0]) => { + return new Promise((resolve, reject) => + setTimeout(() => { + Scheduler.unstable_yieldValue(`Promise resolved [${text}]`); + resolve(text); + }, ms), + ); + }, + ([text, ms]) => text, ); - }, - ([text, ms]) => text, - ); - }); + }); - function span(prop) { - return {type: 'span', hidden: false, children: [], prop}; - } - - function hiddenSpan(prop) { - return {type: 'span', children: [], prop, hidden: true}; - } - - function Text(props) { - Scheduler.unstable_yieldValue(props.text); - return ; - } - - function AsyncText(props) { - const text = props.text; - try { - TextResource.read([props.text, props.ms]); - Scheduler.unstable_yieldValue(text); - return ; - } catch (promise) { - if (typeof promise.then === 'function') { - Scheduler.unstable_yieldValue(`Suspend! [${text}]`); - } else { - Scheduler.unstable_yieldValue(`Error! [${text}]`); + function span(prop) { + return {type: 'span', hidden: false, children: [], prop}; } - throw promise; - } - } - - function advanceTimers(ms) { - // Note: This advances Jest's virtual time but not React's. Use - // ReactNoop.expire for that. - if (typeof ms !== 'number') { - throw new Error('Must specify ms'); - } - jest.advanceTimersByTime(ms); - // Wait until the end of the current tick - // We cannot use a timer since we're faking them - return Promise.resolve().then(() => {}); - } - - it('resumes after an interruption', () => { - function Counter(props, ref) { - const [count, updateCount] = useState(0); - useImperativeHandle(ref, () => ({updateCount})); - return ; - } - Counter = forwardRef(Counter); - - // Initial mount - const counter = React.createRef(null); - ReactNoop.render(); - expect(Scheduler).toFlushAndYield(['Count: 0']); - expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]); - - // Schedule some updates - ReactNoop.batchedUpdates(() => { - counter.current.updateCount(1); - counter.current.updateCount(count => count + 10); - }); - // Partially flush without committing - expect(Scheduler).toFlushAndYieldThrough(['Count: 11']); - expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]); + function hiddenSpan(prop) { + return {type: 'span', children: [], prop, hidden: true}; + } - // Interrupt with a high priority update - ReactNoop.flushSync(() => { - ReactNoop.render(); - }); - expect(Scheduler).toHaveYielded(['Total: 0']); + function Text(props) { + Scheduler.unstable_yieldValue(props.text); + return ; + } - // Resume rendering - expect(Scheduler).toFlushAndYield(['Total: 11']); - expect(ReactNoop.getChildren()).toEqual([span('Total: 11')]); - }); + function AsyncText(props) { + const text = props.text; + try { + TextResource.read([props.text, props.ms]); + Scheduler.unstable_yieldValue(text); + return ; + } catch (promise) { + if (typeof promise.then === 'function') { + Scheduler.unstable_yieldValue(`Suspend! [${text}]`); + } else { + Scheduler.unstable_yieldValue(`Error! [${text}]`); + } + throw promise; + } + } - it('throws inside class components', () => { - class BadCounter extends React.Component { - render() { - const [count] = useState(0); - return ; + function advanceTimers(ms) { + // Note: This advances Jest's virtual time but not React's. Use + // ReactNoop.expire for that. + if (typeof ms !== 'number') { + throw new Error('Must specify ms'); + } + jest.advanceTimersByTime(ms); + // Wait until the end of the current tick + // We cannot use a timer since we're faking them + return Promise.resolve().then(() => {}); } - } - ReactNoop.render(); - - expect(Scheduler).toFlushAndThrow( - 'Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for' + - ' one of the following reasons:\n' + - '1. You might have mismatching versions of React and the renderer (such as React DOM)\n' + - '2. You might be breaking the Rules of Hooks\n' + - '3. You might have more than one copy of React in the same app\n' + - 'See https://fb.me/react-invalid-hook-call for tips about how to debug and fix this problem.', - ); - - // Confirm that a subsequent hook works properly. - function GoodCounter(props, ref) { - const [count] = useState(props.initialCount); - return ; - } - ReactNoop.render(); - expect(Scheduler).toFlushAndYield([10]); - }); - it('throws inside module-style components', () => { - function Counter() { - return { - render() { - const [count] = useState(0); - return ; - }, - }; - } - ReactNoop.render(); - expect(() => - expect(Scheduler).toFlushAndThrow( - 'Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen ' + - 'for one of the following reasons:\n' + - '1. You might have mismatching versions of React and the renderer (such as React DOM)\n' + - '2. You might be breaking the Rules of Hooks\n' + - '3. You might have more than one copy of React in the same app\n' + - 'See https://fb.me/react-invalid-hook-call for tips about how to debug and fix this problem.', - ), - ).toErrorDev( - 'Warning: The component appears to be a function component that returns a class instance. ' + - 'Change Counter to a class that extends React.Component instead. ' + - "If you can't use a class try assigning the prototype on the function as a workaround. " + - '`Counter.prototype = React.Component.prototype`. ' + - "Don't use an arrow function since it cannot be called with `new` by React.", - ); - - // Confirm that a subsequent hook works properly. - function GoodCounter(props) { - const [count] = useState(props.initialCount); - return ; - } - ReactNoop.render(); - expect(Scheduler).toFlushAndYield([10]); - }); + it('resumes after an interruption', () => { + function Counter(props, ref) { + const [count, updateCount] = useState(0); + useImperativeHandle(ref, () => ({updateCount})); + return ; + } + Counter = forwardRef(Counter); - it('throws when called outside the render phase', () => { - expect(() => useState(0)).toThrow( - 'Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for' + - ' one of the following reasons:\n' + - '1. You might have mismatching versions of React and the renderer (such as React DOM)\n' + - '2. You might be breaking the Rules of Hooks\n' + - '3. You might have more than one copy of React in the same app\n' + - 'See https://fb.me/react-invalid-hook-call for tips about how to debug and fix this problem.', - ); - }); + // Initial mount + const counter = React.createRef(null); + ReactNoop.render(); + expect(Scheduler).toFlushAndYield(['Count: 0']); + expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]); - describe('useState', () => { - it('simple mount and update', () => { - function Counter(props, ref) { - const [count, updateCount] = useState(0); - useImperativeHandle(ref, () => ({updateCount})); - return ; - } - Counter = forwardRef(Counter); - const counter = React.createRef(null); - ReactNoop.render(); - expect(Scheduler).toFlushAndYield(['Count: 0']); - expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]); - - act(() => counter.current.updateCount(1)); - expect(Scheduler).toHaveYielded(['Count: 1']); - expect(ReactNoop.getChildren()).toEqual([span('Count: 1')]); - - act(() => counter.current.updateCount(count => count + 10)); - expect(Scheduler).toHaveYielded(['Count: 11']); - expect(ReactNoop.getChildren()).toEqual([span('Count: 11')]); - }); + // Schedule some updates + ReactNoop.batchedUpdates(() => { + counter.current.updateCount(1); + counter.current.updateCount(count => count + 10); + }); + + // Partially flush without committing + expect(Scheduler).toFlushAndYieldThrough(['Count: 11']); + expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]); - it('lazy state initializer', () => { - function Counter(props, ref) { - const [count, updateCount] = useState(() => { - Scheduler.unstable_yieldValue('getInitialState'); - return props.initialState; + // Interrupt with a high priority update + ReactNoop.flushSync(() => { + ReactNoop.render(); }); - useImperativeHandle(ref, () => ({updateCount})); - return ; - } - Counter = forwardRef(Counter); - const counter = React.createRef(null); - ReactNoop.render(); - expect(Scheduler).toFlushAndYield(['getInitialState', 'Count: 42']); - expect(ReactNoop.getChildren()).toEqual([span('Count: 42')]); - - act(() => counter.current.updateCount(7)); - expect(Scheduler).toHaveYielded(['Count: 7']); - expect(ReactNoop.getChildren()).toEqual([span('Count: 7')]); - }); + expect(Scheduler).toHaveYielded(['Total: 0']); - it('multiple states', () => { - function Counter(props, ref) { - const [count, updateCount] = useState(0); - const [label, updateLabel] = useState('Count'); - useImperativeHandle(ref, () => ({updateCount, updateLabel})); - return ; - } - Counter = forwardRef(Counter); - const counter = React.createRef(null); - ReactNoop.render(); - expect(Scheduler).toFlushAndYield(['Count: 0']); - expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]); + // Resume rendering + expect(Scheduler).toFlushAndYield(['Total: 11']); + expect(ReactNoop.getChildren()).toEqual([span('Total: 11')]); + }); - act(() => counter.current.updateCount(7)); - expect(Scheduler).toHaveYielded(['Count: 7']); + it('throws inside class components', () => { + class BadCounter extends React.Component { + render() { + const [count] = useState(0); + return ; + } + } + ReactNoop.render(); + + expect(Scheduler).toFlushAndThrow( + 'Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for' + + ' one of the following reasons:\n' + + '1. You might have mismatching versions of React and the renderer (such as React DOM)\n' + + '2. You might be breaking the Rules of Hooks\n' + + '3. You might have more than one copy of React in the same app\n' + + 'See https://fb.me/react-invalid-hook-call for tips about how to debug and fix this problem.', + ); - act(() => counter.current.updateLabel('Total')); - expect(Scheduler).toHaveYielded(['Total: 7']); - }); + // Confirm that a subsequent hook works properly. + function GoodCounter(props, ref) { + const [count] = useState(props.initialCount); + return ; + } + ReactNoop.render(); + expect(Scheduler).toFlushAndYield([10]); + }); - it('returns the same updater function every time', () => { - let updaters = []; - function Counter() { - const [count, updateCount] = useState(0); - updaters.push(updateCount); - return ; - } - ReactNoop.render(); - expect(Scheduler).toFlushAndYield(['Count: 0']); - expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]); + it('throws inside module-style components', () => { + function Counter() { + return { + render() { + const [count] = useState(0); + return ; + }, + }; + } + ReactNoop.render(); + expect(() => + expect(Scheduler).toFlushAndThrow( + 'Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen ' + + 'for one of the following reasons:\n' + + '1. You might have mismatching versions of React and the renderer (such as React DOM)\n' + + '2. You might be breaking the Rules of Hooks\n' + + '3. You might have more than one copy of React in the same app\n' + + 'See https://fb.me/react-invalid-hook-call for tips about how to debug and fix this problem.', + ), + ).toErrorDev( + 'Warning: The component appears to be a function component that returns a class instance. ' + + 'Change Counter to a class that extends React.Component instead. ' + + "If you can't use a class try assigning the prototype on the function as a workaround. " + + '`Counter.prototype = React.Component.prototype`. ' + + "Don't use an arrow function since it cannot be called with `new` by React.", + ); - act(() => updaters[0](1)); - expect(Scheduler).toHaveYielded(['Count: 1']); - expect(ReactNoop.getChildren()).toEqual([span('Count: 1')]); + // Confirm that a subsequent hook works properly. + function GoodCounter(props) { + const [count] = useState(props.initialCount); + return ; + } + ReactNoop.render(); + expect(Scheduler).toFlushAndYield([10]); + }); - act(() => updaters[0](count => count + 10)); - expect(Scheduler).toHaveYielded(['Count: 11']); - expect(ReactNoop.getChildren()).toEqual([span('Count: 11')]); + it('throws when called outside the render phase', () => { + expect(() => useState(0)).toThrow( + 'Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for' + + ' one of the following reasons:\n' + + '1. You might have mismatching versions of React and the renderer (such as React DOM)\n' + + '2. You might be breaking the Rules of Hooks\n' + + '3. You might have more than one copy of React in the same app\n' + + 'See https://fb.me/react-invalid-hook-call for tips about how to debug and fix this problem.', + ); + }); - expect(updaters).toEqual([updaters[0], updaters[0], updaters[0]]); - }); + describe('useState', () => { + it('simple mount and update', () => { + function Counter(props, ref) { + const [count, updateCount] = useState(0); + useImperativeHandle(ref, () => ({updateCount})); + return ; + } + Counter = forwardRef(Counter); + const counter = React.createRef(null); + ReactNoop.render(); + expect(Scheduler).toFlushAndYield(['Count: 0']); + expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]); + + act(() => counter.current.updateCount(1)); + expect(Scheduler).toHaveYielded(['Count: 1']); + expect(ReactNoop.getChildren()).toEqual([span('Count: 1')]); + + act(() => counter.current.updateCount(count => count + 10)); + expect(Scheduler).toHaveYielded(['Count: 11']); + expect(ReactNoop.getChildren()).toEqual([span('Count: 11')]); + }); - it('warns on set after unmount', () => { - let _updateCount; - function Counter(props, ref) { - const [, updateCount] = useState(0); - _updateCount = updateCount; - return null; - } + it('lazy state initializer', () => { + function Counter(props, ref) { + const [count, updateCount] = useState(() => { + Scheduler.unstable_yieldValue('getInitialState'); + return props.initialState; + }); + useImperativeHandle(ref, () => ({updateCount})); + return ; + } + Counter = forwardRef(Counter); + const counter = React.createRef(null); + ReactNoop.render(); + expect(Scheduler).toFlushAndYield(['getInitialState', 'Count: 42']); + expect(ReactNoop.getChildren()).toEqual([span('Count: 42')]); + + act(() => counter.current.updateCount(7)); + expect(Scheduler).toHaveYielded(['Count: 7']); + expect(ReactNoop.getChildren()).toEqual([span('Count: 7')]); + }); - ReactNoop.render(); - expect(Scheduler).toFlushWithoutYielding(); - ReactNoop.render(null); - expect(Scheduler).toFlushWithoutYielding(); - expect(() => act(() => _updateCount(1))).toErrorDev( - "Warning: Can't perform a React state update on an unmounted " + - 'component. This is a no-op, but it indicates a memory leak in your ' + - 'application. To fix, cancel all subscriptions and asynchronous ' + - 'tasks in a useEffect cleanup function.\n' + - ' in Counter (at **)', - ); - }); + it('multiple states', () => { + function Counter(props, ref) { + const [count, updateCount] = useState(0); + const [label, updateLabel] = useState('Count'); + useImperativeHandle(ref, () => ({updateCount, updateLabel})); + return ; + } + Counter = forwardRef(Counter); + const counter = React.createRef(null); + ReactNoop.render(); + expect(Scheduler).toFlushAndYield(['Count: 0']); + expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]); - it('works with memo', () => { - let _updateCount; - function Counter(props) { - const [count, updateCount] = useState(0); - _updateCount = updateCount; - return ; - } - Counter = memo(Counter); + act(() => counter.current.updateCount(7)); + expect(Scheduler).toHaveYielded(['Count: 7']); - ReactNoop.render(); - expect(Scheduler).toFlushAndYield(['Count: 0']); - expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]); + act(() => counter.current.updateLabel('Total')); + expect(Scheduler).toHaveYielded(['Total: 7']); + }); - ReactNoop.render(); - expect(Scheduler).toFlushAndYield([]); - expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]); + it('returns the same updater function every time', () => { + let updaters = []; + function Counter() { + const [count, updateCount] = useState(0); + updaters.push(updateCount); + return ; + } + ReactNoop.render(); + expect(Scheduler).toFlushAndYield(['Count: 0']); + expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]); - act(() => _updateCount(1)); - expect(Scheduler).toHaveYielded(['Count: 1']); - expect(ReactNoop.getChildren()).toEqual([span('Count: 1')]); - }); - }); + act(() => updaters[0](1)); + expect(Scheduler).toHaveYielded(['Count: 1']); + expect(ReactNoop.getChildren()).toEqual([span('Count: 1')]); - describe('updates during the render phase', () => { - it('restarts the render function and applies the new updates on top', () => { - function ScrollView({row: newRow}) { - let [isScrollingDown, setIsScrollingDown] = useState(false); - let [row, setRow] = useState(null); + act(() => updaters[0](count => count + 10)); + expect(Scheduler).toHaveYielded(['Count: 11']); + expect(ReactNoop.getChildren()).toEqual([span('Count: 11')]); - if (row !== newRow) { - // Row changed since last render. Update isScrollingDown. - setIsScrollingDown(row !== null && newRow > row); - setRow(newRow); - } + expect(updaters).toEqual([updaters[0], updaters[0], updaters[0]]); + }); - return ; - } + it('warns on set after unmount', () => { + let _updateCount; + function Counter(props, ref) { + const [, updateCount] = useState(0); + _updateCount = updateCount; + return null; + } - ReactNoop.render(); - expect(Scheduler).toFlushAndYield(['Scrolling down: false']); - expect(ReactNoop.getChildren()).toEqual([span('Scrolling down: false')]); + ReactNoop.render(); + expect(Scheduler).toFlushWithoutYielding(); + ReactNoop.render(null); + expect(Scheduler).toFlushWithoutYielding(); + expect(() => act(() => _updateCount(1))).toErrorDev( + "Warning: Can't perform a React state update on an unmounted " + + 'component. This is a no-op, but it indicates a memory leak in your ' + + 'application. To fix, cancel all subscriptions and asynchronous ' + + 'tasks in a useEffect cleanup function.\n' + + ' in Counter (at **)', + ); + }); - ReactNoop.render(); - expect(Scheduler).toFlushAndYield(['Scrolling down: true']); - expect(ReactNoop.getChildren()).toEqual([span('Scrolling down: true')]); + it('works with memo', () => { + let _updateCount; + function Counter(props) { + const [count, updateCount] = useState(0); + _updateCount = updateCount; + return ; + } + Counter = memo(Counter); - ReactNoop.render(); - expect(Scheduler).toFlushAndYield(['Scrolling down: true']); - expect(ReactNoop.getChildren()).toEqual([span('Scrolling down: true')]); + ReactNoop.render(); + expect(Scheduler).toFlushAndYield(['Count: 0']); + expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]); - ReactNoop.render(); - expect(Scheduler).toFlushAndYield(['Scrolling down: true']); - expect(ReactNoop.getChildren()).toEqual([span('Scrolling down: true')]); + ReactNoop.render(); + expect(Scheduler).toFlushAndYield([]); + expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]); - ReactNoop.render(); - expect(Scheduler).toFlushAndYield(['Scrolling down: false']); - expect(ReactNoop.getChildren()).toEqual([span('Scrolling down: false')]); + act(() => _updateCount(1)); + expect(Scheduler).toHaveYielded(['Count: 1']); + expect(ReactNoop.getChildren()).toEqual([span('Count: 1')]); + }); + }); - ReactNoop.render(); - expect(Scheduler).toFlushAndYield(['Scrolling down: false']); - expect(ReactNoop.getChildren()).toEqual([span('Scrolling down: false')]); - }); + describe('updates during the render phase', () => { + it('restarts the render function and applies the new updates on top', () => { + function ScrollView({row: newRow}) { + let [isScrollingDown, setIsScrollingDown] = useState(false); + let [row, setRow] = useState(null); - it('keeps restarting until there are no more new updates', () => { - function Counter({row: newRow}) { - let [count, setCount] = useState(0); - if (count < 3) { - setCount(count + 1); - } - Scheduler.unstable_yieldValue('Render: ' + count); - return ; - } + if (row !== newRow) { + // Row changed since last render. Update isScrollingDown. + setIsScrollingDown(row !== null && newRow > row); + setRow(newRow); + } - ReactNoop.render(); - expect(Scheduler).toFlushAndYield([ - 'Render: 0', - 'Render: 1', - 'Render: 2', - 'Render: 3', - 3, - ]); - expect(ReactNoop.getChildren()).toEqual([span(3)]); - }); + return ; + } - it('updates multiple times within same render function', () => { - function Counter({row: newRow}) { - let [count, setCount] = useState(0); - if (count < 12) { - setCount(c => c + 1); - setCount(c => c + 1); - setCount(c => c + 1); - } - Scheduler.unstable_yieldValue('Render: ' + count); - return ; - } + ReactNoop.render(); + expect(Scheduler).toFlushAndYield(['Scrolling down: false']); + expect(ReactNoop.getChildren()).toEqual([ + span('Scrolling down: false'), + ]); - ReactNoop.render(); - expect(Scheduler).toFlushAndYield([ - // Should increase by three each time - 'Render: 0', - 'Render: 3', - 'Render: 6', - 'Render: 9', - 'Render: 12', - 12, - ]); - expect(ReactNoop.getChildren()).toEqual([span(12)]); - }); + ReactNoop.render(); + expect(Scheduler).toFlushAndYield(['Scrolling down: true']); + expect(ReactNoop.getChildren()).toEqual([ + span('Scrolling down: true'), + ]); - it('throws after too many iterations', () => { - function Counter({row: newRow}) { - let [count, setCount] = useState(0); - setCount(count + 1); - Scheduler.unstable_yieldValue('Render: ' + count); - return ; - } - ReactNoop.render(); - expect(Scheduler).toFlushAndThrow( - 'Too many re-renders. React limits the number of renders to prevent ' + - 'an infinite loop.', - ); - }); + ReactNoop.render(); + expect(Scheduler).toFlushAndYield(['Scrolling down: true']); + expect(ReactNoop.getChildren()).toEqual([ + span('Scrolling down: true'), + ]); - it('works with useReducer', () => { - function reducer(state, action) { - return action === 'increment' ? state + 1 : state; - } - function Counter({row: newRow}) { - let [count, dispatch] = useReducer(reducer, 0); - if (count < 3) { - dispatch('increment'); - } - Scheduler.unstable_yieldValue('Render: ' + count); - return ; - } + ReactNoop.render(); + expect(Scheduler).toFlushAndYield(['Scrolling down: true']); + expect(ReactNoop.getChildren()).toEqual([ + span('Scrolling down: true'), + ]); - ReactNoop.render(); - expect(Scheduler).toFlushAndYield([ - 'Render: 0', - 'Render: 1', - 'Render: 2', - 'Render: 3', - 3, - ]); - expect(ReactNoop.getChildren()).toEqual([span(3)]); - }); + ReactNoop.render(); + expect(Scheduler).toFlushAndYield(['Scrolling down: false']); + expect(ReactNoop.getChildren()).toEqual([ + span('Scrolling down: false'), + ]); - it('uses reducer passed at time of render, not time of dispatch', () => { - // This test is a bit contrived but it demonstrates a subtle edge case. + ReactNoop.render(); + expect(Scheduler).toFlushAndYield(['Scrolling down: false']); + expect(ReactNoop.getChildren()).toEqual([ + span('Scrolling down: false'), + ]); + }); - // Reducer A increments by 1. Reducer B increments by 10. - function reducerA(state, action) { - switch (action) { - case 'increment': - return state + 1; - case 'reset': - return 0; - } - } - function reducerB(state, action) { - switch (action) { - case 'increment': - return state + 10; - case 'reset': - return 0; - } - } + it('keeps restarting until there are no more new updates', () => { + function Counter({row: newRow}) { + let [count, setCount] = useState(0); + if (count < 3) { + setCount(count + 1); + } + Scheduler.unstable_yieldValue('Render: ' + count); + return ; + } - function Counter({row: newRow}, ref) { - let [reducer, setReducer] = useState(() => reducerA); - let [count, dispatch] = useReducer(reducer, 0); - useImperativeHandle(ref, () => ({dispatch})); - if (count < 20) { - dispatch('increment'); - // Swap reducers each time we increment - if (reducer === reducerA) { - setReducer(() => reducerB); - } else { - setReducer(() => reducerA); + ReactNoop.render(); + expect(Scheduler).toFlushAndYield([ + 'Render: 0', + 'Render: 1', + 'Render: 2', + 'Render: 3', + 3, + ]); + expect(ReactNoop.getChildren()).toEqual([span(3)]); + }); + + it('updates multiple times within same render function', () => { + function Counter({row: newRow}) { + let [count, setCount] = useState(0); + if (count < 12) { + setCount(c => c + 1); + setCount(c => c + 1); + setCount(c => c + 1); + } + Scheduler.unstable_yieldValue('Render: ' + count); + return ; } - } - Scheduler.unstable_yieldValue('Render: ' + count); - return ; - } - Counter = forwardRef(Counter); - const counter = React.createRef(null); - ReactNoop.render(); - expect(Scheduler).toFlushAndYield([ - // The count should increase by alternating amounts of 10 and 1 - // until we reach 21. - 'Render: 0', - 'Render: 10', - 'Render: 11', - 'Render: 21', - 21, - ]); - expect(ReactNoop.getChildren()).toEqual([span(21)]); - - // Test that it works on update, too. This time the log is a bit different - // because we started with reducerB instead of reducerA. - ReactNoop.act(() => { - counter.current.dispatch('reset'); - }); - ReactNoop.render(); - expect(Scheduler).toHaveYielded([ - 'Render: 0', - 'Render: 1', - 'Render: 11', - 'Render: 12', - 'Render: 22', - 22, - ]); - expect(ReactNoop.getChildren()).toEqual([span(22)]); - }); - it('discards render phase updates if something suspends', () => { - const thenable = {then() {}}; - function Foo({signal}) { - return ( - - - - ); - } + ReactNoop.render(); + expect(Scheduler).toFlushAndYield([ + // Should increase by three each time + 'Render: 0', + 'Render: 3', + 'Render: 6', + 'Render: 9', + 'Render: 12', + 12, + ]); + expect(ReactNoop.getChildren()).toEqual([span(12)]); + }); - function Bar({signal: newSignal}) { - let [counter, setCounter] = useState(0); - let [signal, setSignal] = useState(true); + it('throws after too many iterations', () => { + function Counter({row: newRow}) { + let [count, setCount] = useState(0); + setCount(count + 1); + Scheduler.unstable_yieldValue('Render: ' + count); + return ; + } + ReactNoop.render(); + expect(Scheduler).toFlushAndThrow( + 'Too many re-renders. React limits the number of renders to prevent ' + + 'an infinite loop.', + ); + }); - // Increment a counter every time the signal changes - if (signal !== newSignal) { - setCounter(c => c + 1); - setSignal(newSignal); - if (counter === 0) { - // We're suspending during a render that includes render phase - // updates. Those updates should not persist to the next render. - Scheduler.unstable_yieldValue('Suspend!'); - throw thenable; + it('works with useReducer', () => { + function reducer(state, action) { + return action === 'increment' ? state + 1 : state; + } + function Counter({row: newRow}) { + let [count, dispatch] = useReducer(reducer, 0); + if (count < 3) { + dispatch('increment'); + } + Scheduler.unstable_yieldValue('Render: ' + count); + return ; } - } - return ; - } + ReactNoop.render(); + expect(Scheduler).toFlushAndYield([ + 'Render: 0', + 'Render: 1', + 'Render: 2', + 'Render: 3', + 3, + ]); + expect(ReactNoop.getChildren()).toEqual([span(3)]); + }); - const root = ReactNoop.createRoot(); - root.render(); + it('uses reducer passed at time of render, not time of dispatch', () => { + // This test is a bit contrived but it demonstrates a subtle edge case. + + // Reducer A increments by 1. Reducer B increments by 10. + function reducerA(state, action) { + switch (action) { + case 'increment': + return state + 1; + case 'reset': + return 0; + } + } + function reducerB(state, action) { + switch (action) { + case 'increment': + return state + 10; + case 'reset': + return 0; + } + } - expect(Scheduler).toFlushAndYield([0]); - expect(root).toMatchRenderedOutput(); + function Counter({row: newRow}, ref) { + let [reducer, setReducer] = useState(() => reducerA); + let [count, dispatch] = useReducer(reducer, 0); + useImperativeHandle(ref, () => ({dispatch})); + if (count < 20) { + dispatch('increment'); + // Swap reducers each time we increment + if (reducer === reducerA) { + setReducer(() => reducerB); + } else { + setReducer(() => reducerA); + } + } + Scheduler.unstable_yieldValue('Render: ' + count); + return ; + } + Counter = forwardRef(Counter); + const counter = React.createRef(null); + ReactNoop.render(); + expect(Scheduler).toFlushAndYield([ + // The count should increase by alternating amounts of 10 and 1 + // until we reach 21. + 'Render: 0', + 'Render: 10', + 'Render: 11', + 'Render: 21', + 21, + ]); + expect(ReactNoop.getChildren()).toEqual([span(21)]); - root.render(); - expect(Scheduler).toFlushAndYield(['Suspend!']); - expect(root).toMatchRenderedOutput(); + // Test that it works on update, too. This time the log is a bit different + // because we started with reducerB instead of reducerA. + ReactNoop.act(() => { + counter.current.dispatch('reset'); + }); + ReactNoop.render(); + expect(Scheduler).toHaveYielded([ + 'Render: 0', + 'Render: 1', + 'Render: 11', + 'Render: 12', + 'Render: 22', + 22, + ]); + expect(ReactNoop.getChildren()).toEqual([span(22)]); + }); - // Rendering again should suspend again. - root.render(); - expect(Scheduler).toFlushAndYield(['Suspend!']); - }); + it('discards render phase updates if something suspends', () => { + const thenable = {then() {}}; + function Foo({signal}) { + return ( + + + + ); + } - it('discards render phase updates if something suspends, but not other updates in the same component', async () => { - const thenable = {then() {}}; - function Foo({signal}) { - return ( - - - - ); - } + function Bar({signal: newSignal}) { + let [counter, setCounter] = useState(0); + let [signal, setSignal] = useState(true); + + // Increment a counter every time the signal changes + if (signal !== newSignal) { + setCounter(c => c + 1); + setSignal(newSignal); + if (counter === 0) { + // We're suspending during a render that includes render phase + // updates. Those updates should not persist to the next render. + Scheduler.unstable_yieldValue('Suspend!'); + throw thenable; + } + } + + return ; + } - let setLabel; - function Bar({signal: newSignal}) { - let [counter, setCounter] = useState(0); + const root = ReactNoop.createRoot(); + root.render(); - if (counter === 1) { - // We're suspending during a render that includes render phase - // updates. Those updates should not persist to the next render. - Scheduler.unstable_yieldValue('Suspend!'); - throw thenable; - } + expect(Scheduler).toFlushAndYield([0]); + expect(root).toMatchRenderedOutput(); - let [signal, setSignal] = useState(true); + root.render(); + expect(Scheduler).toFlushAndYield(['Suspend!']); + expect(root).toMatchRenderedOutput(); - // Increment a counter every time the signal changes - if (signal !== newSignal) { - setCounter(c => c + 1); - setSignal(newSignal); - } + // Rendering again should suspend again. + root.render(); + expect(Scheduler).toFlushAndYield(['Suspend!']); + }); - let [label, _setLabel] = useState('A'); - setLabel = _setLabel; + it('discards render phase updates if something suspends, but not other updates in the same component', async () => { + const thenable = {then() {}}; + function Foo({signal}) { + return ( + + + + ); + } - return ; - } + let setLabel; + function Bar({signal: newSignal}) { + let [counter, setCounter] = useState(0); - const root = ReactNoop.createRoot(); - root.render(); + if (counter === 1) { + // We're suspending during a render that includes render phase + // updates. Those updates should not persist to the next render. + Scheduler.unstable_yieldValue('Suspend!'); + throw thenable; + } - expect(Scheduler).toFlushAndYield(['A:0']); - expect(root).toMatchRenderedOutput(); + let [signal, setSignal] = useState(true); - await ReactNoop.act(async () => { - root.render(); - setLabel('B'); - }); - expect(Scheduler).toHaveYielded(['Suspend!']); - expect(root).toMatchRenderedOutput(); - - // Rendering again should suspend again. - root.render(); - expect(Scheduler).toFlushAndYield(['Suspend!']); - - // Flip the signal back to "cancel" the update. However, the update to - // label should still proceed. It shouldn't have been dropped. - root.render(); - expect(Scheduler).toFlushAndYield(['B:0']); - expect(root).toMatchRenderedOutput(); - }); + // Increment a counter every time the signal changes + if (signal !== newSignal) { + setCounter(c => c + 1); + setSignal(newSignal); + } - // TODO: This should probably warn - it.experimental('calling startTransition inside render phase', async () => { - let startTransition; - function App() { - let [counter, setCounter] = useState(0); - let [_startTransition] = useTransition(); - startTransition = _startTransition; - - if (counter === 0) { - startTransition(() => { - setCounter(c => c + 1); - }); - } + let [label, _setLabel] = useState('A'); + setLabel = _setLabel; - return ; - } + return ; + } - const root = ReactNoop.createRoot(); - root.render(); - expect(Scheduler).toFlushAndYield([1]); - expect(root).toMatchRenderedOutput(); - }); - }); + const root = ReactNoop.createRoot(); + root.render(); - describe('useReducer', () => { - it('simple mount and update', () => { - const INCREMENT = 'INCREMENT'; - const DECREMENT = 'DECREMENT'; - - function reducer(state, action) { - switch (action) { - case 'INCREMENT': - return state + 1; - case 'DECREMENT': - return state - 1; - default: - return state; - } - } + expect(Scheduler).toFlushAndYield(['A:0']); + expect(root).toMatchRenderedOutput(); - function Counter(props, ref) { - const [count, dispatch] = useReducer(reducer, 0); - useImperativeHandle(ref, () => ({dispatch})); - return ; - } - Counter = forwardRef(Counter); - const counter = React.createRef(null); - ReactNoop.render(); - expect(Scheduler).toFlushAndYield(['Count: 0']); - expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]); - - act(() => counter.current.dispatch(INCREMENT)); - expect(Scheduler).toHaveYielded(['Count: 1']); - expect(ReactNoop.getChildren()).toEqual([span('Count: 1')]); - act(() => { - counter.current.dispatch(DECREMENT); - counter.current.dispatch(DECREMENT); - counter.current.dispatch(DECREMENT); + await ReactNoop.act(async () => { + root.render(); + setLabel('B'); + }); + expect(Scheduler).toHaveYielded(['Suspend!']); + expect(root).toMatchRenderedOutput(); + + // Rendering again should suspend again. + root.render(); + expect(Scheduler).toFlushAndYield(['Suspend!']); + + // Flip the signal back to "cancel" the update. However, the update to + // label should still proceed. It shouldn't have been dropped. + root.render(); + expect(Scheduler).toFlushAndYield(['B:0']); + expect(root).toMatchRenderedOutput(); + }); + + // TODO: This should probably warn + it.experimental( + 'calling startTransition inside render phase', + async () => { + let startTransition; + function App() { + let [counter, setCounter] = useState(0); + let [_startTransition] = useTransition(); + startTransition = _startTransition; + + if (counter === 0) { + startTransition(() => { + setCounter(c => c + 1); + }); + } + + return ; + } + + const root = ReactNoop.createRoot(); + root.render(); + expect(Scheduler).toFlushAndYield([1]); + expect(root).toMatchRenderedOutput(); + }, + ); }); - expect(Scheduler).toHaveYielded(['Count: -2']); - expect(ReactNoop.getChildren()).toEqual([span('Count: -2')]); - }); + describe('useReducer', () => { + it('simple mount and update', () => { + const INCREMENT = 'INCREMENT'; + const DECREMENT = 'DECREMENT'; + + function reducer(state, action) { + switch (action) { + case 'INCREMENT': + return state + 1; + case 'DECREMENT': + return state - 1; + default: + return state; + } + } - it('lazy init', () => { - const INCREMENT = 'INCREMENT'; - const DECREMENT = 'DECREMENT'; - - function reducer(state, action) { - switch (action) { - case 'INCREMENT': - return state + 1; - case 'DECREMENT': - return state - 1; - default: - return state; - } - } + function Counter(props, ref) { + const [count, dispatch] = useReducer(reducer, 0); + useImperativeHandle(ref, () => ({dispatch})); + return ; + } + Counter = forwardRef(Counter); + const counter = React.createRef(null); + ReactNoop.render(); + expect(Scheduler).toFlushAndYield(['Count: 0']); + expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]); + + act(() => counter.current.dispatch(INCREMENT)); + expect(Scheduler).toHaveYielded(['Count: 1']); + expect(ReactNoop.getChildren()).toEqual([span('Count: 1')]); + act(() => { + counter.current.dispatch(DECREMENT); + counter.current.dispatch(DECREMENT); + counter.current.dispatch(DECREMENT); + }); - function Counter(props, ref) { - const [count, dispatch] = useReducer(reducer, props, p => { - Scheduler.unstable_yieldValue('Init'); - return p.initialCount; + expect(Scheduler).toHaveYielded(['Count: -2']); + expect(ReactNoop.getChildren()).toEqual([span('Count: -2')]); }); - useImperativeHandle(ref, () => ({dispatch})); - return ; - } - Counter = forwardRef(Counter); - const counter = React.createRef(null); - ReactNoop.render(); - expect(Scheduler).toFlushAndYield(['Init', 'Count: 10']); - expect(ReactNoop.getChildren()).toEqual([span('Count: 10')]); - - act(() => counter.current.dispatch(INCREMENT)); - expect(Scheduler).toHaveYielded(['Count: 11']); - expect(ReactNoop.getChildren()).toEqual([span('Count: 11')]); - - act(() => { - counter.current.dispatch(DECREMENT); - counter.current.dispatch(DECREMENT); - counter.current.dispatch(DECREMENT); - }); - expect(Scheduler).toHaveYielded(['Count: 8']); - expect(ReactNoop.getChildren()).toEqual([span('Count: 8')]); - }); + it('lazy init', () => { + const INCREMENT = 'INCREMENT'; + const DECREMENT = 'DECREMENT'; + + function reducer(state, action) { + switch (action) { + case 'INCREMENT': + return state + 1; + case 'DECREMENT': + return state - 1; + default: + return state; + } + } - // Regression test for https://github.com/facebook/react/issues/14360 - it('handles dispatches with mixed priorities', () => { - const INCREMENT = 'INCREMENT'; + function Counter(props, ref) { + const [count, dispatch] = useReducer(reducer, props, p => { + Scheduler.unstable_yieldValue('Init'); + return p.initialCount; + }); + useImperativeHandle(ref, () => ({dispatch})); + return ; + } + Counter = forwardRef(Counter); + const counter = React.createRef(null); + ReactNoop.render(); + expect(Scheduler).toFlushAndYield(['Init', 'Count: 10']); + expect(ReactNoop.getChildren()).toEqual([span('Count: 10')]); + + act(() => counter.current.dispatch(INCREMENT)); + expect(Scheduler).toHaveYielded(['Count: 11']); + expect(ReactNoop.getChildren()).toEqual([span('Count: 11')]); + + act(() => { + counter.current.dispatch(DECREMENT); + counter.current.dispatch(DECREMENT); + counter.current.dispatch(DECREMENT); + }); - function reducer(state, action) { - return action === INCREMENT ? state + 1 : state; - } + expect(Scheduler).toHaveYielded(['Count: 8']); + expect(ReactNoop.getChildren()).toEqual([span('Count: 8')]); + }); - function Counter(props, ref) { - const [count, dispatch] = useReducer(reducer, 0); - useImperativeHandle(ref, () => ({dispatch})); - return ; - } + // Regression test for https://github.com/facebook/react/issues/14360 + it('handles dispatches with mixed priorities', () => { + const INCREMENT = 'INCREMENT'; - Counter = forwardRef(Counter); - const counter = React.createRef(null); - ReactNoop.render(); + function reducer(state, action) { + return action === INCREMENT ? state + 1 : state; + } - expect(Scheduler).toFlushAndYield(['Count: 0']); - expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]); + function Counter(props, ref) { + const [count, dispatch] = useReducer(reducer, 0); + useImperativeHandle(ref, () => ({dispatch})); + return ; + } - ReactNoop.batchedUpdates(() => { - counter.current.dispatch(INCREMENT); - counter.current.dispatch(INCREMENT); - counter.current.dispatch(INCREMENT); - }); + Counter = forwardRef(Counter); + const counter = React.createRef(null); + ReactNoop.render(); - ReactNoop.flushSync(() => { - counter.current.dispatch(INCREMENT); - }); - expect(Scheduler).toHaveYielded(['Count: 1']); - expect(ReactNoop.getChildren()).toEqual([span('Count: 1')]); + expect(Scheduler).toFlushAndYield(['Count: 0']); + expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]); - expect(Scheduler).toFlushAndYield(['Count: 4']); - expect(ReactNoop.getChildren()).toEqual([span('Count: 4')]); - }); - }); + ReactNoop.batchedUpdates(() => { + counter.current.dispatch(INCREMENT); + counter.current.dispatch(INCREMENT); + counter.current.dispatch(INCREMENT); + }); + + ReactNoop.flushSync(() => { + counter.current.dispatch(INCREMENT); + }); + expect(Scheduler).toHaveYielded(['Count: 1']); + expect(ReactNoop.getChildren()).toEqual([span('Count: 1')]); - describe('useEffect', () => { - it('simple mount and update', () => { - function Counter(props) { - useEffect(() => { - Scheduler.unstable_yieldValue(`Passive effect [${props.count}]`); + expect(Scheduler).toFlushAndYield(['Count: 4']); + expect(ReactNoop.getChildren()).toEqual([span('Count: 4')]); }); - return ; - } - act(() => { - ReactNoop.render(, () => - Scheduler.unstable_yieldValue('Sync effect'), - ); - expect(Scheduler).toFlushAndYieldThrough(['Count: 0', 'Sync effect']); - expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]); - // Effects are deferred until after the commit - expect(Scheduler).toFlushAndYield(['Passive effect [0]']); }); - act(() => { - ReactNoop.render(, () => - Scheduler.unstable_yieldValue('Sync effect'), - ); - expect(Scheduler).toFlushAndYieldThrough(['Count: 1', 'Sync effect']); - expect(ReactNoop.getChildren()).toEqual([span('Count: 1')]); - // Effects are deferred until after the commit - expect(Scheduler).toFlushAndYield(['Passive effect [1]']); - }); - }); + describe('useEffect', () => { + it('simple mount and update', () => { + function Counter(props) { + useEffect(() => { + Scheduler.unstable_yieldValue(`Passive effect [${props.count}]`); + }); + return ; + } + act(() => { + ReactNoop.render(, () => + Scheduler.unstable_yieldValue('Sync effect'), + ); + expect(Scheduler).toFlushAndYieldThrough([ + 'Count: 0', + 'Sync effect', + ]); + expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]); + // Effects are deferred until after the commit + expect(Scheduler).toFlushAndYield(['Passive effect [0]']); + }); - it('flushes passive effects even with sibling deletions', () => { - function LayoutEffect(props) { - useLayoutEffect(() => { - Scheduler.unstable_yieldValue(`Layout effect`); + act(() => { + ReactNoop.render(, () => + Scheduler.unstable_yieldValue('Sync effect'), + ); + expect(Scheduler).toFlushAndYieldThrough([ + 'Count: 1', + 'Sync effect', + ]); + expect(ReactNoop.getChildren()).toEqual([span('Count: 1')]); + // Effects are deferred until after the commit + expect(Scheduler).toFlushAndYield(['Passive effect [1]']); + }); }); - return ; - } - function PassiveEffect(props) { - useEffect(() => { - Scheduler.unstable_yieldValue(`Passive effect`); - }, []); - return ; - } - let passive = ; - act(() => { - ReactNoop.render([, passive]); - expect(Scheduler).toFlushAndYieldThrough([ - 'Layout', - 'Passive', - 'Layout effect', - ]); - expect(ReactNoop.getChildren()).toEqual([ - span('Layout'), - span('Passive'), - ]); - // Destroying the first child shouldn't prevent the passive effect from - // being executed - ReactNoop.render([passive]); - expect(Scheduler).toFlushAndYield(['Passive effect']); - expect(ReactNoop.getChildren()).toEqual([span('Passive')]); - }); - // exiting act calls flushPassiveEffects(), but there are none left to flush. - expect(Scheduler).toHaveYielded([]); - }); - it('flushes passive effects even if siblings schedule an update', () => { - function PassiveEffect(props) { - useEffect(() => { - Scheduler.unstable_yieldValue('Passive effect'); - }); - return ; - } - function LayoutEffect(props) { - let [count, setCount] = useState(0); - useLayoutEffect(() => { - // Scheduling work shouldn't interfere with the queued passive effect - if (count === 0) { - setCount(1); - } - Scheduler.unstable_yieldValue('Layout effect ' + count); + it('flushes passive effects even with sibling deletions', () => { + function LayoutEffect(props) { + useLayoutEffect(() => { + Scheduler.unstable_yieldValue(`Layout effect`); + }); + return ; + } + function PassiveEffect(props) { + useEffect(() => { + Scheduler.unstable_yieldValue(`Passive effect`); + }, []); + return ; + } + let passive = ; + act(() => { + ReactNoop.render([, passive]); + expect(Scheduler).toFlushAndYieldThrough([ + 'Layout', + 'Passive', + 'Layout effect', + ]); + expect(ReactNoop.getChildren()).toEqual([ + span('Layout'), + span('Passive'), + ]); + // Destroying the first child shouldn't prevent the passive effect from + // being executed + ReactNoop.render([passive]); + expect(Scheduler).toFlushAndYield(['Passive effect']); + expect(ReactNoop.getChildren()).toEqual([span('Passive')]); + }); + // exiting act calls flushPassiveEffects(), but there are none left to flush. + expect(Scheduler).toHaveYielded([]); }); - return ; - } - ReactNoop.render([, ]); + it('flushes passive effects even if siblings schedule an update', () => { + function PassiveEffect(props) { + useEffect(() => { + Scheduler.unstable_yieldValue('Passive effect'); + }); + return ; + } + function LayoutEffect(props) { + let [count, setCount] = useState(0); + useLayoutEffect(() => { + // Scheduling work shouldn't interfere with the queued passive effect + if (count === 0) { + setCount(1); + } + Scheduler.unstable_yieldValue('Layout effect ' + count); + }); + return ; + } - act(() => { - expect(Scheduler).toFlushAndYield([ - 'Passive', - 'Layout', - 'Layout effect 0', - 'Passive effect', - 'Layout', - 'Layout effect 1', - ]); - }); + ReactNoop.render([ + , + , + ]); - expect(ReactNoop.getChildren()).toEqual([ - span('Passive'), - span('Layout'), - ]); - }); + act(() => { + expect(Scheduler).toFlushAndYield([ + 'Passive', + 'Layout', + 'Layout effect 0', + 'Passive effect', + 'Layout', + 'Layout effect 1', + ]); + }); - it('flushes passive effects even if siblings schedule a new root', () => { - function PassiveEffect(props) { - useEffect(() => { - Scheduler.unstable_yieldValue('Passive effect'); - }, []); - return ; - } - function LayoutEffect(props) { - useLayoutEffect(() => { - Scheduler.unstable_yieldValue('Layout effect'); - // Scheduling work shouldn't interfere with the queued passive effect - ReactNoop.renderToRootWithID(, 'root2'); + expect(ReactNoop.getChildren()).toEqual([ + span('Passive'), + span('Layout'), + ]); }); - return ; - } - act(() => { - ReactNoop.render([, ]); - expect(Scheduler).toFlushAndYield([ - 'Passive', - 'Layout', - 'Layout effect', - 'Passive effect', - 'New Root', - ]); - expect(ReactNoop.getChildren()).toEqual([ - span('Passive'), - span('Layout'), - ]); - }); - }); - it( - 'flushes effects serially by flushing old effects before flushing ' + - "new ones, if they haven't already fired", - () => { - function getCommittedText() { - const children = ReactNoop.getChildren(); - if (children === null) { - return null; + it('flushes passive effects even if siblings schedule a new root', () => { + function PassiveEffect(props) { + useEffect(() => { + Scheduler.unstable_yieldValue('Passive effect'); + }, []); + return ; } - return children[0].prop; - } - - function Counter(props) { - useEffect(() => { - Scheduler.unstable_yieldValue( - `Committed state when effect was fired: ${getCommittedText()}`, - ); + function LayoutEffect(props) { + useLayoutEffect(() => { + Scheduler.unstable_yieldValue('Layout effect'); + // Scheduling work shouldn't interfere with the queued passive effect + ReactNoop.renderToRootWithID(, 'root2'); + }); + return ; + } + act(() => { + ReactNoop.render([ + , + , + ]); + expect(Scheduler).toFlushAndYield([ + 'Passive', + 'Layout', + 'Layout effect', + 'Passive effect', + 'New Root', + ]); + expect(ReactNoop.getChildren()).toEqual([ + span('Passive'), + span('Layout'), + ]); }); - return ; - } - act(() => { - ReactNoop.render(, () => - Scheduler.unstable_yieldValue('Sync effect'), - ); - expect(Scheduler).toFlushAndYieldThrough([0, 'Sync effect']); - expect(ReactNoop.getChildren()).toEqual([span(0)]); - // Before the effects have a chance to flush, schedule another update - ReactNoop.render(, () => - Scheduler.unstable_yieldValue('Sync effect'), - ); - expect(Scheduler).toFlushAndYieldThrough([ - // The previous effect flushes before the reconciliation - 'Committed state when effect was fired: 0', - 1, - 'Sync effect', - ]); - expect(ReactNoop.getChildren()).toEqual([span(1)]); }); - expect(Scheduler).toHaveYielded([ - 'Committed state when effect was fired: 1', - ]); - }, - ); - - it('defers passive effect destroy functions during unmount', () => { - function Child({bar, foo}) { - React.useEffect(() => { - Scheduler.unstable_yieldValue('passive bar create'); - return () => { - Scheduler.unstable_yieldValue('passive bar destroy'); - }; - }, [bar]); - React.useLayoutEffect(() => { - Scheduler.unstable_yieldValue('layout bar create'); - return () => { - Scheduler.unstable_yieldValue('layout bar destroy'); - }; - }, [bar]); - React.useEffect(() => { - Scheduler.unstable_yieldValue('passive foo create'); - return () => { - Scheduler.unstable_yieldValue('passive foo destroy'); - }; - }, [foo]); - React.useLayoutEffect(() => { - Scheduler.unstable_yieldValue('layout foo create'); - return () => { - Scheduler.unstable_yieldValue('layout foo destroy'); - }; - }, [foo]); - Scheduler.unstable_yieldValue('render'); - return null; - } + it( + 'flushes effects serially by flushing old effects before flushing ' + + "new ones, if they haven't already fired", + () => { + function getCommittedText() { + const children = ReactNoop.getChildren(); + if (children === null) { + return null; + } + return children[0].prop; + } + + function Counter(props) { + useEffect(() => { + Scheduler.unstable_yieldValue( + `Committed state when effect was fired: ${getCommittedText()}`, + ); + }); + return ; + } + act(() => { + ReactNoop.render(, () => + Scheduler.unstable_yieldValue('Sync effect'), + ); + expect(Scheduler).toFlushAndYieldThrough([0, 'Sync effect']); + expect(ReactNoop.getChildren()).toEqual([span(0)]); + // Before the effects have a chance to flush, schedule another update + ReactNoop.render(, () => + Scheduler.unstable_yieldValue('Sync effect'), + ); + expect(Scheduler).toFlushAndYieldThrough([ + // The previous effect flushes before the reconciliation + 'Committed state when effect was fired: 0', + 1, + 'Sync effect', + ]); + expect(ReactNoop.getChildren()).toEqual([span(1)]); + }); - act(() => { - ReactNoop.render(, () => - Scheduler.unstable_yieldValue('Sync effect'), + expect(Scheduler).toHaveYielded([ + 'Committed state when effect was fired: 1', + ]); + }, ); - expect(Scheduler).toFlushAndYieldThrough([ - 'render', - 'layout bar create', - 'layout foo create', - 'Sync effect', - ]); - // Effects are deferred until after the commit - expect(Scheduler).toFlushAndYield([ - 'passive bar create', - 'passive foo create', - ]); - }); - - // This update is exists to test an internal implementation detail: - // Effects without updating dependencies lose their layout/passive tag during an update. - act(() => { - ReactNoop.render(, () => - Scheduler.unstable_yieldValue('Sync effect'), - ); - expect(Scheduler).toFlushAndYieldThrough([ - 'render', - 'layout foo destroy', - 'layout foo create', - 'Sync effect', - ]); - // Effects are deferred until after the commit - expect(Scheduler).toFlushAndYield([ - 'passive foo destroy', - 'passive foo create', - ]); - }); - // Unmount the component and verify that passive destroy functions are deferred until post-commit. - act(() => { - ReactNoop.render(null, () => - Scheduler.unstable_yieldValue('Sync effect'), - ); - expect(Scheduler).toFlushAndYieldThrough([ - 'layout bar destroy', - 'layout foo destroy', - 'Sync effect', - ]); - // Effects are deferred until after the commit - expect(Scheduler).toFlushAndYield([ - 'passive bar destroy', - 'passive foo destroy', - ]); - }); - }); + if ( + deferPassiveEffectCleanupDuringUnmount && + runAllPassiveEffectDestroysBeforeCreates + ) { + it('defers passive effect destroy functions during unmount', () => { + function Child({bar, foo}) { + React.useEffect(() => { + Scheduler.unstable_yieldValue('passive bar create'); + return () => { + Scheduler.unstable_yieldValue('passive bar destroy'); + }; + }, [bar]); + React.useLayoutEffect(() => { + Scheduler.unstable_yieldValue('layout bar create'); + return () => { + Scheduler.unstable_yieldValue('layout bar destroy'); + }; + }, [bar]); + React.useEffect(() => { + Scheduler.unstable_yieldValue('passive foo create'); + return () => { + Scheduler.unstable_yieldValue('passive foo destroy'); + }; + }, [foo]); + React.useLayoutEffect(() => { + Scheduler.unstable_yieldValue('layout foo create'); + return () => { + Scheduler.unstable_yieldValue('layout foo destroy'); + }; + }, [foo]); + Scheduler.unstable_yieldValue('render'); + return null; + } + + act(() => { + ReactNoop.render(, () => + Scheduler.unstable_yieldValue('Sync effect'), + ); + expect(Scheduler).toFlushAndYieldThrough([ + 'render', + 'layout bar create', + 'layout foo create', + 'Sync effect', + ]); + // Effects are deferred until after the commit + expect(Scheduler).toFlushAndYield([ + 'passive bar create', + 'passive foo create', + ]); + }); - it('updates have async priority', () => { - function Counter(props) { - const [count, updateCount] = useState('(empty)'); - useEffect(() => { - Scheduler.unstable_yieldValue(`Schedule update [${props.count}]`); - updateCount(props.count); - }, [props.count]); - return ; - } - act(() => { - ReactNoop.render(, () => - Scheduler.unstable_yieldValue('Sync effect'), - ); - expect(Scheduler).toFlushAndYieldThrough([ - 'Count: (empty)', - 'Sync effect', - ]); - expect(ReactNoop.getChildren()).toEqual([span('Count: (empty)')]); - ReactNoop.flushPassiveEffects(); - expect(Scheduler).toHaveYielded(['Schedule update [0]']); - expect(Scheduler).toFlushAndYield(['Count: 0']); - }); + // This update is exists to test an internal implementation detail: + // Effects without updating dependencies lose their layout/passive tag during an update. + act(() => { + ReactNoop.render(, () => + Scheduler.unstable_yieldValue('Sync effect'), + ); + expect(Scheduler).toFlushAndYieldThrough([ + 'render', + 'layout foo destroy', + 'layout foo create', + 'Sync effect', + ]); + // Effects are deferred until after the commit + expect(Scheduler).toFlushAndYield([ + 'passive foo destroy', + 'passive foo create', + ]); + }); - act(() => { - ReactNoop.render(, () => - Scheduler.unstable_yieldValue('Sync effect'), - ); - expect(Scheduler).toFlushAndYieldThrough(['Count: 0', 'Sync effect']); - expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]); - ReactNoop.flushPassiveEffects(); - expect(Scheduler).toHaveYielded(['Schedule update [1]']); - expect(Scheduler).toFlushAndYield(['Count: 1']); - }); - }); + // Unmount the component and verify that passive destroy functions are deferred until post-commit. + act(() => { + ReactNoop.render(null, () => + Scheduler.unstable_yieldValue('Sync effect'), + ); + expect(Scheduler).toFlushAndYieldThrough([ + 'layout bar destroy', + 'layout foo destroy', + 'Sync effect', + ]); + // Effects are deferred until after the commit + expect(Scheduler).toFlushAndYield([ + 'passive bar destroy', + 'passive foo destroy', + ]); + }); + }); + } - it('updates have async priority even if effects are flushed early', () => { - function Counter(props) { - const [count, updateCount] = useState('(empty)'); - useEffect(() => { - Scheduler.unstable_yieldValue(`Schedule update [${props.count}]`); - updateCount(props.count); - }, [props.count]); - return ; - } - act(() => { - ReactNoop.render(, () => - Scheduler.unstable_yieldValue('Sync effect'), - ); - expect(Scheduler).toFlushAndYieldThrough([ - 'Count: (empty)', - 'Sync effect', - ]); - expect(ReactNoop.getChildren()).toEqual([span('Count: (empty)')]); + it('updates have async priority', () => { + function Counter(props) { + const [count, updateCount] = useState('(empty)'); + useEffect(() => { + Scheduler.unstable_yieldValue(`Schedule update [${props.count}]`); + updateCount(props.count); + }, [props.count]); + return ; + } + act(() => { + ReactNoop.render(, () => + Scheduler.unstable_yieldValue('Sync effect'), + ); + expect(Scheduler).toFlushAndYieldThrough([ + 'Count: (empty)', + 'Sync effect', + ]); + expect(ReactNoop.getChildren()).toEqual([span('Count: (empty)')]); + ReactNoop.flushPassiveEffects(); + expect(Scheduler).toHaveYielded(['Schedule update [0]']); + expect(Scheduler).toFlushAndYield(['Count: 0']); + }); - // Rendering again should flush the previous commit's effects - ReactNoop.render(, () => - Scheduler.unstable_yieldValue('Sync effect'), - ); - expect(Scheduler).toFlushAndYieldThrough([ - 'Schedule update [0]', - 'Count: 0', - ]); - expect(ReactNoop.getChildren()).toEqual([span('Count: (empty)')]); + act(() => { + ReactNoop.render(, () => + Scheduler.unstable_yieldValue('Sync effect'), + ); + expect(Scheduler).toFlushAndYieldThrough([ + 'Count: 0', + 'Sync effect', + ]); + expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]); + ReactNoop.flushPassiveEffects(); + expect(Scheduler).toHaveYielded(['Schedule update [1]']); + expect(Scheduler).toFlushAndYield(['Count: 1']); + }); + }); - expect(Scheduler).toFlushAndYieldThrough(['Sync effect']); - expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]); - ReactNoop.flushPassiveEffects(); - expect(Scheduler).toHaveYielded(['Schedule update [1]']); - expect(Scheduler).toFlushAndYield(['Count: 1']); - expect(ReactNoop.getChildren()).toEqual([span('Count: 1')]); - }); - }); + it('updates have async priority even if effects are flushed early', () => { + function Counter(props) { + const [count, updateCount] = useState('(empty)'); + useEffect(() => { + Scheduler.unstable_yieldValue(`Schedule update [${props.count}]`); + updateCount(props.count); + }, [props.count]); + return ; + } + act(() => { + ReactNoop.render(, () => + Scheduler.unstable_yieldValue('Sync effect'), + ); + expect(Scheduler).toFlushAndYieldThrough([ + 'Count: (empty)', + 'Sync effect', + ]); + expect(ReactNoop.getChildren()).toEqual([span('Count: (empty)')]); + + // Rendering again should flush the previous commit's effects + ReactNoop.render(, () => + Scheduler.unstable_yieldValue('Sync effect'), + ); + expect(Scheduler).toFlushAndYieldThrough([ + 'Schedule update [0]', + 'Count: 0', + ]); + expect(ReactNoop.getChildren()).toEqual([span('Count: (empty)')]); + + expect(Scheduler).toFlushAndYieldThrough(['Sync effect']); + expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]); + ReactNoop.flushPassiveEffects(); + expect(Scheduler).toHaveYielded(['Schedule update [1]']); + expect(Scheduler).toFlushAndYield(['Count: 1']); + expect(ReactNoop.getChildren()).toEqual([span('Count: 1')]); + }); + }); - it('flushes passive effects when flushing discrete updates', () => { - let _updateCount; - function Counter(props) { - const [count, updateCount] = useState(0); - _updateCount = updateCount; - useEffect(() => { - Scheduler.unstable_yieldValue(`Will set count to 1`); - updateCount(1); - }, []); - return ; - } + it('flushes passive effects when flushing discrete updates', () => { + let _updateCount; + function Counter(props) { + const [count, updateCount] = useState(0); + _updateCount = updateCount; + useEffect(() => { + Scheduler.unstable_yieldValue(`Will set count to 1`); + updateCount(1); + }, []); + return ; + } - // we explicitly wait for missing act() warnings here since - // it's a lot harder to simulate this condition inside an act scope - expect(() => { - ReactNoop.render(, () => - Scheduler.unstable_yieldValue('Sync effect'), - ); - expect(Scheduler).toFlushAndYieldThrough(['Count: 0', 'Sync effect']); - expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]); - }).toErrorDev(['An update to Counter ran an effect']); + // we explicitly wait for missing act() warnings here since + // it's a lot harder to simulate this condition inside an act scope + expect(() => { + ReactNoop.render(, () => + Scheduler.unstable_yieldValue('Sync effect'), + ); + expect(Scheduler).toFlushAndYieldThrough([ + 'Count: 0', + 'Sync effect', + ]); + expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]); + }).toErrorDev(['An update to Counter ran an effect']); + + // A discrete event forces the passive effect to be flushed -- + // updateCount(1) happens first, so 2 wins. + ReactNoop.flushDiscreteUpdates(); + ReactNoop.discreteUpdates(() => { + // (use batchedUpdates to silence the act() warning) + ReactNoop.batchedUpdates(() => { + _updateCount(2); + }); + }); + expect(Scheduler).toHaveYielded(['Will set count to 1']); + expect(() => { + expect(Scheduler).toFlushAndYield(['Count: 2']); + }).toErrorDev([ + 'An update to Counter ran an effect', + 'An update to Counter ran an effect', + ]); - // A discrete event forces the passive effect to be flushed -- - // updateCount(1) happens first, so 2 wins. - ReactNoop.flushDiscreteUpdates(); - ReactNoop.discreteUpdates(() => { - // (use batchedUpdates to silence the act() warning) - ReactNoop.batchedUpdates(() => { - _updateCount(2); + expect(ReactNoop.getChildren()).toEqual([span('Count: 2')]); }); - }); - expect(Scheduler).toHaveYielded(['Will set count to 1']); - expect(() => { - expect(Scheduler).toFlushAndYield(['Count: 2']); - }).toErrorDev([ - 'An update to Counter ran an effect', - 'An update to Counter ran an effect', - ]); - - expect(ReactNoop.getChildren()).toEqual([span('Count: 2')]); - }); - it('flushes passive effects when flushing discrete updates (with tracing)', () => { - const onInteractionScheduledWorkCompleted = jest.fn(); - const onWorkCanceled = jest.fn(); - SchedulerTracing.unstable_subscribe({ - onInteractionScheduledWorkCompleted, - onInteractionTraced: jest.fn(), - onWorkCanceled, - onWorkScheduled: jest.fn(), - onWorkStarted: jest.fn(), - onWorkStopped: jest.fn(), - }); + it('flushes passive effects when flushing discrete updates (with tracing)', () => { + const onInteractionScheduledWorkCompleted = jest.fn(); + const onWorkCanceled = jest.fn(); + SchedulerTracing.unstable_subscribe({ + onInteractionScheduledWorkCompleted, + onInteractionTraced: jest.fn(), + onWorkCanceled, + onWorkScheduled: jest.fn(), + onWorkStarted: jest.fn(), + onWorkStopped: jest.fn(), + }); - let _updateCount; - function Counter(props) { - const [count, updateCount] = useState(0); - _updateCount = updateCount; - useEffect(() => { - expect(SchedulerTracing.unstable_getCurrent()).toMatchInteractions([ - tracingEvent, - ]); - Scheduler.unstable_yieldValue(`Will set count to 1`); - updateCount(1); - }, []); - return ; - } + let _updateCount; + function Counter(props) { + const [count, updateCount] = useState(0); + _updateCount = updateCount; + useEffect(() => { + expect( + SchedulerTracing.unstable_getCurrent(), + ).toMatchInteractions([tracingEvent]); + Scheduler.unstable_yieldValue(`Will set count to 1`); + updateCount(1); + }, []); + return ; + } - const tracingEvent = {id: 0, name: 'hello', timestamp: 0}; - // we explicitly wait for missing act() warnings here since - // it's a lot harder to simulate this condition inside an act scope - expect(() => { - SchedulerTracing.unstable_trace( - tracingEvent.name, - tracingEvent.timestamp, - () => { - ReactNoop.render(, () => - Scheduler.unstable_yieldValue('Sync effect'), + const tracingEvent = {id: 0, name: 'hello', timestamp: 0}; + // we explicitly wait for missing act() warnings here since + // it's a lot harder to simulate this condition inside an act scope + expect(() => { + SchedulerTracing.unstable_trace( + tracingEvent.name, + tracingEvent.timestamp, + () => { + ReactNoop.render(, () => + Scheduler.unstable_yieldValue('Sync effect'), + ); + }, ); - }, - ); - expect(Scheduler).toFlushAndYieldThrough(['Count: 0', 'Sync effect']); - expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]); - }).toErrorDev(['An update to Counter ran an effect']); + expect(Scheduler).toFlushAndYieldThrough([ + 'Count: 0', + 'Sync effect', + ]); + expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]); + }).toErrorDev(['An update to Counter ran an effect']); + + expect(onInteractionScheduledWorkCompleted).toHaveBeenCalledTimes(0); + + // A discrete event forces the passive effect to be flushed -- + // updateCount(1) happens first, so 2 wins. + ReactNoop.flushDiscreteUpdates(); + ReactNoop.discreteUpdates(() => { + // (use batchedUpdates to silence the act() warning) + ReactNoop.batchedUpdates(() => { + _updateCount(2); + }); + }); + expect(Scheduler).toHaveYielded(['Will set count to 1']); + expect(() => { + expect(Scheduler).toFlushAndYield(['Count: 2']); + }).toErrorDev([ + 'An update to Counter ran an effect', + 'An update to Counter ran an effect', + ]); - expect(onInteractionScheduledWorkCompleted).toHaveBeenCalledTimes(0); + expect(ReactNoop.getChildren()).toEqual([span('Count: 2')]); - // A discrete event forces the passive effect to be flushed -- - // updateCount(1) happens first, so 2 wins. - ReactNoop.flushDiscreteUpdates(); - ReactNoop.discreteUpdates(() => { - // (use batchedUpdates to silence the act() warning) - ReactNoop.batchedUpdates(() => { - _updateCount(2); + expect(onInteractionScheduledWorkCompleted).toHaveBeenCalledTimes(1); + expect(onWorkCanceled).toHaveBeenCalledTimes(0); }); - }); - expect(Scheduler).toHaveYielded(['Will set count to 1']); - expect(() => { - expect(Scheduler).toFlushAndYield(['Count: 2']); - }).toErrorDev([ - 'An update to Counter ran an effect', - 'An update to Counter ran an effect', - ]); - - expect(ReactNoop.getChildren()).toEqual([span('Count: 2')]); - - expect(onInteractionScheduledWorkCompleted).toHaveBeenCalledTimes(1); - expect(onWorkCanceled).toHaveBeenCalledTimes(0); - }); - it( - 'in legacy mode, useEffect is deferred and updates finish synchronously ' + - '(in a single batch)', - () => { - function Counter(props) { - const [count, updateCount] = useState('(empty)'); - useEffect(() => { - // Update multiple times. These should all be batched together in - // a single render. - updateCount(props.count); - updateCount(props.count); - updateCount(props.count); - updateCount(props.count); - updateCount(props.count); - updateCount(props.count); - }, [props.count]); - return ; - } - act(() => { - ReactNoop.renderLegacySyncRoot(); - // Even in legacy mode, effects are deferred until after paint - expect(Scheduler).toFlushAndYieldThrough(['Count: (empty)']); - expect(ReactNoop.getChildren()).toEqual([span('Count: (empty)')]); - }); + it( + 'in legacy mode, useEffect is deferred and updates finish synchronously ' + + '(in a single batch)', + () => { + function Counter(props) { + const [count, updateCount] = useState('(empty)'); + useEffect(() => { + // Update multiple times. These should all be batched together in + // a single render. + updateCount(props.count); + updateCount(props.count); + updateCount(props.count); + updateCount(props.count); + updateCount(props.count); + updateCount(props.count); + }, [props.count]); + return ; + } + act(() => { + ReactNoop.renderLegacySyncRoot(); + // Even in legacy mode, effects are deferred until after paint + expect(Scheduler).toFlushAndYieldThrough(['Count: (empty)']); + expect(ReactNoop.getChildren()).toEqual([span('Count: (empty)')]); + }); - // effects get fored on exiting act() - // There were multiple updates, but there should only be a - // single render - expect(Scheduler).toHaveYielded(['Count: 0']); - expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]); - }, - ); - - it('flushSync is not allowed', () => { - function Counter(props) { - const [count, updateCount] = useState('(empty)'); - useEffect(() => { - Scheduler.unstable_yieldValue(`Schedule update [${props.count}]`); - ReactNoop.flushSync(() => { - updateCount(props.count); - }); - }, [props.count]); - return ; - } - act(() => { - ReactNoop.render(, () => - Scheduler.unstable_yieldValue('Sync effect'), + // effects get fored on exiting act() + // There were multiple updates, but there should only be a + // single render + expect(Scheduler).toHaveYielded(['Count: 0']); + expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]); + }, ); - expect(Scheduler).toFlushAndYieldThrough([ - 'Count: (empty)', - 'Sync effect', - ]); - expect(ReactNoop.getChildren()).toEqual([span('Count: (empty)')]); - expect(() => { - ReactNoop.flushPassiveEffects(); - }).toThrow('flushSync was called from inside a lifecycle method'); - }); - }); - it('unmounts previous effect', () => { - function Counter(props) { - useEffect(() => { - Scheduler.unstable_yieldValue(`Did create [${props.count}]`); - return () => { - Scheduler.unstable_yieldValue(`Did destroy [${props.count}]`); - }; + it('flushSync is not allowed', () => { + function Counter(props) { + const [count, updateCount] = useState('(empty)'); + useEffect(() => { + Scheduler.unstable_yieldValue(`Schedule update [${props.count}]`); + ReactNoop.flushSync(() => { + updateCount(props.count); + }); + }, [props.count]); + return ; + } + act(() => { + ReactNoop.render(, () => + Scheduler.unstable_yieldValue('Sync effect'), + ); + expect(Scheduler).toFlushAndYieldThrough([ + 'Count: (empty)', + 'Sync effect', + ]); + expect(ReactNoop.getChildren()).toEqual([span('Count: (empty)')]); + expect(() => { + ReactNoop.flushPassiveEffects(); + }).toThrow('flushSync was called from inside a lifecycle method'); + }); }); - return ; - } - act(() => { - ReactNoop.render(, () => - Scheduler.unstable_yieldValue('Sync effect'), - ); - expect(Scheduler).toFlushAndYieldThrough(['Count: 0', 'Sync effect']); - expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]); - }); - expect(Scheduler).toHaveYielded(['Did create [0]']); + it('unmounts previous effect', () => { + function Counter(props) { + useEffect(() => { + Scheduler.unstable_yieldValue(`Did create [${props.count}]`); + return () => { + Scheduler.unstable_yieldValue(`Did destroy [${props.count}]`); + }; + }); + return ; + } + act(() => { + ReactNoop.render(, () => + Scheduler.unstable_yieldValue('Sync effect'), + ); + expect(Scheduler).toFlushAndYieldThrough([ + 'Count: 0', + 'Sync effect', + ]); + expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]); + }); - act(() => { - ReactNoop.render(, () => - Scheduler.unstable_yieldValue('Sync effect'), - ); - expect(Scheduler).toFlushAndYieldThrough(['Count: 1', 'Sync effect']); - expect(ReactNoop.getChildren()).toEqual([span('Count: 1')]); - }); + expect(Scheduler).toHaveYielded(['Did create [0]']); - expect(Scheduler).toHaveYielded(['Did destroy [0]', 'Did create [1]']); - }); + act(() => { + ReactNoop.render(, () => + Scheduler.unstable_yieldValue('Sync effect'), + ); + expect(Scheduler).toFlushAndYieldThrough([ + 'Count: 1', + 'Sync effect', + ]); + expect(ReactNoop.getChildren()).toEqual([span('Count: 1')]); + }); - it('unmounts on deletion', () => { - function Counter(props) { - useEffect(() => { - Scheduler.unstable_yieldValue(`Did create [${props.count}]`); - return () => { - Scheduler.unstable_yieldValue(`Did destroy [${props.count}]`); - }; + expect(Scheduler).toHaveYielded([ + 'Did destroy [0]', + 'Did create [1]', + ]); }); - return ; - } - act(() => { - ReactNoop.render(, () => - Scheduler.unstable_yieldValue('Sync effect'), - ); - expect(Scheduler).toFlushAndYieldThrough(['Count: 0', 'Sync effect']); - expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]); - }); - expect(Scheduler).toHaveYielded(['Did create [0]']); + it('unmounts on deletion', () => { + function Counter(props) { + useEffect(() => { + Scheduler.unstable_yieldValue(`Did create [${props.count}]`); + return () => { + Scheduler.unstable_yieldValue(`Did destroy [${props.count}]`); + }; + }); + return ; + } + act(() => { + ReactNoop.render(, () => + Scheduler.unstable_yieldValue('Sync effect'), + ); + expect(Scheduler).toFlushAndYieldThrough([ + 'Count: 0', + 'Sync effect', + ]); + expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]); + }); - ReactNoop.render(null); - expect(Scheduler).toFlushAndYield(['Did destroy [0]']); - expect(ReactNoop.getChildren()).toEqual([]); - }); + expect(Scheduler).toHaveYielded(['Did create [0]']); - it('unmounts on deletion after skipped effect', () => { - function Counter(props) { - useEffect(() => { - Scheduler.unstable_yieldValue(`Did create [${props.count}]`); - return () => { - Scheduler.unstable_yieldValue(`Did destroy [${props.count}]`); - }; - }, []); - return ; - } - act(() => { - ReactNoop.render(, () => - Scheduler.unstable_yieldValue('Sync effect'), - ); - expect(Scheduler).toFlushAndYieldThrough(['Count: 0', 'Sync effect']); - expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]); - }); + ReactNoop.render(null); + expect(Scheduler).toFlushAndYield(['Did destroy [0]']); + expect(ReactNoop.getChildren()).toEqual([]); + }); - expect(Scheduler).toHaveYielded(['Did create [0]']); + it('unmounts on deletion after skipped effect', () => { + function Counter(props) { + useEffect(() => { + Scheduler.unstable_yieldValue(`Did create [${props.count}]`); + return () => { + Scheduler.unstable_yieldValue(`Did destroy [${props.count}]`); + }; + }, []); + return ; + } + act(() => { + ReactNoop.render(, () => + Scheduler.unstable_yieldValue('Sync effect'), + ); + expect(Scheduler).toFlushAndYieldThrough([ + 'Count: 0', + 'Sync effect', + ]); + expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]); + }); - act(() => { - ReactNoop.render(, () => - Scheduler.unstable_yieldValue('Sync effect'), - ); - expect(Scheduler).toFlushAndYieldThrough(['Count: 1', 'Sync effect']); - expect(ReactNoop.getChildren()).toEqual([span('Count: 1')]); - }); + expect(Scheduler).toHaveYielded(['Did create [0]']); - expect(Scheduler).toHaveYielded([]); + act(() => { + ReactNoop.render(, () => + Scheduler.unstable_yieldValue('Sync effect'), + ); + expect(Scheduler).toFlushAndYieldThrough([ + 'Count: 1', + 'Sync effect', + ]); + expect(ReactNoop.getChildren()).toEqual([span('Count: 1')]); + }); - ReactNoop.render(null); - expect(Scheduler).toFlushAndYield(['Did destroy [0]']); - expect(ReactNoop.getChildren()).toEqual([]); - }); + expect(Scheduler).toHaveYielded([]); - it('always fires effects if no dependencies are provided', () => { - function effect() { - Scheduler.unstable_yieldValue(`Did create`); - return () => { - Scheduler.unstable_yieldValue(`Did destroy`); - }; - } - function Counter(props) { - useEffect(effect); - return ; - } - act(() => { - ReactNoop.render(, () => - Scheduler.unstable_yieldValue('Sync effect'), - ); - expect(Scheduler).toFlushAndYieldThrough(['Count: 0', 'Sync effect']); - expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]); - }); + ReactNoop.render(null); + expect(Scheduler).toFlushAndYield(['Did destroy [0]']); + expect(ReactNoop.getChildren()).toEqual([]); + }); - expect(Scheduler).toHaveYielded(['Did create']); + it('always fires effects if no dependencies are provided', () => { + function effect() { + Scheduler.unstable_yieldValue(`Did create`); + return () => { + Scheduler.unstable_yieldValue(`Did destroy`); + }; + } + function Counter(props) { + useEffect(effect); + return ; + } + act(() => { + ReactNoop.render(, () => + Scheduler.unstable_yieldValue('Sync effect'), + ); + expect(Scheduler).toFlushAndYieldThrough([ + 'Count: 0', + 'Sync effect', + ]); + expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]); + }); - act(() => { - ReactNoop.render(, () => - Scheduler.unstable_yieldValue('Sync effect'), - ); - expect(Scheduler).toFlushAndYieldThrough(['Count: 1', 'Sync effect']); - expect(ReactNoop.getChildren()).toEqual([span('Count: 1')]); - }); + expect(Scheduler).toHaveYielded(['Did create']); - expect(Scheduler).toHaveYielded(['Did destroy', 'Did create']); + act(() => { + ReactNoop.render(, () => + Scheduler.unstable_yieldValue('Sync effect'), + ); + expect(Scheduler).toFlushAndYieldThrough([ + 'Count: 1', + 'Sync effect', + ]); + expect(ReactNoop.getChildren()).toEqual([span('Count: 1')]); + }); - ReactNoop.render(null); - expect(Scheduler).toFlushAndYield(['Did destroy']); - expect(ReactNoop.getChildren()).toEqual([]); - }); + expect(Scheduler).toHaveYielded(['Did destroy', 'Did create']); - it('skips effect if inputs have not changed', () => { - function Counter(props) { - const text = `${props.label}: ${props.count}`; - useEffect(() => { - Scheduler.unstable_yieldValue(`Did create [${text}]`); - return () => { - Scheduler.unstable_yieldValue(`Did destroy [${text}]`); - }; - }, [props.label, props.count]); - return ; - } - act(() => { - ReactNoop.render(, () => - Scheduler.unstable_yieldValue('Sync effect'), - ); - expect(Scheduler).toFlushAndYieldThrough(['Count: 0', 'Sync effect']); - }); + ReactNoop.render(null); + expect(Scheduler).toFlushAndYield(['Did destroy']); + expect(ReactNoop.getChildren()).toEqual([]); + }); - expect(Scheduler).toHaveYielded(['Did create [Count: 0]']); - expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]); + it('skips effect if inputs have not changed', () => { + function Counter(props) { + const text = `${props.label}: ${props.count}`; + useEffect(() => { + Scheduler.unstable_yieldValue(`Did create [${text}]`); + return () => { + Scheduler.unstable_yieldValue(`Did destroy [${text}]`); + }; + }, [props.label, props.count]); + return ; + } + act(() => { + ReactNoop.render(, () => + Scheduler.unstable_yieldValue('Sync effect'), + ); + expect(Scheduler).toFlushAndYieldThrough([ + 'Count: 0', + 'Sync effect', + ]); + }); - act(() => { - ReactNoop.render(, () => - Scheduler.unstable_yieldValue('Sync effect'), - ); - // Count changed - expect(Scheduler).toFlushAndYieldThrough(['Count: 1', 'Sync effect']); - expect(ReactNoop.getChildren()).toEqual([span('Count: 1')]); - }); + expect(Scheduler).toHaveYielded(['Did create [Count: 0]']); + expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]); - expect(Scheduler).toHaveYielded([ - 'Did destroy [Count: 0]', - 'Did create [Count: 1]', - ]); + act(() => { + ReactNoop.render(, () => + Scheduler.unstable_yieldValue('Sync effect'), + ); + // Count changed + expect(Scheduler).toFlushAndYieldThrough([ + 'Count: 1', + 'Sync effect', + ]); + expect(ReactNoop.getChildren()).toEqual([span('Count: 1')]); + }); - act(() => { - ReactNoop.render(, () => - Scheduler.unstable_yieldValue('Sync effect'), - ); - // Nothing changed, so no effect should have fired - expect(Scheduler).toFlushAndYieldThrough(['Count: 1', 'Sync effect']); - }); + expect(Scheduler).toHaveYielded([ + 'Did destroy [Count: 0]', + 'Did create [Count: 1]', + ]); - expect(Scheduler).toHaveYielded([]); - expect(ReactNoop.getChildren()).toEqual([span('Count: 1')]); + act(() => { + ReactNoop.render(, () => + Scheduler.unstable_yieldValue('Sync effect'), + ); + // Nothing changed, so no effect should have fired + expect(Scheduler).toFlushAndYieldThrough([ + 'Count: 1', + 'Sync effect', + ]); + }); - act(() => { - ReactNoop.render(, () => - Scheduler.unstable_yieldValue('Sync effect'), - ); - // Label changed - expect(Scheduler).toFlushAndYieldThrough(['Total: 1', 'Sync effect']); - expect(ReactNoop.getChildren()).toEqual([span('Total: 1')]); - }); + expect(Scheduler).toHaveYielded([]); + expect(ReactNoop.getChildren()).toEqual([span('Count: 1')]); - expect(Scheduler).toHaveYielded([ - 'Did destroy [Count: 1]', - 'Did create [Total: 1]', - ]); - }); + act(() => { + ReactNoop.render(, () => + Scheduler.unstable_yieldValue('Sync effect'), + ); + // Label changed + expect(Scheduler).toFlushAndYieldThrough([ + 'Total: 1', + 'Sync effect', + ]); + expect(ReactNoop.getChildren()).toEqual([span('Total: 1')]); + }); - it('multiple effects', () => { - function Counter(props) { - useEffect(() => { - Scheduler.unstable_yieldValue(`Did commit 1 [${props.count}]`); - }); - useEffect(() => { - Scheduler.unstable_yieldValue(`Did commit 2 [${props.count}]`); + expect(Scheduler).toHaveYielded([ + 'Did destroy [Count: 1]', + 'Did create [Total: 1]', + ]); }); - return ; - } - act(() => { - ReactNoop.render(, () => - Scheduler.unstable_yieldValue('Sync effect'), - ); - expect(Scheduler).toFlushAndYieldThrough(['Count: 0', 'Sync effect']); - expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]); - }); - expect(Scheduler).toHaveYielded(['Did commit 1 [0]', 'Did commit 2 [0]']); + it('multiple effects', () => { + function Counter(props) { + useEffect(() => { + Scheduler.unstable_yieldValue(`Did commit 1 [${props.count}]`); + }); + useEffect(() => { + Scheduler.unstable_yieldValue(`Did commit 2 [${props.count}]`); + }); + return ; + } + act(() => { + ReactNoop.render(, () => + Scheduler.unstable_yieldValue('Sync effect'), + ); + expect(Scheduler).toFlushAndYieldThrough([ + 'Count: 0', + 'Sync effect', + ]); + expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]); + }); - act(() => { - ReactNoop.render(, () => - Scheduler.unstable_yieldValue('Sync effect'), - ); - expect(Scheduler).toFlushAndYieldThrough(['Count: 1', 'Sync effect']); - expect(ReactNoop.getChildren()).toEqual([span('Count: 1')]); - }); - expect(Scheduler).toHaveYielded(['Did commit 1 [1]', 'Did commit 2 [1]']); - }); + expect(Scheduler).toHaveYielded([ + 'Did commit 1 [0]', + 'Did commit 2 [0]', + ]); - it('unmounts all previous effects before creating any new ones', () => { - function Counter(props) { - useEffect(() => { - Scheduler.unstable_yieldValue(`Mount A [${props.count}]`); - return () => { - Scheduler.unstable_yieldValue(`Unmount A [${props.count}]`); - }; - }); - useEffect(() => { - Scheduler.unstable_yieldValue(`Mount B [${props.count}]`); - return () => { - Scheduler.unstable_yieldValue(`Unmount B [${props.count}]`); - }; + act(() => { + ReactNoop.render(, () => + Scheduler.unstable_yieldValue('Sync effect'), + ); + expect(Scheduler).toFlushAndYieldThrough([ + 'Count: 1', + 'Sync effect', + ]); + expect(ReactNoop.getChildren()).toEqual([span('Count: 1')]); + }); + expect(Scheduler).toHaveYielded([ + 'Did commit 1 [1]', + 'Did commit 2 [1]', + ]); }); - return ; - } - act(() => { - ReactNoop.render(, () => - Scheduler.unstable_yieldValue('Sync effect'), - ); - expect(Scheduler).toFlushAndYieldThrough(['Count: 0', 'Sync effect']); - expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]); - }); - expect(Scheduler).toHaveYielded(['Mount A [0]', 'Mount B [0]']); + it('unmounts all previous effects before creating any new ones', () => { + function Counter(props) { + useEffect(() => { + Scheduler.unstable_yieldValue(`Mount A [${props.count}]`); + return () => { + Scheduler.unstable_yieldValue(`Unmount A [${props.count}]`); + }; + }); + useEffect(() => { + Scheduler.unstable_yieldValue(`Mount B [${props.count}]`); + return () => { + Scheduler.unstable_yieldValue(`Unmount B [${props.count}]`); + }; + }); + return ; + } + act(() => { + ReactNoop.render(, () => + Scheduler.unstable_yieldValue('Sync effect'), + ); + expect(Scheduler).toFlushAndYieldThrough([ + 'Count: 0', + 'Sync effect', + ]); + expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]); + }); - act(() => { - ReactNoop.render(, () => - Scheduler.unstable_yieldValue('Sync effect'), - ); - expect(Scheduler).toFlushAndYieldThrough(['Count: 1', 'Sync effect']); - expect(ReactNoop.getChildren()).toEqual([span('Count: 1')]); - }); - expect(Scheduler).toHaveYielded([ - 'Unmount A [0]', - 'Unmount B [0]', - 'Mount A [1]', - 'Mount B [1]', - ]); - }); + expect(Scheduler).toHaveYielded(['Mount A [0]', 'Mount B [0]']); - it('unmounts all previous effects between siblings before creating any new ones', () => { - function Counter({count, label}) { - useEffect(() => { - Scheduler.unstable_yieldValue(`Mount ${label} [${count}]`); - return () => { - Scheduler.unstable_yieldValue(`Unmount ${label} [${count}]`); - }; + act(() => { + ReactNoop.render(, () => + Scheduler.unstable_yieldValue('Sync effect'), + ); + expect(Scheduler).toFlushAndYieldThrough([ + 'Count: 1', + 'Sync effect', + ]); + expect(ReactNoop.getChildren()).toEqual([span('Count: 1')]); + }); + expect(Scheduler).toHaveYielded([ + 'Unmount A [0]', + 'Unmount B [0]', + 'Mount A [1]', + 'Mount B [1]', + ]); }); - return ; - } - act(() => { - ReactNoop.render( - - - - , - () => Scheduler.unstable_yieldValue('Sync effect'), - ); - expect(Scheduler).toFlushAndYieldThrough(['A 0', 'B 0', 'Sync effect']); - expect(ReactNoop.getChildren()).toEqual([span('A 0'), span('B 0')]); - }); - expect(Scheduler).toHaveYielded(['Mount A [0]', 'Mount B [0]']); + if (runAllPassiveEffectDestroysBeforeCreates) { + it('unmounts all previous effects between siblings before creating any new ones', () => { + function Counter({count, label}) { + useEffect(() => { + Scheduler.unstable_yieldValue(`Mount ${label} [${count}]`); + return () => { + Scheduler.unstable_yieldValue(`Unmount ${label} [${count}]`); + }; + }); + return ; + } + act(() => { + ReactNoop.render( + + + + , + () => Scheduler.unstable_yieldValue('Sync effect'), + ); + expect(Scheduler).toFlushAndYieldThrough([ + 'A 0', + 'B 0', + 'Sync effect', + ]); + expect(ReactNoop.getChildren()).toEqual([ + span('A 0'), + span('B 0'), + ]); + }); - act(() => { - ReactNoop.render( - - - - , - () => Scheduler.unstable_yieldValue('Sync effect'), - ); - expect(Scheduler).toFlushAndYieldThrough(['A 1', 'B 1', 'Sync effect']); - expect(ReactNoop.getChildren()).toEqual([span('A 1'), span('B 1')]); - }); - expect(Scheduler).toHaveYielded([ - 'Unmount A [0]', - 'Unmount B [0]', - 'Mount A [1]', - 'Mount B [1]', - ]); - - act(() => { - ReactNoop.render( - - - - , - () => Scheduler.unstable_yieldValue('Sync effect'), - ); - expect(Scheduler).toFlushAndYieldThrough(['B 2', 'C 0', 'Sync effect']); - expect(ReactNoop.getChildren()).toEqual([span('B 2'), span('C 0')]); - }); - expect(Scheduler).toHaveYielded([ - 'Unmount A [1]', - 'Unmount B [1]', - 'Mount B [2]', - 'Mount C [0]', - ]); - }); + expect(Scheduler).toHaveYielded(['Mount A [0]', 'Mount B [0]']); + + act(() => { + ReactNoop.render( + + + + , + () => Scheduler.unstable_yieldValue('Sync effect'), + ); + expect(Scheduler).toFlushAndYieldThrough([ + 'A 1', + 'B 1', + 'Sync effect', + ]); + expect(ReactNoop.getChildren()).toEqual([ + span('A 1'), + span('B 1'), + ]); + }); + expect(Scheduler).toHaveYielded([ + 'Unmount A [0]', + 'Unmount B [0]', + 'Mount A [1]', + 'Mount B [1]', + ]); + + act(() => { + ReactNoop.render( + + + + , + () => Scheduler.unstable_yieldValue('Sync effect'), + ); + expect(Scheduler).toFlushAndYieldThrough([ + 'B 2', + 'C 0', + 'Sync effect', + ]); + expect(ReactNoop.getChildren()).toEqual([ + span('B 2'), + span('C 0'), + ]); + }); + expect(Scheduler).toHaveYielded([ + 'Unmount A [1]', + 'Unmount B [1]', + 'Mount B [2]', + 'Mount C [0]', + ]); + }); + } - it('handles errors on mount', () => { - function Counter(props) { - useEffect(() => { - Scheduler.unstable_yieldValue(`Mount A [${props.count}]`); - return () => { - Scheduler.unstable_yieldValue(`Unmount A [${props.count}]`); - }; + it('handles errors in create on mount', () => { + function Counter(props) { + useEffect(() => { + Scheduler.unstable_yieldValue(`Mount A [${props.count}]`); + return () => { + Scheduler.unstable_yieldValue(`Unmount A [${props.count}]`); + }; + }); + useEffect(() => { + Scheduler.unstable_yieldValue('Oops!'); + throw new Error('Oops!'); + // eslint-disable-next-line no-unreachable + Scheduler.unstable_yieldValue(`Mount B [${props.count}]`); + return () => { + Scheduler.unstable_yieldValue(`Unmount B [${props.count}]`); + }; + }); + return ; + } + act(() => { + ReactNoop.render(, () => + Scheduler.unstable_yieldValue('Sync effect'), + ); + expect(Scheduler).toFlushAndYieldThrough([ + 'Count: 0', + 'Sync effect', + ]); + expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]); + expect(() => ReactNoop.flushPassiveEffects()).toThrow('Oops'); + }); + + expect(Scheduler).toHaveYielded([ + 'Mount A [0]', + 'Oops!', + // Clean up effect A. There's no effect B to clean-up, because it + // never mounted. + 'Unmount A [0]', + ]); + expect(ReactNoop.getChildren()).toEqual([]); }); - useEffect(() => { - Scheduler.unstable_yieldValue('Oops!'); - throw new Error('Oops!'); - // eslint-disable-next-line no-unreachable - Scheduler.unstable_yieldValue(`Mount B [${props.count}]`); - return () => { - Scheduler.unstable_yieldValue(`Unmount B [${props.count}]`); - }; + + it('handles errors in create on update', () => { + function Counter(props) { + useEffect(() => { + Scheduler.unstable_yieldValue(`Mount A [${props.count}]`); + return () => { + Scheduler.unstable_yieldValue(`Unmount A [${props.count}]`); + }; + }); + useEffect(() => { + if (props.count === 1) { + Scheduler.unstable_yieldValue('Oops!'); + throw new Error('Oops!'); + } + Scheduler.unstable_yieldValue(`Mount B [${props.count}]`); + return () => { + Scheduler.unstable_yieldValue(`Unmount B [${props.count}]`); + }; + }); + return ; + } + act(() => { + ReactNoop.render(, () => + Scheduler.unstable_yieldValue('Sync effect'), + ); + expect(Scheduler).toFlushAndYieldThrough([ + 'Count: 0', + 'Sync effect', + ]); + expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]); + ReactNoop.flushPassiveEffects(); + expect(Scheduler).toHaveYielded(['Mount A [0]', 'Mount B [0]']); + }); + + act(() => { + // This update will trigger an error + ReactNoop.render(, () => + Scheduler.unstable_yieldValue('Sync effect'), + ); + expect(Scheduler).toFlushAndYieldThrough([ + 'Count: 1', + 'Sync effect', + ]); + expect(ReactNoop.getChildren()).toEqual([span('Count: 1')]); + expect(() => ReactNoop.flushPassiveEffects()).toThrow('Oops'); + expect(Scheduler).toHaveYielded( + deferPassiveEffectCleanupDuringUnmount && + runAllPassiveEffectDestroysBeforeCreates + ? ['Unmount A [0]', 'Unmount B [0]', 'Mount A [1]', 'Oops!'] + : [ + 'Unmount A [0]', + 'Unmount B [0]', + 'Mount A [1]', + 'Oops!', + 'Unmount A [1]', + ], + ); + expect(ReactNoop.getChildren()).toEqual([]); + }); + if ( + deferPassiveEffectCleanupDuringUnmount && + runAllPassiveEffectDestroysBeforeCreates + ) { + expect(Scheduler).toHaveYielded([ + // Clean up effect A runs passively on unmount. + // There's no effect B to clean-up, because it never mounted. + 'Unmount A [1]', + ]); + } }); - return ; - } - act(() => { - ReactNoop.render(, () => - Scheduler.unstable_yieldValue('Sync effect'), - ); - expect(Scheduler).toFlushAndYieldThrough(['Count: 0', 'Sync effect']); - expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]); - expect(() => ReactNoop.flushPassiveEffects()).toThrow('Oops'); - }); - expect(Scheduler).toHaveYielded([ - 'Mount A [0]', - 'Oops!', - // Clean up effect A. There's no effect B to clean-up, because it - // never mounted. - 'Unmount A [0]', - ]); - expect(ReactNoop.getChildren()).toEqual([]); - }); + it('handles errors in destroy on update', () => { + function Counter(props) { + useEffect(() => { + Scheduler.unstable_yieldValue(`Mount A [${props.count}]`); + return () => { + Scheduler.unstable_yieldValue('Oops!'); + if (props.count === 0) { + throw new Error('Oops!'); + } + }; + }); + useEffect(() => { + Scheduler.unstable_yieldValue(`Mount B [${props.count}]`); + return () => { + Scheduler.unstable_yieldValue(`Unmount B [${props.count}]`); + }; + }); + return ; + } - it('handles errors on update', () => { - function Counter(props) { - useEffect(() => { - Scheduler.unstable_yieldValue(`Mount A [${props.count}]`); - return () => { - Scheduler.unstable_yieldValue(`Unmount A [${props.count}]`); - }; + act(() => { + ReactNoop.render(, () => + Scheduler.unstable_yieldValue('Sync effect'), + ); + expect(Scheduler).toFlushAndYieldThrough([ + 'Count: 0', + 'Sync effect', + ]); + expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]); + ReactNoop.flushPassiveEffects(); + expect(Scheduler).toHaveYielded(['Mount A [0]', 'Mount B [0]']); + }); + + if ( + deferPassiveEffectCleanupDuringUnmount && + runAllPassiveEffectDestroysBeforeCreates + ) { + act(() => { + // This update will trigger an error during passive effect unmount + ReactNoop.render(, () => + Scheduler.unstable_yieldValue('Sync effect'), + ); + expect(Scheduler).toFlushAndYieldThrough([ + 'Count: 1', + 'Sync effect', + ]); + expect(ReactNoop.getChildren()).toEqual([span('Count: 1')]); + expect(() => ReactNoop.flushPassiveEffects()).toThrow('Oops'); + + // This branch enables a feature flag that flushes all passive destroys in a + // separate pass before flushing any passive creates. + // A result of this two-pass flush is that an error thrown from unmount does + // not block the subsequent create functions from being run. + expect(Scheduler).toHaveYielded([ + 'Oops!', + 'Unmount B [0]', + 'Mount A [1]', + 'Mount B [1]', + ]); + }); + + // gets unmounted because an error is thrown above. + // The remaining destroy functions are run later on unmount, since they're passive. + // In this case, one of them throws again (because of how the test is written). + expect(Scheduler).toHaveYielded(['Oops!', 'Unmount B [1]']); + expect(ReactNoop.getChildren()).toEqual([]); + } else { + act(() => { + // This update will trigger an error during passive effect unmount + ReactNoop.render(, () => + Scheduler.unstable_yieldValue('Sync effect'), + ); + expect(() => { + expect(Scheduler).toFlushAndYield(['Count: 1', 'Sync effect']); + }).toThrow('Oops!'); + expect(ReactNoop.getChildren()).toEqual([]); + ReactNoop.flushPassiveEffects(); + }); + } }); - useEffect(() => { - if (props.count === 1) { - Scheduler.unstable_yieldValue('Oops!'); - throw new Error('Oops!'); - } - Scheduler.unstable_yieldValue(`Mount B [${props.count}]`); - return () => { - Scheduler.unstable_yieldValue(`Unmount B [${props.count}]`); - }; + + it('works with memo', () => { + function Counter({count}) { + useLayoutEffect(() => { + Scheduler.unstable_yieldValue('Mount: ' + count); + return () => Scheduler.unstable_yieldValue('Unmount: ' + count); + }); + return ; + } + Counter = memo(Counter); + + ReactNoop.render(, () => + Scheduler.unstable_yieldValue('Sync effect'), + ); + expect(Scheduler).toFlushAndYieldThrough([ + 'Count: 0', + 'Mount: 0', + 'Sync effect', + ]); + expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]); + + ReactNoop.render(, () => + Scheduler.unstable_yieldValue('Sync effect'), + ); + expect(Scheduler).toFlushAndYieldThrough([ + 'Count: 1', + 'Unmount: 0', + 'Mount: 1', + 'Sync effect', + ]); + expect(ReactNoop.getChildren()).toEqual([span('Count: 1')]); + + ReactNoop.render(null); + expect(Scheduler).toFlushAndYieldThrough(['Unmount: 1']); + expect(ReactNoop.getChildren()).toEqual([]); }); - return ; - } - act(() => { - ReactNoop.render(, () => - Scheduler.unstable_yieldValue('Sync effect'), - ); - expect(Scheduler).toFlushAndYieldThrough(['Count: 0', 'Sync effect']); - expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]); - ReactNoop.flushPassiveEffects(); - expect(Scheduler).toHaveYielded(['Mount A [0]', 'Mount B [0]']); }); - act(() => { - // This update will trigger an error - ReactNoop.render(, () => - Scheduler.unstable_yieldValue('Sync effect'), - ); - expect(Scheduler).toFlushAndYieldThrough(['Count: 1', 'Sync effect']); - expect(ReactNoop.getChildren()).toEqual([span('Count: 1')]); - expect(() => ReactNoop.flushPassiveEffects()).toThrow('Oops'); - expect(Scheduler).toHaveYielded([ - 'Unmount A [0]', - 'Unmount B [0]', - 'Mount A [1]', - 'Oops!', - ]); - expect(ReactNoop.getChildren()).toEqual([]); - }); - expect(Scheduler).toHaveYielded([ - // Clean up effect A runs passively on unmount. - // There's no effect B to clean-up, because it never mounted. - 'Unmount A [1]', - ]); - }); + describe('useLayoutEffect', () => { + it('fires layout effects after the host has been mutated', () => { + function getCommittedText() { + const yields = Scheduler.unstable_clearYields(); + const children = ReactNoop.getChildren(); + Scheduler.unstable_yieldValue(yields); + if (children === null) { + return null; + } + return children[0].prop; + } - it('handles errors on unmount', () => { - function Counter(props) { - useEffect(() => { - Scheduler.unstable_yieldValue(`Mount A [${props.count}]`); - return () => { - Scheduler.unstable_yieldValue('Oops!'); - throw new Error('Oops!'); - }; - }); - useEffect(() => { - Scheduler.unstable_yieldValue(`Mount B [${props.count}]`); - return () => { - Scheduler.unstable_yieldValue(`Unmount B [${props.count}]`); - }; - }); - return ; - } + function Counter(props) { + useLayoutEffect(() => { + Scheduler.unstable_yieldValue(`Current: ${getCommittedText()}`); + }); + return ; + } - act(() => { - ReactNoop.render(, () => - Scheduler.unstable_yieldValue('Sync effect'), - ); - expect(Scheduler).toFlushAndYieldThrough(['Count: 0', 'Sync effect']); - expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]); - ReactNoop.flushPassiveEffects(); - expect(Scheduler).toHaveYielded(['Mount A [0]', 'Mount B [0]']); - }); + ReactNoop.render(, () => + Scheduler.unstable_yieldValue('Sync effect'), + ); + expect(Scheduler).toFlushAndYieldThrough([ + [0], + 'Current: 0', + 'Sync effect', + ]); + expect(ReactNoop.getChildren()).toEqual([span(0)]); - act(() => { - // This update will trigger an error during passive effect unmount - ReactNoop.render(, () => - Scheduler.unstable_yieldValue('Sync effect'), - ); - expect(Scheduler).toFlushAndYieldThrough(['Count: 1', 'Sync effect']); - expect(ReactNoop.getChildren()).toEqual([span('Count: 1')]); - expect(() => ReactNoop.flushPassiveEffects()).toThrow('Oops'); - - // This tests enables a feature flag that flushes all passive destroys in a - // separate pass before flushing any passive creates. - // A result of this two-pass flush is that an error thrown from unmount does - // not block the subsequent create functions from being run. - expect(Scheduler).toHaveYielded([ - 'Oops!', - 'Unmount B [0]', - 'Mount A [1]', - 'Mount B [1]', - ]); - }); + ReactNoop.render(, () => + Scheduler.unstable_yieldValue('Sync effect'), + ); + expect(Scheduler).toFlushAndYieldThrough([ + [1], + 'Current: 1', + 'Sync effect', + ]); + expect(ReactNoop.getChildren()).toEqual([span(1)]); + }); - // gets unmounted because an error is thrown above. - // The remaining destroy functions are run later on unmount, since they're passive. - // In this case, one of them throws again (because of how the test is written). - expect(Scheduler).toHaveYielded(['Oops!', 'Unmount B [1]']); - expect(ReactNoop.getChildren()).toEqual([]); - }); + it('force flushes passive effects before firing new layout effects', () => { + let committedText = '(empty)'; + + function Counter(props) { + useLayoutEffect(() => { + // Normally this would go in a mutation effect, but this test + // intentionally omits a mutation effect. + committedText = props.count + ''; + + Scheduler.unstable_yieldValue( + `Mount layout [current: ${committedText}]`, + ); + return () => { + Scheduler.unstable_yieldValue( + `Unmount layout [current: ${committedText}]`, + ); + }; + }); + useEffect(() => { + Scheduler.unstable_yieldValue( + `Mount normal [current: ${committedText}]`, + ); + return () => { + Scheduler.unstable_yieldValue( + `Unmount normal [current: ${committedText}]`, + ); + }; + }); + return null; + } + + act(() => { + ReactNoop.render(, () => + Scheduler.unstable_yieldValue('Sync effect'), + ); + expect(Scheduler).toFlushAndYieldThrough([ + 'Mount layout [current: 0]', + 'Sync effect', + ]); + expect(committedText).toEqual('0'); + ReactNoop.render(, () => + Scheduler.unstable_yieldValue('Sync effect'), + ); + expect(Scheduler).toFlushAndYieldThrough([ + 'Mount normal [current: 0]', + 'Unmount layout [current: 0]', + 'Mount layout [current: 1]', + 'Sync effect', + ]); + expect(committedText).toEqual('1'); + }); - it('works with memo', () => { - function Counter({count}) { - useLayoutEffect(() => { - Scheduler.unstable_yieldValue('Mount: ' + count); - return () => Scheduler.unstable_yieldValue('Unmount: ' + count); + expect(Scheduler).toHaveYielded([ + 'Unmount normal [current: 1]', + 'Mount normal [current: 1]', + ]); }); - return ; - } - Counter = memo(Counter); - - ReactNoop.render(, () => - Scheduler.unstable_yieldValue('Sync effect'), - ); - expect(Scheduler).toFlushAndYieldThrough([ - 'Count: 0', - 'Mount: 0', - 'Sync effect', - ]); - expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]); - - ReactNoop.render(, () => - Scheduler.unstable_yieldValue('Sync effect'), - ); - expect(Scheduler).toFlushAndYieldThrough([ - 'Count: 1', - 'Unmount: 0', - 'Mount: 1', - 'Sync effect', - ]); - expect(ReactNoop.getChildren()).toEqual([span('Count: 1')]); - - ReactNoop.render(null); - expect(Scheduler).toFlushAndYieldThrough(['Unmount: 1']); - expect(ReactNoop.getChildren()).toEqual([]); - }); - }); + }); - describe('useLayoutEffect', () => { - it('fires layout effects after the host has been mutated', () => { - function getCommittedText() { - const yields = Scheduler.unstable_clearYields(); - const children = ReactNoop.getChildren(); - Scheduler.unstable_yieldValue(yields); - if (children === null) { - return null; - } - return children[0].prop; - } + describe('useCallback', () => { + it('memoizes callback by comparing inputs', () => { + class IncrementButton extends React.PureComponent { + increment = () => { + this.props.increment(); + }; + render() { + return ; + } + } - function Counter(props) { - useLayoutEffect(() => { - Scheduler.unstable_yieldValue(`Current: ${getCommittedText()}`); - }); - return ; - } + function Counter({incrementBy}) { + const [count, updateCount] = useState(0); + const increment = useCallback( + () => updateCount(c => c + incrementBy), + [incrementBy], + ); + return ( + <> + + + + ); + } - ReactNoop.render(, () => - Scheduler.unstable_yieldValue('Sync effect'), - ); - expect(Scheduler).toFlushAndYieldThrough([ - [0], - 'Current: 0', - 'Sync effect', - ]); - expect(ReactNoop.getChildren()).toEqual([span(0)]); - - ReactNoop.render(, () => - Scheduler.unstable_yieldValue('Sync effect'), - ); - expect(Scheduler).toFlushAndYieldThrough([ - [1], - 'Current: 1', - 'Sync effect', - ]); - expect(ReactNoop.getChildren()).toEqual([span(1)]); - }); + const button = React.createRef(null); + ReactNoop.render(); + expect(Scheduler).toFlushAndYield(['Increment', 'Count: 0']); + expect(ReactNoop.getChildren()).toEqual([ + span('Increment'), + span('Count: 0'), + ]); - it('force flushes passive effects before firing new layout effects', () => { - let committedText = '(empty)'; + act(button.current.increment); + expect(Scheduler).toHaveYielded([ + // Button should not re-render, because its props haven't changed + // 'Increment', + 'Count: 1', + ]); + expect(ReactNoop.getChildren()).toEqual([ + span('Increment'), + span('Count: 1'), + ]); - function Counter(props) { - useLayoutEffect(() => { - // Normally this would go in a mutation effect, but this test - // intentionally omits a mutation effect. - committedText = props.count + ''; + // Increase the increment amount + ReactNoop.render(); + expect(Scheduler).toFlushAndYield([ + // Inputs did change this time + 'Increment', + 'Count: 1', + ]); + expect(ReactNoop.getChildren()).toEqual([ + span('Increment'), + span('Count: 1'), + ]); - Scheduler.unstable_yieldValue( - `Mount layout [current: ${committedText}]`, - ); - return () => { - Scheduler.unstable_yieldValue( - `Unmount layout [current: ${committedText}]`, - ); - }; - }); - useEffect(() => { - Scheduler.unstable_yieldValue( - `Mount normal [current: ${committedText}]`, - ); - return () => { - Scheduler.unstable_yieldValue( - `Unmount normal [current: ${committedText}]`, - ); - }; + // Callback should have updated + act(button.current.increment); + expect(Scheduler).toHaveYielded(['Count: 11']); + expect(ReactNoop.getChildren()).toEqual([ + span('Increment'), + span('Count: 11'), + ]); }); - return null; - } - - act(() => { - ReactNoop.render(, () => - Scheduler.unstable_yieldValue('Sync effect'), - ); - expect(Scheduler).toFlushAndYieldThrough([ - 'Mount layout [current: 0]', - 'Sync effect', - ]); - expect(committedText).toEqual('0'); - ReactNoop.render(, () => - Scheduler.unstable_yieldValue('Sync effect'), - ); - expect(Scheduler).toFlushAndYieldThrough([ - 'Mount normal [current: 0]', - 'Unmount layout [current: 0]', - 'Mount layout [current: 1]', - 'Sync effect', - ]); - expect(committedText).toEqual('1'); }); - expect(Scheduler).toHaveYielded([ - 'Unmount normal [current: 1]', - 'Mount normal [current: 1]', - ]); - }); - }); + describe('useMemo', () => { + it('memoizes value by comparing to previous inputs', () => { + function CapitalizedText(props) { + const text = props.text; + const capitalizedText = useMemo(() => { + Scheduler.unstable_yieldValue(`Capitalize '${text}'`); + return text.toUpperCase(); + }, [text]); + return ; + } - describe('useCallback', () => { - it('memoizes callback by comparing inputs', () => { - class IncrementButton extends React.PureComponent { - increment = () => { - this.props.increment(); - }; - render() { - return ; - } - } + ReactNoop.render(); + expect(Scheduler).toFlushAndYield(["Capitalize 'hello'", 'HELLO']); + expect(ReactNoop.getChildren()).toEqual([span('HELLO')]); - function Counter({incrementBy}) { - const [count, updateCount] = useState(0); - const increment = useCallback(() => updateCount(c => c + incrementBy), [ - incrementBy, - ]); - return ( - <> - - - - ); - } + ReactNoop.render(); + expect(Scheduler).toFlushAndYield(["Capitalize 'hi'", 'HI']); + expect(ReactNoop.getChildren()).toEqual([span('HI')]); - const button = React.createRef(null); - ReactNoop.render(); - expect(Scheduler).toFlushAndYield(['Increment', 'Count: 0']); - expect(ReactNoop.getChildren()).toEqual([ - span('Increment'), - span('Count: 0'), - ]); - - act(button.current.increment); - expect(Scheduler).toHaveYielded([ - // Button should not re-render, because its props haven't changed - // 'Increment', - 'Count: 1', - ]); - expect(ReactNoop.getChildren()).toEqual([ - span('Increment'), - span('Count: 1'), - ]); - - // Increase the increment amount - ReactNoop.render(); - expect(Scheduler).toFlushAndYield([ - // Inputs did change this time - 'Increment', - 'Count: 1', - ]); - expect(ReactNoop.getChildren()).toEqual([ - span('Increment'), - span('Count: 1'), - ]); - - // Callback should have updated - act(button.current.increment); - expect(Scheduler).toHaveYielded(['Count: 11']); - expect(ReactNoop.getChildren()).toEqual([ - span('Increment'), - span('Count: 11'), - ]); - }); - }); + ReactNoop.render(); + expect(Scheduler).toFlushAndYield(['HI']); + expect(ReactNoop.getChildren()).toEqual([span('HI')]); - describe('useMemo', () => { - it('memoizes value by comparing to previous inputs', () => { - function CapitalizedText(props) { - const text = props.text; - const capitalizedText = useMemo(() => { - Scheduler.unstable_yieldValue(`Capitalize '${text}'`); - return text.toUpperCase(); - }, [text]); - return ; - } + ReactNoop.render(); + expect(Scheduler).toFlushAndYield([ + "Capitalize 'goodbye'", + 'GOODBYE', + ]); + expect(ReactNoop.getChildren()).toEqual([span('GOODBYE')]); + }); - ReactNoop.render(); - expect(Scheduler).toFlushAndYield(["Capitalize 'hello'", 'HELLO']); - expect(ReactNoop.getChildren()).toEqual([span('HELLO')]); + it('always re-computes if no inputs are provided', () => { + function LazyCompute(props) { + const computed = useMemo(props.compute); + return ; + } - ReactNoop.render(); - expect(Scheduler).toFlushAndYield(["Capitalize 'hi'", 'HI']); - expect(ReactNoop.getChildren()).toEqual([span('HI')]); + function computeA() { + Scheduler.unstable_yieldValue('compute A'); + return 'A'; + } - ReactNoop.render(); - expect(Scheduler).toFlushAndYield(['HI']); - expect(ReactNoop.getChildren()).toEqual([span('HI')]); + function computeB() { + Scheduler.unstable_yieldValue('compute B'); + return 'B'; + } - ReactNoop.render(); - expect(Scheduler).toFlushAndYield(["Capitalize 'goodbye'", 'GOODBYE']); - expect(ReactNoop.getChildren()).toEqual([span('GOODBYE')]); - }); + ReactNoop.render(); + expect(Scheduler).toFlushAndYield(['compute A', 'A']); - it('always re-computes if no inputs are provided', () => { - function LazyCompute(props) { - const computed = useMemo(props.compute); - return ; - } + ReactNoop.render(); + expect(Scheduler).toFlushAndYield(['compute A', 'A']); - function computeA() { - Scheduler.unstable_yieldValue('compute A'); - return 'A'; - } + ReactNoop.render(); + expect(Scheduler).toFlushAndYield(['compute A', 'A']); - function computeB() { - Scheduler.unstable_yieldValue('compute B'); - return 'B'; - } + ReactNoop.render(); + expect(Scheduler).toFlushAndYield(['compute B', 'B']); + }); - ReactNoop.render(); - expect(Scheduler).toFlushAndYield(['compute A', 'A']); + it('should not invoke memoized function during re-renders unless inputs change', () => { + function LazyCompute(props) { + const computed = useMemo(() => props.compute(props.input), [ + props.input, + ]); + const [count, setCount] = useState(0); + if (count < 3) { + setCount(count + 1); + } + return ; + } - ReactNoop.render(); - expect(Scheduler).toFlushAndYield(['compute A', 'A']); + function compute(val) { + Scheduler.unstable_yieldValue('compute ' + val); + return val; + } - ReactNoop.render(); - expect(Scheduler).toFlushAndYield(['compute A', 'A']); + ReactNoop.render(); + expect(Scheduler).toFlushAndYield(['compute A', 'A']); - ReactNoop.render(); - expect(Scheduler).toFlushAndYield(['compute B', 'B']); - }); + ReactNoop.render(); + expect(Scheduler).toFlushAndYield(['A']); - it('should not invoke memoized function during re-renders unless inputs change', () => { - function LazyCompute(props) { - const computed = useMemo(() => props.compute(props.input), [ - props.input, - ]); - const [count, setCount] = useState(0); - if (count < 3) { - setCount(count + 1); - } - return ; - } + ReactNoop.render(); + expect(Scheduler).toFlushAndYield(['compute B', 'B']); + }); + }); - function compute(val) { - Scheduler.unstable_yieldValue('compute ' + val); - return val; - } + describe('useRef', () => { + it('creates a ref object initialized with the provided value', () => { + jest.useFakeTimers(); + + function useDebouncedCallback(callback, ms, inputs) { + const timeoutID = useRef(-1); + useEffect(() => { + return function unmount() { + clearTimeout(timeoutID.current); + }; + }, []); + const debouncedCallback = useCallback( + (...args) => { + clearTimeout(timeoutID.current); + timeoutID.current = setTimeout(callback, ms, ...args); + }, + [callback, ms], + ); + return useCallback(debouncedCallback, inputs); + } - ReactNoop.render(); - expect(Scheduler).toFlushAndYield(['compute A', 'A']); + let ping; + function App() { + ping = useDebouncedCallback( + value => { + Scheduler.unstable_yieldValue('ping: ' + value); + }, + 100, + [], + ); + return null; + } - ReactNoop.render(); - expect(Scheduler).toFlushAndYield(['A']); + act(() => { + ReactNoop.render(); + }); + expect(Scheduler).toHaveYielded([]); - ReactNoop.render(); - expect(Scheduler).toFlushAndYield(['compute B', 'B']); - }); - }); + ping(1); + ping(2); + ping(3); - describe('useRef', () => { - it('creates a ref object initialized with the provided value', () => { - jest.useFakeTimers(); + expect(Scheduler).toHaveYielded([]); - function useDebouncedCallback(callback, ms, inputs) { - const timeoutID = useRef(-1); - useEffect(() => { - return function unmount() { - clearTimeout(timeoutID.current); - }; - }, []); - const debouncedCallback = useCallback( - (...args) => { - clearTimeout(timeoutID.current); - timeoutID.current = setTimeout(callback, ms, ...args); - }, - [callback, ms], - ); - return useCallback(debouncedCallback, inputs); - } + jest.advanceTimersByTime(100); - let ping; - function App() { - ping = useDebouncedCallback( - value => { - Scheduler.unstable_yieldValue('ping: ' + value); - }, - 100, - [], - ); - return null; - } + expect(Scheduler).toHaveYielded(['ping: 3']); - act(() => { - ReactNoop.render(); - }); - expect(Scheduler).toHaveYielded([]); + ping(4); + jest.advanceTimersByTime(20); + ping(5); + ping(6); + jest.advanceTimersByTime(80); - ping(1); - ping(2); - ping(3); + expect(Scheduler).toHaveYielded([]); - expect(Scheduler).toHaveYielded([]); + jest.advanceTimersByTime(20); + expect(Scheduler).toHaveYielded(['ping: 6']); + }); - jest.advanceTimersByTime(100); + it('should return the same ref during re-renders', () => { + function Counter() { + const ref = useRef('val'); + const [count, setCount] = useState(0); + const [firstRef] = useState(ref); - expect(Scheduler).toHaveYielded(['ping: 3']); + if (firstRef !== ref) { + throw new Error('should never change'); + } - ping(4); - jest.advanceTimersByTime(20); - ping(5); - ping(6); - jest.advanceTimersByTime(80); + if (count < 3) { + setCount(count + 1); + } - expect(Scheduler).toHaveYielded([]); + return ; + } - jest.advanceTimersByTime(20); - expect(Scheduler).toHaveYielded(['ping: 6']); - }); + ReactNoop.render(); + expect(Scheduler).toFlushAndYield(['val']); - it('should return the same ref during re-renders', () => { - function Counter() { - const ref = useRef('val'); - const [count, setCount] = useState(0); - const [firstRef] = useState(ref); + ReactNoop.render(); + expect(Scheduler).toFlushAndYield(['val']); + }); + }); - if (firstRef !== ref) { - throw new Error('should never change'); - } + describe('useImperativeHandle', () => { + it('does not update when deps are the same', () => { + const INCREMENT = 'INCREMENT'; - if (count < 3) { - setCount(count + 1); - } + function reducer(state, action) { + return action === INCREMENT ? state + 1 : state; + } - return ; - } + function Counter(props, ref) { + const [count, dispatch] = useReducer(reducer, 0); + useImperativeHandle(ref, () => ({count, dispatch}), []); + return ; + } - ReactNoop.render(); - expect(Scheduler).toFlushAndYield(['val']); + Counter = forwardRef(Counter); + const counter = React.createRef(null); + ReactNoop.render(); + expect(Scheduler).toFlushAndYield(['Count: 0']); + expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]); + expect(counter.current.count).toBe(0); - ReactNoop.render(); - expect(Scheduler).toFlushAndYield(['val']); - }); - }); + act(() => { + counter.current.dispatch(INCREMENT); + }); + expect(Scheduler).toHaveYielded(['Count: 1']); + expect(ReactNoop.getChildren()).toEqual([span('Count: 1')]); + // Intentionally not updated because of [] deps: + expect(counter.current.count).toBe(0); + }); - describe('useImperativeHandle', () => { - it('does not update when deps are the same', () => { - const INCREMENT = 'INCREMENT'; + // Regression test for https://github.com/facebook/react/issues/14782 + it('automatically updates when deps are not specified', () => { + const INCREMENT = 'INCREMENT'; - function reducer(state, action) { - return action === INCREMENT ? state + 1 : state; - } + function reducer(state, action) { + return action === INCREMENT ? state + 1 : state; + } - function Counter(props, ref) { - const [count, dispatch] = useReducer(reducer, 0); - useImperativeHandle(ref, () => ({count, dispatch}), []); - return ; - } + function Counter(props, ref) { + const [count, dispatch] = useReducer(reducer, 0); + useImperativeHandle(ref, () => ({count, dispatch})); + return ; + } - Counter = forwardRef(Counter); - const counter = React.createRef(null); - ReactNoop.render(); - expect(Scheduler).toFlushAndYield(['Count: 0']); - expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]); - expect(counter.current.count).toBe(0); + Counter = forwardRef(Counter); + const counter = React.createRef(null); + ReactNoop.render(); + expect(Scheduler).toFlushAndYield(['Count: 0']); + expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]); + expect(counter.current.count).toBe(0); - act(() => { - counter.current.dispatch(INCREMENT); - }); - expect(Scheduler).toHaveYielded(['Count: 1']); - expect(ReactNoop.getChildren()).toEqual([span('Count: 1')]); - // Intentionally not updated because of [] deps: - expect(counter.current.count).toBe(0); - }); + act(() => { + counter.current.dispatch(INCREMENT); + }); + expect(Scheduler).toHaveYielded(['Count: 1']); + expect(ReactNoop.getChildren()).toEqual([span('Count: 1')]); + expect(counter.current.count).toBe(1); + }); - // Regression test for https://github.com/facebook/react/issues/14782 - it('automatically updates when deps are not specified', () => { - const INCREMENT = 'INCREMENT'; + it('updates when deps are different', () => { + const INCREMENT = 'INCREMENT'; - function reducer(state, action) { - return action === INCREMENT ? state + 1 : state; - } + function reducer(state, action) { + return action === INCREMENT ? state + 1 : state; + } - function Counter(props, ref) { - const [count, dispatch] = useReducer(reducer, 0); - useImperativeHandle(ref, () => ({count, dispatch})); - return ; - } + let totalRefUpdates = 0; + function Counter(props, ref) { + const [count, dispatch] = useReducer(reducer, 0); + useImperativeHandle( + ref, + () => { + totalRefUpdates++; + return {count, dispatch}; + }, + [count], + ); + return ; + } - Counter = forwardRef(Counter); - const counter = React.createRef(null); - ReactNoop.render(); - expect(Scheduler).toFlushAndYield(['Count: 0']); - expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]); - expect(counter.current.count).toBe(0); + Counter = forwardRef(Counter); + const counter = React.createRef(null); + ReactNoop.render(); + expect(Scheduler).toFlushAndYield(['Count: 0']); + expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]); + expect(counter.current.count).toBe(0); + expect(totalRefUpdates).toBe(1); - act(() => { - counter.current.dispatch(INCREMENT); + act(() => { + counter.current.dispatch(INCREMENT); + }); + expect(Scheduler).toHaveYielded(['Count: 1']); + expect(ReactNoop.getChildren()).toEqual([span('Count: 1')]); + expect(counter.current.count).toBe(1); + expect(totalRefUpdates).toBe(2); + + // Update that doesn't change the ref dependencies + ReactNoop.render(); + expect(Scheduler).toFlushAndYield(['Count: 1']); + expect(ReactNoop.getChildren()).toEqual([span('Count: 1')]); + expect(counter.current.count).toBe(1); + expect(totalRefUpdates).toBe(2); // Should not increase since last time + }); }); - expect(Scheduler).toHaveYielded(['Count: 1']); - expect(ReactNoop.getChildren()).toEqual([span('Count: 1')]); - expect(counter.current.count).toBe(1); - }); + describe('useTransition', () => { + it.experimental( + 'delays showing loading state until after timeout', + async () => { + let transition; + function App() { + const [show, setShow] = useState(false); + const [startTransition, isPending] = useTransition({ + timeoutMs: 1000, + }); + transition = () => { + startTransition(() => { + setShow(true); + }); + }; + return ( + }> + {show ? ( + + ) : ( + + )} + + ); + } + ReactNoop.render(); + expect(Scheduler).toFlushAndYield(['Before... Pending: false']); + expect(ReactNoop.getChildren()).toEqual([ + span('Before... Pending: false'), + ]); + + act(() => { + Scheduler.unstable_runWithPriority( + Scheduler.unstable_UserBlockingPriority, + transition, + ); + }); + Scheduler.unstable_advanceTime(500); + await advanceTimers(500); + expect(Scheduler).toHaveYielded([ + 'Before... Pending: true', + 'Suspend! [After... Pending: false]', + 'Loading... Pending: false', + ]); + expect(ReactNoop.getChildren()).toEqual([ + span('Before... Pending: true'), + ]); + + Scheduler.unstable_advanceTime(1000); + await advanceTimers(1000); + expect(ReactNoop.getChildren()).toEqual([ + hiddenSpan('Before... Pending: true'), + span('Loading... Pending: false'), + ]); + + Scheduler.unstable_advanceTime(500); + await advanceTimers(500); + expect(Scheduler).toHaveYielded([ + 'Promise resolved [After... Pending: false]', + ]); + expect(Scheduler).toFlushAndYield(['After... Pending: false']); + expect(ReactNoop.getChildren()).toEqual([ + span('After... Pending: false'), + ]); + }, + ); + it.experimental( + 'delays showing loading state until after busyDelayMs + busyMinDurationMs', + async () => { + let transition; + function App() { + const [show, setShow] = useState(false); + const [startTransition, isPending] = useTransition({ + busyDelayMs: 1000, + busyMinDurationMs: 2000, + }); + transition = () => { + startTransition(() => { + setShow(true); + }); + }; + return ( + }> + {show ? ( + + ) : ( + + )} + + ); + } + ReactNoop.render(); + expect(Scheduler).toFlushAndYield(['Before... Pending: false']); + expect(ReactNoop.getChildren()).toEqual([ + span('Before... Pending: false'), + ]); + + act(() => { + Scheduler.unstable_runWithPriority( + Scheduler.unstable_UserBlockingPriority, + transition, + ); + }); + Scheduler.unstable_advanceTime(1000); + await advanceTimers(1000); + expect(Scheduler).toHaveYielded([ + 'Before... Pending: true', + 'Suspend! [After... Pending: false]', + 'Loading... Pending: false', + ]); + expect(ReactNoop.getChildren()).toEqual([ + span('Before... Pending: true'), + ]); + + Scheduler.unstable_advanceTime(1000); + await advanceTimers(1000); + expect(Scheduler).toHaveYielded([ + 'Promise resolved [After... Pending: false]', + ]); + expect(Scheduler).toFlushAndYield(['After... Pending: false']); + expect(ReactNoop.getChildren()).toEqual([ + span('Before... Pending: true'), + ]); + + Scheduler.unstable_advanceTime(1000); + await advanceTimers(1000); + expect(ReactNoop.getChildren()).toEqual([ + span('Before... Pending: true'), + ]); + Scheduler.unstable_advanceTime(250); + await advanceTimers(250); + expect(ReactNoop.getChildren()).toEqual([ + span('After... Pending: false'), + ]); + }, + ); + }); + describe('useDeferredValue', () => { + it.experimental( + 'defers text value until specified timeout', + async () => { + function TextBox({text}) { + return ; + } + + let _setText; + function App() { + const [text, setText] = useState('A'); + const deferredText = useDeferredValue(text, { + timeoutMs: 500, + }); + _setText = setText; + return ( + <> + + }> + + + + ); + } + + act(() => { + ReactNoop.render(); + }); - it('updates when deps are different', () => { - const INCREMENT = 'INCREMENT'; + expect(Scheduler).toHaveYielded(['A', 'Suspend! [A]', 'Loading']); + expect(ReactNoop.getChildren()).toEqual([ + span('A'), + span('Loading'), + ]); - function reducer(state, action) { - return action === INCREMENT ? state + 1 : state; - } + Scheduler.unstable_advanceTime(1000); + await advanceTimers(1000); + expect(Scheduler).toHaveYielded(['Promise resolved [A]']); + expect(Scheduler).toFlushAndYield(['A']); + expect(ReactNoop.getChildren()).toEqual([span('A'), span('A')]); - let totalRefUpdates = 0; - function Counter(props, ref) { - const [count, dispatch] = useReducer(reducer, 0); - useImperativeHandle( - ref, - () => { - totalRefUpdates++; - return {count, dispatch}; + act(() => { + _setText('B'); + }); + expect(Scheduler).toHaveYielded([ + 'B', + 'A', + 'B', + 'Suspend! [B]', + 'Loading', + ]); + expect(Scheduler).toFlushAndYield([]); + expect(ReactNoop.getChildren()).toEqual([span('B'), span('A')]); + + Scheduler.unstable_advanceTime(250); + await advanceTimers(250); + expect(Scheduler).toFlushAndYield([]); + expect(ReactNoop.getChildren()).toEqual([span('B'), span('A')]); + + Scheduler.unstable_advanceTime(500); + await advanceTimers(500); + expect(ReactNoop.getChildren()).toEqual([ + span('B'), + hiddenSpan('A'), + span('Loading'), + ]); + + Scheduler.unstable_advanceTime(250); + await advanceTimers(250); + expect(Scheduler).toHaveYielded(['Promise resolved [B]']); + + act(() => { + expect(Scheduler).toFlushAndYield(['B']); + }); + expect(ReactNoop.getChildren()).toEqual([span('B'), span('B')]); }, - [count], ); - return ; - } - - Counter = forwardRef(Counter); - const counter = React.createRef(null); - ReactNoop.render(); - expect(Scheduler).toFlushAndYield(['Count: 0']); - expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]); - expect(counter.current.count).toBe(0); - expect(totalRefUpdates).toBe(1); - - act(() => { - counter.current.dispatch(INCREMENT); }); - expect(Scheduler).toHaveYielded(['Count: 1']); - expect(ReactNoop.getChildren()).toEqual([span('Count: 1')]); - expect(counter.current.count).toBe(1); - expect(totalRefUpdates).toBe(2); - - // Update that doesn't change the ref dependencies - ReactNoop.render(); - expect(Scheduler).toFlushAndYield(['Count: 1']); - expect(ReactNoop.getChildren()).toEqual([span('Count: 1')]); - expect(counter.current.count).toBe(1); - expect(totalRefUpdates).toBe(2); // Should not increase since last time - }); - }); - describe('useTransition', () => { - it.experimental( - 'delays showing loading state until after timeout', - async () => { - let transition; - function App() { - const [show, setShow] = useState(false); - const [startTransition, isPending] = useTransition({ - timeoutMs: 1000, - }); - transition = () => { - startTransition(() => { - setShow(true); - }); - }; - return ( - }> - {show ? ( - - ) : ( - - )} - - ); - } - ReactNoop.render(); - expect(Scheduler).toFlushAndYield(['Before... Pending: false']); - expect(ReactNoop.getChildren()).toEqual([ - span('Before... Pending: false'), - ]); - act(() => { - Scheduler.unstable_runWithPriority( - Scheduler.unstable_UserBlockingPriority, - transition, - ); - }); - Scheduler.unstable_advanceTime(500); - await advanceTimers(500); - expect(Scheduler).toHaveYielded([ - 'Before... Pending: true', - 'Suspend! [After... Pending: false]', - 'Loading... Pending: false', - ]); - expect(ReactNoop.getChildren()).toEqual([ - span('Before... Pending: true'), - ]); + describe('progressive enhancement (not supported)', () => { + it('mount additional state', () => { + let updateA; + let updateB; + // let updateC; + + function App(props) { + const [A, _updateA] = useState(0); + const [B, _updateB] = useState(0); + updateA = _updateA; + updateB = _updateB; + + let C; + if (props.loadC) { + useState(0); + } else { + C = '[not loaded]'; + } + + return ; + } - Scheduler.unstable_advanceTime(1000); - await advanceTimers(1000); - expect(ReactNoop.getChildren()).toEqual([ - hiddenSpan('Before... Pending: true'), - span('Loading... Pending: false'), - ]); + ReactNoop.render(); + expect(Scheduler).toFlushAndYield(['A: 0, B: 0, C: [not loaded]']); + expect(ReactNoop.getChildren()).toEqual([ + span('A: 0, B: 0, C: [not loaded]'), + ]); - Scheduler.unstable_advanceTime(500); - await advanceTimers(500); - expect(Scheduler).toHaveYielded([ - 'Promise resolved [After... Pending: false]', - ]); - expect(Scheduler).toFlushAndYield(['After... Pending: false']); - expect(ReactNoop.getChildren()).toEqual([ - span('After... Pending: false'), - ]); - }, - ); - it.experimental( - 'delays showing loading state until after busyDelayMs + busyMinDurationMs', - async () => { - let transition; - function App() { - const [show, setShow] = useState(false); - const [startTransition, isPending] = useTransition({ - busyDelayMs: 1000, - busyMinDurationMs: 2000, + act(() => { + updateA(2); + updateB(3); }); - transition = () => { - startTransition(() => { - setShow(true); - }); - }; - return ( - }> - {show ? ( - - ) : ( - - )} - - ); - } - ReactNoop.render(); - expect(Scheduler).toFlushAndYield(['Before... Pending: false']); - expect(ReactNoop.getChildren()).toEqual([ - span('Before... Pending: false'), - ]); - act(() => { - Scheduler.unstable_runWithPriority( - Scheduler.unstable_UserBlockingPriority, - transition, - ); - }); - Scheduler.unstable_advanceTime(1000); - await advanceTimers(1000); - expect(Scheduler).toHaveYielded([ - 'Before... Pending: true', - 'Suspend! [After... Pending: false]', - 'Loading... Pending: false', - ]); - expect(ReactNoop.getChildren()).toEqual([ - span('Before... Pending: true'), - ]); + expect(Scheduler).toHaveYielded(['A: 2, B: 3, C: [not loaded]']); + expect(ReactNoop.getChildren()).toEqual([ + span('A: 2, B: 3, C: [not loaded]'), + ]); - Scheduler.unstable_advanceTime(1000); - await advanceTimers(1000); - expect(Scheduler).toHaveYielded([ - 'Promise resolved [After... Pending: false]', - ]); - expect(Scheduler).toFlushAndYield(['After... Pending: false']); - expect(ReactNoop.getChildren()).toEqual([ - span('Before... Pending: true'), - ]); + ReactNoop.render(); + expect(() => { + expect(() => { + expect(Scheduler).toFlushAndYield(['A: 2, B: 3, C: 0']); + }).toThrow('Rendered more hooks than during the previous render'); + }).toErrorDev([ + 'Warning: React has detected a change in the order of Hooks called by App. ' + + 'This will lead to bugs and errors if not fixed. For more information, ' + + 'read the Rules of Hooks: https://fb.me/rules-of-hooks\n\n' + + ' Previous render Next render\n' + + ' ------------------------------------------------------\n' + + '1. useState useState\n' + + '2. useState useState\n' + + '3. undefined useState\n' + + ' ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n', + ]); - Scheduler.unstable_advanceTime(1000); - await advanceTimers(1000); - expect(ReactNoop.getChildren()).toEqual([ - span('Before... Pending: true'), - ]); - Scheduler.unstable_advanceTime(250); - await advanceTimers(250); - expect(ReactNoop.getChildren()).toEqual([ - span('After... Pending: false'), - ]); - }, - ); - }); - describe('useDeferredValue', () => { - it.experimental('defers text value until specified timeout', async () => { - function TextBox({text}) { - return ; - } + // Uncomment if/when we support this again + // expect(ReactNoop.getChildren()).toEqual([span('A: 2, B: 3, C: 0')]); - let _setText; - function App() { - const [text, setText] = useState('A'); - const deferredText = useDeferredValue(text, { - timeoutMs: 500, + // updateC(4); + // expect(Scheduler).toFlushAndYield(['A: 2, B: 3, C: 4']); + // expect(ReactNoop.getChildren()).toEqual([span('A: 2, B: 3, C: 4')]); }); - _setText = setText; - return ( - <> - - }> - - - - ); - } - act(() => { - ReactNoop.render(); - }); + it('unmount state', () => { + let updateA; + let updateB; + let updateC; + + function App(props) { + const [A, _updateA] = useState(0); + const [B, _updateB] = useState(0); + updateA = _updateA; + updateB = _updateB; + + let C; + if (props.loadC) { + const [_C, _updateC] = useState(0); + C = _C; + updateC = _updateC; + } else { + C = '[not loaded]'; + } + + return ; + } - expect(Scheduler).toHaveYielded(['A', 'Suspend! [A]', 'Loading']); - expect(ReactNoop.getChildren()).toEqual([span('A'), span('Loading')]); + ReactNoop.render(); + expect(Scheduler).toFlushAndYield(['A: 0, B: 0, C: 0']); + expect(ReactNoop.getChildren()).toEqual([span('A: 0, B: 0, C: 0')]); + act(() => { + updateA(2); + updateB(3); + updateC(4); + }); + expect(Scheduler).toHaveYielded(['A: 2, B: 3, C: 4']); + expect(ReactNoop.getChildren()).toEqual([span('A: 2, B: 3, C: 4')]); + ReactNoop.render(); + expect(Scheduler).toFlushAndThrow( + 'Rendered fewer hooks than expected. This may be caused by an ' + + 'accidental early return statement.', + ); + }); - Scheduler.unstable_advanceTime(1000); - await advanceTimers(1000); - expect(Scheduler).toHaveYielded(['Promise resolved [A]']); - expect(Scheduler).toFlushAndYield(['A']); - expect(ReactNoop.getChildren()).toEqual([span('A'), span('A')]); + it('unmount effects', () => { + function App(props) { + useEffect(() => { + Scheduler.unstable_yieldValue('Mount A'); + return () => { + Scheduler.unstable_yieldValue('Unmount A'); + }; + }, []); + + if (props.showMore) { + useEffect(() => { + Scheduler.unstable_yieldValue('Mount B'); + return () => { + Scheduler.unstable_yieldValue('Unmount B'); + }; + }, []); + } - act(() => { - _setText('B'); - }); - expect(Scheduler).toHaveYielded([ - 'B', - 'A', - 'B', - 'Suspend! [B]', - 'Loading', - ]); - expect(Scheduler).toFlushAndYield([]); - expect(ReactNoop.getChildren()).toEqual([span('B'), span('A')]); - - Scheduler.unstable_advanceTime(250); - await advanceTimers(250); - expect(Scheduler).toFlushAndYield([]); - expect(ReactNoop.getChildren()).toEqual([span('B'), span('A')]); - - Scheduler.unstable_advanceTime(500); - await advanceTimers(500); - expect(ReactNoop.getChildren()).toEqual([ - span('B'), - hiddenSpan('A'), - span('Loading'), - ]); - - Scheduler.unstable_advanceTime(250); - await advanceTimers(250); - expect(Scheduler).toHaveYielded(['Promise resolved [B]']); - - act(() => { - expect(Scheduler).toFlushAndYield(['B']); - }); - expect(ReactNoop.getChildren()).toEqual([span('B'), span('B')]); - }); - }); + return null; + } - describe('progressive enhancement (not supported)', () => { - it('mount additional state', () => { - let updateA; - let updateB; - // let updateC; - - function App(props) { - const [A, _updateA] = useState(0); - const [B, _updateB] = useState(0); - updateA = _updateA; - updateB = _updateB; - - let C; - if (props.loadC) { - useState(0); - } else { - C = '[not loaded]'; - } + act(() => { + ReactNoop.render(, () => + Scheduler.unstable_yieldValue('Sync effect'), + ); + expect(Scheduler).toFlushAndYieldThrough(['Sync effect']); + }); - return ; - } + expect(Scheduler).toHaveYielded(['Mount A']); + + act(() => { + ReactNoop.render(); + expect(() => { + expect(() => { + expect(Scheduler).toFlushAndYield([]); + }).toThrow('Rendered more hooks than during the previous render'); + }).toErrorDev([ + 'Warning: React has detected a change in the order of Hooks called by App. ' + + 'This will lead to bugs and errors if not fixed. For more information, ' + + 'read the Rules of Hooks: https://fb.me/rules-of-hooks\n\n' + + ' Previous render Next render\n' + + ' ------------------------------------------------------\n' + + '1. useEffect useEffect\n' + + '2. undefined useEffect\n' + + ' ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n', + ]); + }); - ReactNoop.render(); - expect(Scheduler).toFlushAndYield(['A: 0, B: 0, C: [not loaded]']); - expect(ReactNoop.getChildren()).toEqual([ - span('A: 0, B: 0, C: [not loaded]'), - ]); + // Uncomment if/when we support this again + // ReactNoop.flushPassiveEffects(); + // expect(Scheduler).toHaveYielded(['Mount B']); - act(() => { - updateA(2); - updateB(3); + // ReactNoop.render(); + // expect(Scheduler).toFlushAndThrow( + // 'Rendered fewer hooks than expected. This may be caused by an ' + + // 'accidental early return statement.', + // ); + }); }); - expect(Scheduler).toHaveYielded(['A: 2, B: 3, C: [not loaded]']); - expect(ReactNoop.getChildren()).toEqual([ - span('A: 2, B: 3, C: [not loaded]'), - ]); - - ReactNoop.render(); - expect(() => { - expect(() => { - expect(Scheduler).toFlushAndYield(['A: 2, B: 3, C: 0']); - }).toThrow('Rendered more hooks than during the previous render'); - }).toErrorDev([ - 'Warning: React has detected a change in the order of Hooks called by App. ' + - 'This will lead to bugs and errors if not fixed. For more information, ' + - 'read the Rules of Hooks: https://fb.me/rules-of-hooks\n\n' + - ' Previous render Next render\n' + - ' ------------------------------------------------------\n' + - '1. useState useState\n' + - '2. useState useState\n' + - '3. undefined useState\n' + - ' ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n', - ]); - - // Uncomment if/when we support this again - // expect(ReactNoop.getChildren()).toEqual([span('A: 2, B: 3, C: 0')]); - - // updateC(4); - // expect(Scheduler).toFlushAndYield(['A: 2, B: 3, C: 4']); - // expect(ReactNoop.getChildren()).toEqual([span('A: 2, B: 3, C: 4')]); - }); + it('eager bailout optimization should always compare to latest rendered reducer', () => { + // Edge case based on a bug report + let setCounter; + function App() { + const [counter, _setCounter] = useState(1); + setCounter = _setCounter; + return ; + } - it('unmount state', () => { - let updateA; - let updateB; - let updateC; - - function App(props) { - const [A, _updateA] = useState(0); - const [B, _updateB] = useState(0); - updateA = _updateA; - updateB = _updateB; - - let C; - if (props.loadC) { - const [_C, _updateC] = useState(0); - C = _C; - updateC = _updateC; - } else { - C = '[not loaded]'; + function Component({count}) { + const [state, dispatch] = useReducer(() => { + // This reducer closes over a value from props. If the reducer is not + // properly updated, the eager reducer will compare to an old value + // and bail out incorrectly. + Scheduler.unstable_yieldValue('Reducer: ' + count); + return count; + }, -1); + useEffect(() => { + Scheduler.unstable_yieldValue('Effect: ' + count); + dispatch(); + }, [count]); + Scheduler.unstable_yieldValue('Render: ' + state); + return count; } - return ; - } + act(() => { + ReactNoop.render(); + expect(Scheduler).toFlushAndYield([ + 'Render: -1', + 'Effect: 1', + 'Reducer: 1', + 'Reducer: 1', + 'Render: 1', + ]); + expect(ReactNoop).toMatchRenderedOutput('1'); + }); - ReactNoop.render(); - expect(Scheduler).toFlushAndYield(['A: 0, B: 0, C: 0']); - expect(ReactNoop.getChildren()).toEqual([span('A: 0, B: 0, C: 0')]); - act(() => { - updateA(2); - updateB(3); - updateC(4); + act(() => { + setCounter(2); + }); + expect(Scheduler).toHaveYielded([ + 'Render: 1', + 'Effect: 2', + 'Reducer: 2', + 'Reducer: 2', + 'Render: 2', + ]); + expect(ReactNoop).toMatchRenderedOutput('2'); }); - expect(Scheduler).toHaveYielded(['A: 2, B: 3, C: 4']); - expect(ReactNoop.getChildren()).toEqual([span('A: 2, B: 3, C: 4')]); - ReactNoop.render(); - expect(Scheduler).toFlushAndThrow( - 'Rendered fewer hooks than expected. This may be caused by an ' + - 'accidental early return statement.', - ); - }); - it('unmount effects', () => { - function App(props) { - useEffect(() => { - Scheduler.unstable_yieldValue('Mount A'); - return () => { - Scheduler.unstable_yieldValue('Unmount A'); - }; - }, []); + // Regression test. Covers a case where an internal state variable + // (`didReceiveUpdate`) is not reset properly. + it('state bail out edge case (#16359)', async () => { + let setCounterA; + let setCounterB; - if (props.showMore) { + function CounterA() { + const [counter, setCounter] = useState(0); + setCounterA = setCounter; + Scheduler.unstable_yieldValue('Render A: ' + counter); useEffect(() => { - Scheduler.unstable_yieldValue('Mount B'); - return () => { - Scheduler.unstable_yieldValue('Unmount B'); - }; - }, []); + Scheduler.unstable_yieldValue('Commit A: ' + counter); + }); + return counter; } - return null; - } + function CounterB() { + const [counter, setCounter] = useState(0); + setCounterB = setCounter; + Scheduler.unstable_yieldValue('Render B: ' + counter); + useEffect(() => { + Scheduler.unstable_yieldValue('Commit B: ' + counter); + }); + return counter; + } - act(() => { - ReactNoop.render(, () => - Scheduler.unstable_yieldValue('Sync effect'), - ); - expect(Scheduler).toFlushAndYieldThrough(['Sync effect']); - }); + const root = ReactNoop.createRoot(null); + await ReactNoop.act(async () => { + root.render( + <> + + + , + ); + }); + expect(Scheduler).toHaveYielded([ + 'Render A: 0', + 'Render B: 0', + 'Commit A: 0', + 'Commit B: 0', + ]); - expect(Scheduler).toHaveYielded(['Mount A']); + await ReactNoop.act(async () => { + setCounterA(1); - act(() => { - ReactNoop.render(); - expect(() => { - expect(() => { - expect(Scheduler).toFlushAndYield([]); - }).toThrow('Rendered more hooks than during the previous render'); - }).toErrorDev([ - 'Warning: React has detected a change in the order of Hooks called by App. ' + - 'This will lead to bugs and errors if not fixed. For more information, ' + - 'read the Rules of Hooks: https://fb.me/rules-of-hooks\n\n' + - ' Previous render Next render\n' + - ' ------------------------------------------------------\n' + - '1. useEffect useEffect\n' + - '2. undefined useEffect\n' + - ' ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n', + // In the same batch, update B twice. To trigger the condition we're + // testing, the first update is necessary to bypass the early + // bailout optimization. + setCounterB(1); + setCounterB(0); + }); + expect(Scheduler).toHaveYielded([ + 'Render A: 1', + 'Render B: 0', + 'Commit A: 1', + // B should not fire an effect because the update bailed out + // 'Commit B: 0', ]); }); - // Uncomment if/when we support this again - // ReactNoop.flushPassiveEffects(); - // expect(Scheduler).toHaveYielded(['Mount B']); + it('should update latest rendered reducer when a preceding state receives a render phase update', () => { + // Similar to previous test, except using a preceding render phase update + // instead of new props. + let dispatch; + function App() { + const [step, setStep] = useState(0); + const [shadow, _dispatch] = useReducer(() => step, step); + dispatch = _dispatch; - // ReactNoop.render(); - // expect(Scheduler).toFlushAndThrow( - // 'Rendered fewer hooks than expected. This may be caused by an ' + - // 'accidental early return statement.', - // ); - }); - }); + if (step < 5) { + setStep(step + 1); + } - it('eager bailout optimization should always compare to latest rendered reducer', () => { - // Edge case based on a bug report - let setCounter; - function App() { - const [counter, _setCounter] = useState(1); - setCounter = _setCounter; - return ; - } - - function Component({count}) { - const [state, dispatch] = useReducer(() => { - // This reducer closes over a value from props. If the reducer is not - // properly updated, the eager reducer will compare to an old value - // and bail out incorrectly. - Scheduler.unstable_yieldValue('Reducer: ' + count); - return count; - }, -1); - useEffect(() => { - Scheduler.unstable_yieldValue('Effect: ' + count); - dispatch(); - }, [count]); - Scheduler.unstable_yieldValue('Render: ' + state); - return count; - } - - act(() => { - ReactNoop.render(); - expect(Scheduler).toFlushAndYield([ - 'Render: -1', - 'Effect: 1', - 'Reducer: 1', - 'Reducer: 1', - 'Render: 1', - ]); - expect(ReactNoop).toMatchRenderedOutput('1'); - }); + Scheduler.unstable_yieldValue(`Step: ${step}, Shadow: ${shadow}`); + return shadow; + } - act(() => { - setCounter(2); - }); - expect(Scheduler).toHaveYielded([ - 'Render: 1', - 'Effect: 2', - 'Reducer: 2', - 'Reducer: 2', - 'Render: 2', - ]); - expect(ReactNoop).toMatchRenderedOutput('2'); - }); + ReactNoop.render(); + expect(Scheduler).toFlushAndYield([ + 'Step: 0, Shadow: 0', + 'Step: 1, Shadow: 0', + 'Step: 2, Shadow: 0', + 'Step: 3, Shadow: 0', + 'Step: 4, Shadow: 0', + 'Step: 5, Shadow: 0', + ]); + expect(ReactNoop).toMatchRenderedOutput('0'); - // Regression test. Covers a case where an internal state variable - // (`didReceiveUpdate`) is not reset properly. - it('state bail out edge case (#16359)', async () => { - let setCounterA; - let setCounterB; - - function CounterA() { - const [counter, setCounter] = useState(0); - setCounterA = setCounter; - Scheduler.unstable_yieldValue('Render A: ' + counter); - useEffect(() => { - Scheduler.unstable_yieldValue('Commit A: ' + counter); - }); - return counter; - } - - function CounterB() { - const [counter, setCounter] = useState(0); - setCounterB = setCounter; - Scheduler.unstable_yieldValue('Render B: ' + counter); - useEffect(() => { - Scheduler.unstable_yieldValue('Commit B: ' + counter); + act(() => dispatch()); + expect(Scheduler).toHaveYielded(['Step: 5, Shadow: 5']); + expect(ReactNoop).toMatchRenderedOutput('5'); }); - return counter; - } - - const root = ReactNoop.createRoot(null); - await ReactNoop.act(async () => { - root.render( - <> - - - , - ); - }); - expect(Scheduler).toHaveYielded([ - 'Render A: 0', - 'Render B: 0', - 'Commit A: 0', - 'Commit B: 0', - ]); - - await ReactNoop.act(async () => { - setCounterA(1); - - // In the same batch, update B twice. To trigger the condition we're - // testing, the first update is necessary to bypass the early - // bailout optimization. - setCounterB(1); - setCounterB(0); - }); - expect(Scheduler).toHaveYielded([ - 'Render A: 1', - 'Render B: 0', - 'Commit A: 1', - // B should not fire an effect because the update bailed out - // 'Commit B: 0', - ]); - }); - it('should update latest rendered reducer when a preceding state receives a render phase update', () => { - // Similar to previous test, except using a preceding render phase update - // instead of new props. - let dispatch; - function App() { - const [step, setStep] = useState(0); - const [shadow, _dispatch] = useReducer(() => step, step); - dispatch = _dispatch; - - if (step < 5) { - setStep(step + 1); - } + it('should process the rest pending updates after a render phase update', () => { + // Similar to previous test, except using a preceding render phase update + // instead of new props. + let updateA; + let updateC; + function App() { + const [a, setA] = useState(false); + const [b, setB] = useState(false); + if (a !== b) { + setB(a); + } + // Even though we called setB above, + // we should still apply the changes to C, + // during this render pass. + const [c, setC] = useState(false); + updateA = setA; + updateC = setC; + return `${a ? 'A' : 'a'}${b ? 'B' : 'b'}${c ? 'C' : 'c'}`; + } - Scheduler.unstable_yieldValue(`Step: ${step}, Shadow: ${shadow}`); - return shadow; - } - - ReactNoop.render(); - expect(Scheduler).toFlushAndYield([ - 'Step: 0, Shadow: 0', - 'Step: 1, Shadow: 0', - 'Step: 2, Shadow: 0', - 'Step: 3, Shadow: 0', - 'Step: 4, Shadow: 0', - 'Step: 5, Shadow: 0', - ]); - expect(ReactNoop).toMatchRenderedOutput('0'); - - act(() => dispatch()); - expect(Scheduler).toHaveYielded(['Step: 5, Shadow: 5']); - expect(ReactNoop).toMatchRenderedOutput('5'); - }); + act(() => ReactNoop.render()); + expect(ReactNoop).toMatchRenderedOutput('abc'); - it('should process the rest pending updates after a render phase update', () => { - // Similar to previous test, except using a preceding render phase update - // instead of new props. - let updateA; - let updateC; - function App() { - const [a, setA] = useState(false); - const [b, setB] = useState(false); - if (a !== b) { - setB(a); - } - // Even though we called setB above, - // we should still apply the changes to C, - // during this render pass. - const [c, setC] = useState(false); - updateA = setA; - updateC = setC; - return `${a ? 'A' : 'a'}${b ? 'B' : 'b'}${c ? 'C' : 'c'}`; - } - - act(() => ReactNoop.render()); - expect(ReactNoop).toMatchRenderedOutput('abc'); - - act(() => { - updateA(true); - // This update should not get dropped. - updateC(true); + act(() => { + updateA(true); + // This update should not get dropped. + updateC(true); + }); + expect(ReactNoop).toMatchRenderedOutput('ABC'); + }); }); - expect(ReactNoop).toMatchRenderedOutput('ABC'); }); }); diff --git a/packages/react-reconciler/src/__tests__/ReactSuspenseWithNoopRenderer-test.internal.js b/packages/react-reconciler/src/__tests__/ReactSuspenseWithNoopRenderer-test.internal.js index 71dd2c8df71b0..0cca771b1df22 100644 --- a/packages/react-reconciler/src/__tests__/ReactSuspenseWithNoopRenderer-test.internal.js +++ b/packages/react-reconciler/src/__tests__/ReactSuspenseWithNoopRenderer-test.internal.js @@ -9,1230 +9,1585 @@ let Suspense; let TextResource; let textResourceShouldFail; -describe('ReactSuspenseWithNoopRenderer', () => { - if (!__EXPERIMENTAL__) { - it("empty test so Jest doesn't complain", () => {}); - return; - } - - beforeEach(() => { - jest.resetModules(); - ReactFeatureFlags = require('shared/ReactFeatureFlags'); - ReactFeatureFlags.debugRenderPhaseSideEffectsForStrictMode = false; - ReactFeatureFlags.replayFailedUnitOfWorkWithInvokeGuardedCallback = false; - ReactFeatureFlags.flushSuspenseFallbacksInTests = false; - ReactFeatureFlags.deferPassiveEffectCleanupDuringUnmount = true; - React = require('react'); - Fragment = React.Fragment; - ReactNoop = require('react-noop-renderer'); - Scheduler = require('scheduler'); - ReactCache = require('react-cache'); - Suspense = React.Suspense; - - TextResource = ReactCache.unstable_createResource( - ([text, ms = 0]) => { - return new Promise((resolve, reject) => - setTimeout(() => { - if (textResourceShouldFail) { - Scheduler.unstable_yieldValue(`Promise rejected [${text}]`); - reject(new Error('Failed to load: ' + text)); - } else { - Scheduler.unstable_yieldValue(`Promise resolved [${text}]`); - resolve(text); - } - }, ms), - ); - }, - ([text, ms]) => text, - ); - textResourceShouldFail = false; - }); - - // function div(...children) { - // children = children.map( - // c => (typeof c === 'string' ? {text: c, hidden: false} : c), - // ); - // return {type: 'div', children, prop: undefined, hidden: false}; - // } - - function span(prop) { - return {type: 'span', children: [], prop, hidden: false}; - } - - function hiddenSpan(prop) { - return {type: 'span', children: [], prop, hidden: true}; - } - - function advanceTimers(ms) { - // Note: This advances Jest's virtual time but not React's. Use - // ReactNoop.expire for that. - if (typeof ms !== 'number') { - throw new Error('Must specify ms'); - } - jest.advanceTimersByTime(ms); - // Wait until the end of the current tick - // We cannot use a timer since we're faking them - return Promise.resolve().then(() => {}); - } - - function Text(props) { - Scheduler.unstable_yieldValue(props.text); - return ; - } - - function AsyncText(props) { - const text = props.text; - try { - TextResource.read([props.text, props.ms]); - Scheduler.unstable_yieldValue(text); - return ; - } catch (promise) { - if (typeof promise.then === 'function') { - Scheduler.unstable_yieldValue(`Suspend! [${text}]`); - } else { - Scheduler.unstable_yieldValue(`Error! [${text}]`); - } - throw promise; - } - } - - it('warns if the deprecated maxDuration option is used', () => { - function Foo() { - return ( - -
; - +function loadModules({ + deferPassiveEffectCleanupDuringUnmount, + runAllPassiveEffectDestroysBeforeCreates, +}) { + ReactFeatureFlags = require('shared/ReactFeatureFlags'); + ReactFeatureFlags.debugRenderPhaseSideEffectsForStrictMode = false; + ReactFeatureFlags.replayFailedUnitOfWorkWithInvokeGuardedCallback = false; + ReactFeatureFlags.flushSuspenseFallbacksInTests = false; + ReactFeatureFlags.deferPassiveEffectCleanupDuringUnmount = deferPassiveEffectCleanupDuringUnmount; + ReactFeatureFlags.runAllPassiveEffectDestroysBeforeCreates = runAllPassiveEffectDestroysBeforeCreates; + React = require('react'); + Fragment = React.Fragment; + ReactNoop = require('react-noop-renderer'); + Scheduler = require('scheduler'); + ReactCache = require('react-cache'); + Suspense = React.Suspense; + + TextResource = ReactCache.unstable_createResource( + ([text, ms = 0]) => { + return new Promise((resolve, reject) => + setTimeout(() => { + if (textResourceShouldFail) { + Scheduler.unstable_yieldValue(`Promise rejected [${text}]`); + reject(new Error('Failed to load: ' + text)); + } else { + Scheduler.unstable_yieldValue(`Promise resolved [${text}]`); + resolve(text); + } + }, ms), ); - } - - ReactNoop.render(); - - expect(() => Scheduler.unstable_flushAll()).toErrorDev([ - 'Warning: maxDuration has been removed from React. ' + - 'Remove the maxDuration prop.' + - '\n in Suspense (at **)' + - '\n in Foo (at **)', - ]); - }); + }, + ([text, ms]) => text, + ); + textResourceShouldFail = false; +} + +[true, false].forEach(deferPassiveEffectCleanupDuringUnmount => { + [true, false].forEach(runAllPassiveEffectDestroysBeforeCreates => { + describe(`ReactSuspenseWithNoopRenderer deferPassiveEffectCleanupDuringUnmount:${deferPassiveEffectCleanupDuringUnmount} runAllPassiveEffectDestroysBeforeCreates:${runAllPassiveEffectDestroysBeforeCreates}`, () => { + if (!__EXPERIMENTAL__) { + it("empty test so Jest doesn't complain", () => {}); + return; + } - it('does not restart rendering for initial render', async () => { - function Bar(props) { - Scheduler.unstable_yieldValue('Bar'); - return props.children; - } + beforeEach(() => { + jest.resetModules(); - function Foo() { - Scheduler.unstable_yieldValue('Foo'); - return ( - <> - }> - - - - - - - - - ); - } - - ReactNoop.render(); - expect(Scheduler).toFlushAndYieldThrough([ - 'Foo', - 'Bar', - // A suspends - 'Suspend! [A]', - // But we keep rendering the siblings - 'B', - 'Loading...', - 'C', - // We leave D incomplete. - ]); - expect(ReactNoop.getChildren()).toEqual([]); - - // Flush the promise completely - Scheduler.unstable_advanceTime(100); - await advanceTimers(100); - expect(Scheduler).toHaveYielded(['Promise resolved [A]']); - - // Even though the promise has resolved, we should now flush - // and commit the in progress render instead of restarting. - expect(Scheduler).toFlushAndYield(['D']); - expect(ReactNoop.getChildren()).toEqual([ - span('Loading...'), - span('C'), - span('D'), - ]); - - // Await one micro task to attach the retry listeners. - await null; - - // Next, we'll flush the complete content. - expect(Scheduler).toFlushAndYield(['Bar', 'A', 'B']); - - expect(ReactNoop.getChildren()).toEqual([ - span('A'), - span('B'), - span('C'), - span('D'), - ]); - }); + loadModules({ + deferPassiveEffectCleanupDuringUnmount, + runAllPassiveEffectDestroysBeforeCreates, + }); + }); - it('suspends rendering and continues later', async () => { - function Bar(props) { - Scheduler.unstable_yieldValue('Bar'); - return props.children; - } - - function Foo({renderBar}) { - Scheduler.unstable_yieldValue('Foo'); - return ( - }> - {renderBar ? ( - - - - - ) : null} - - ); - } - - // Render empty shell. - ReactNoop.render(); - expect(Scheduler).toFlushAndYield(['Foo']); - - // The update will suspend. - ReactNoop.render(); - expect(Scheduler).toFlushAndYield([ - 'Foo', - 'Bar', - // A suspends - 'Suspend! [A]', - // But we keep rendering the siblings - 'B', - 'Loading...', - ]); - expect(ReactNoop.getChildren()).toEqual([]); - - // Flush some of the time - await advanceTimers(50); - // Still nothing... - expect(Scheduler).toFlushWithoutYielding(); - expect(ReactNoop.getChildren()).toEqual([]); - - // Flush the promise completely - await advanceTimers(50); - // Renders successfully - expect(Scheduler).toHaveYielded(['Promise resolved [A]']); - expect(Scheduler).toFlushAndYield(['Foo', 'Bar', 'A', 'B']); - expect(ReactNoop.getChildren()).toEqual([span('A'), span('B')]); - }); + // function div(...children) { + // children = children.map( + // c => (typeof c === 'string' ? {text: c, hidden: false} : c), + // ); + // return {type: 'div', children, prop: undefined, hidden: false}; + // } - it('suspends siblings and later recovers each independently', async () => { - // Render two sibling Suspense components - ReactNoop.render( - - }> - - - }> - - - , - ); - expect(Scheduler).toFlushAndYield([ - 'Suspend! [A]', - 'Loading A...', - 'Suspend! [B]', - 'Loading B...', - ]); - expect(ReactNoop.getChildren()).toEqual([ - span('Loading A...'), - span('Loading B...'), - ]); - - // Advance time by enough that the first Suspense's promise resolves and - // switches back to the normal view. The second Suspense should still - // show the placeholder - ReactNoop.expire(5000); - await advanceTimers(5000); - - expect(Scheduler).toHaveYielded(['Promise resolved [A]']); - expect(Scheduler).toFlushAndYield(['A']); - expect(ReactNoop.getChildren()).toEqual([span('A'), span('Loading B...')]); - - // Advance time by enough that the second Suspense's promise resolves - // and switches back to the normal view - ReactNoop.expire(1000); - await advanceTimers(1000); - - expect(Scheduler).toHaveYielded(['Promise resolved [B]']); - expect(Scheduler).toFlushAndYield(['B']); - expect(ReactNoop.getChildren()).toEqual([span('A'), span('B')]); - }); + function span(prop) { + return {type: 'span', children: [], prop, hidden: false}; + } - it('continues rendering siblings after suspending', async () => { - // A shell is needed. The update cause it to suspend. - ReactNoop.render(} />); - expect(Scheduler).toFlushAndYield([]); - // B suspends. Continue rendering the remaining siblings. - ReactNoop.render( - }> - - - - - , - ); - // B suspends. Continue rendering the remaining siblings. - expect(Scheduler).toFlushAndYield([ - 'A', - 'Suspend! [B]', - 'C', - 'D', - 'Loading...', - ]); - // Did not commit yet. - expect(ReactNoop.getChildren()).toEqual([]); - - // Wait for data to resolve - await advanceTimers(100); - // Renders successfully - expect(Scheduler).toHaveYielded(['Promise resolved [B]']); - expect(Scheduler).toFlushAndYield(['A', 'B', 'C', 'D']); - expect(ReactNoop.getChildren()).toEqual([ - span('A'), - span('B'), - span('C'), - span('D'), - ]); - }); + function hiddenSpan(prop) { + return {type: 'span', children: [], prop, hidden: true}; + } - it('retries on error', async () => { - class ErrorBoundary extends React.Component { - state = {error: null}; - componentDidCatch(error) { - this.setState({error}); + function advanceTimers(ms) { + // Note: This advances Jest's virtual time but not React's. Use + // ReactNoop.expire for that. + if (typeof ms !== 'number') { + throw new Error('Must specify ms'); + } + jest.advanceTimersByTime(ms); + // Wait until the end of the current tick + // We cannot use a timer since we're faking them + return Promise.resolve().then(() => {}); } - reset() { - this.setState({error: null}); + + function Text(props) { + Scheduler.unstable_yieldValue(props.text); + return ; } - render() { - if (this.state.error !== null) { - return ; + + function AsyncText(props) { + const text = props.text; + try { + TextResource.read([props.text, props.ms]); + Scheduler.unstable_yieldValue(text); + return ; + } catch (promise) { + if (typeof promise.then === 'function') { + Scheduler.unstable_yieldValue(`Suspend! [${text}]`); + } else { + Scheduler.unstable_yieldValue(`Error! [${text}]`); + } + throw promise; } - return this.props.children; } - } - - const errorBoundary = React.createRef(); - function App({renderContent}) { - return ( - }> - {renderContent ? ( - - - - ) : null} - - ); - } - ReactNoop.render(); - expect(Scheduler).toFlushAndYield([]); - expect(ReactNoop.getChildren()).toEqual([]); + it('warns if the deprecated maxDuration option is used', () => { + function Foo() { + return ( + +
; + + ); + } + + ReactNoop.render(); + + expect(() => Scheduler.unstable_flushAll()).toErrorDev([ + 'Warning: maxDuration has been removed from React. ' + + 'Remove the maxDuration prop.' + + '\n in Suspense (at **)' + + '\n in Foo (at **)', + ]); + }); + + it('does not restart rendering for initial render', async () => { + function Bar(props) { + Scheduler.unstable_yieldValue('Bar'); + return props.children; + } + + function Foo() { + Scheduler.unstable_yieldValue('Foo'); + return ( + <> + }> + + + + + + + + + ); + } - ReactNoop.render(); - expect(Scheduler).toFlushAndYield(['Suspend! [Result]', 'Loading...']); - expect(ReactNoop.getChildren()).toEqual([]); + ReactNoop.render(); + expect(Scheduler).toFlushAndYieldThrough([ + 'Foo', + 'Bar', + // A suspends + 'Suspend! [A]', + // But we keep rendering the siblings + 'B', + 'Loading...', + 'C', + // We leave D incomplete. + ]); + expect(ReactNoop.getChildren()).toEqual([]); - textResourceShouldFail = true; - ReactNoop.expire(1000); - await advanceTimers(1000); - textResourceShouldFail = false; + // Flush the promise completely + Scheduler.unstable_advanceTime(100); + await advanceTimers(100); + expect(Scheduler).toHaveYielded(['Promise resolved [A]']); - expect(Scheduler).toHaveYielded(['Promise rejected [Result]']); + // Even though the promise has resolved, we should now flush + // and commit the in progress render instead of restarting. + expect(Scheduler).toFlushAndYield(['D']); + expect(ReactNoop.getChildren()).toEqual([ + span('Loading...'), + span('C'), + span('D'), + ]); - expect(Scheduler).toFlushAndYield([ - 'Error! [Result]', + // Await one micro task to attach the retry listeners. + await null; - // React retries one more time - 'Error! [Result]', + // Next, we'll flush the complete content. + expect(Scheduler).toFlushAndYield(['Bar', 'A', 'B']); - // Errored again on retry. Now handle it. - 'Caught error: Failed to load: Result', - ]); - expect(ReactNoop.getChildren()).toEqual([ - span('Caught error: Failed to load: Result'), - ]); - }); + expect(ReactNoop.getChildren()).toEqual([ + span('A'), + span('B'), + span('C'), + span('D'), + ]); + }); - it('retries on error after falling back to a placeholder', async () => { - class ErrorBoundary extends React.Component { - state = {error: null}; - componentDidCatch(error) { - this.setState({error}); - } - reset() { - this.setState({error: null}); - } - render() { - if (this.state.error !== null) { - return ; + it('suspends rendering and continues later', async () => { + function Bar(props) { + Scheduler.unstable_yieldValue('Bar'); + return props.children; } - return this.props.children; - } - } - - const errorBoundary = React.createRef(); - function App() { - return ( - }> - - - - - ); - } - ReactNoop.render(); - expect(Scheduler).toFlushAndYield(['Suspend! [Result]', 'Loading...']); - expect(ReactNoop.getChildren()).toEqual([span('Loading...')]); + function Foo({renderBar}) { + Scheduler.unstable_yieldValue('Foo'); + return ( + }> + {renderBar ? ( + + + + + ) : null} + + ); + } - textResourceShouldFail = true; - ReactNoop.expire(3000); - await advanceTimers(3000); - textResourceShouldFail = false; + // Render empty shell. + ReactNoop.render(); + expect(Scheduler).toFlushAndYield(['Foo']); + + // The update will suspend. + ReactNoop.render(); + expect(Scheduler).toFlushAndYield([ + 'Foo', + 'Bar', + // A suspends + 'Suspend! [A]', + // But we keep rendering the siblings + 'B', + 'Loading...', + ]); + expect(ReactNoop.getChildren()).toEqual([]); + + // Flush some of the time + await advanceTimers(50); + // Still nothing... + expect(Scheduler).toFlushWithoutYielding(); + expect(ReactNoop.getChildren()).toEqual([]); + + // Flush the promise completely + await advanceTimers(50); + // Renders successfully + expect(Scheduler).toHaveYielded(['Promise resolved [A]']); + expect(Scheduler).toFlushAndYield(['Foo', 'Bar', 'A', 'B']); + expect(ReactNoop.getChildren()).toEqual([span('A'), span('B')]); + }); - expect(Scheduler).toHaveYielded(['Promise rejected [Result]']); - expect(Scheduler).toFlushAndYield([ - 'Error! [Result]', + it('suspends siblings and later recovers each independently', async () => { + // Render two sibling Suspense components + ReactNoop.render( + + }> + + + }> + + + , + ); + expect(Scheduler).toFlushAndYield([ + 'Suspend! [A]', + 'Loading A...', + 'Suspend! [B]', + 'Loading B...', + ]); + expect(ReactNoop.getChildren()).toEqual([ + span('Loading A...'), + span('Loading B...'), + ]); - // React retries one more time - 'Error! [Result]', + // Advance time by enough that the first Suspense's promise resolves and + // switches back to the normal view. The second Suspense should still + // show the placeholder + ReactNoop.expire(5000); + await advanceTimers(5000); - // Errored again on retry. Now handle it. + expect(Scheduler).toHaveYielded(['Promise resolved [A]']); + expect(Scheduler).toFlushAndYield(['A']); + expect(ReactNoop.getChildren()).toEqual([ + span('A'), + span('Loading B...'), + ]); - 'Caught error: Failed to load: Result', - ]); - expect(ReactNoop.getChildren()).toEqual([ - span('Caught error: Failed to load: Result'), - ]); - }); + // Advance time by enough that the second Suspense's promise resolves + // and switches back to the normal view + ReactNoop.expire(1000); + await advanceTimers(1000); - it('can update at a higher priority while in a suspended state', async () => { - function App(props) { - return ( - }> - - - - ); - } - - // Initial mount - ReactNoop.render(); - expect(Scheduler).toFlushAndYield(['A', 'Suspend! [1]', 'Loading...']); - await advanceTimers(0); - expect(Scheduler).toHaveYielded(['Promise resolved [1]']); - expect(Scheduler).toFlushAndYield(['A', '1']); - expect(ReactNoop.getChildren()).toEqual([span('A'), span('1')]); - - // Update the low-pri text - ReactNoop.render(); - expect(Scheduler).toFlushAndYield([ - 'A', - // Suspends - 'Suspend! [2]', - 'Loading...', - ]); - - // While we're still waiting for the low-pri update to complete, update the - // high-pri text at high priority. - ReactNoop.flushSync(() => { - ReactNoop.render(); - }); - expect(Scheduler).toHaveYielded(['B', '1']); - expect(ReactNoop.getChildren()).toEqual([span('B'), span('1')]); + expect(Scheduler).toHaveYielded(['Promise resolved [B]']); + expect(Scheduler).toFlushAndYield(['B']); + expect(ReactNoop.getChildren()).toEqual([span('A'), span('B')]); + }); - // Unblock the low-pri text and finish - await advanceTimers(0); - expect(Scheduler).toHaveYielded(['Promise resolved [2]']); - expect(ReactNoop.getChildren()).toEqual([span('B'), span('1')]); - }); + it('continues rendering siblings after suspending', async () => { + // A shell is needed. The update cause it to suspend. + ReactNoop.render(} />); + expect(Scheduler).toFlushAndYield([]); + // B suspends. Continue rendering the remaining siblings. + ReactNoop.render( + }> + + + + + , + ); + // B suspends. Continue rendering the remaining siblings. + expect(Scheduler).toFlushAndYield([ + 'A', + 'Suspend! [B]', + 'C', + 'D', + 'Loading...', + ]); + // Did not commit yet. + expect(ReactNoop.getChildren()).toEqual([]); + + // Wait for data to resolve + await advanceTimers(100); + // Renders successfully + expect(Scheduler).toHaveYielded(['Promise resolved [B]']); + expect(Scheduler).toFlushAndYield(['A', 'B', 'C', 'D']); + expect(ReactNoop.getChildren()).toEqual([ + span('A'), + span('B'), + span('C'), + span('D'), + ]); + }); - it('keeps working on lower priority work after being pinged', async () => { - // Advance the virtual time so that we're close to the edge of a bucket. - ReactNoop.expire(149); + it('retries on error', async () => { + class ErrorBoundary extends React.Component { + state = {error: null}; + componentDidCatch(error) { + this.setState({error}); + } + reset() { + this.setState({error: null}); + } + render() { + if (this.state.error !== null) { + return ( + + ); + } + return this.props.children; + } + } - function App(props) { - return ( - }> - {props.showA && } - {props.showB && } - - ); - } - - ReactNoop.render(); - expect(Scheduler).toFlushAndYield([]); - expect(ReactNoop.getChildren()).toEqual([]); - - ReactNoop.render(); - expect(Scheduler).toFlushAndYield(['Suspend! [A]', 'Loading...']); - expect(ReactNoop.getChildren()).toEqual([]); - - // Advance React's virtual time by enough to fall into a new async bucket, - // but not enough to expire the suspense timeout. - ReactNoop.expire(120); - ReactNoop.render(); - expect(Scheduler).toFlushAndYield(['Suspend! [A]', 'B', 'Loading...']); - expect(ReactNoop.getChildren()).toEqual([]); - - await advanceTimers(0); - expect(Scheduler).toHaveYielded(['Promise resolved [A]']); - expect(Scheduler).toFlushAndYield(['A', 'B']); - expect(ReactNoop.getChildren()).toEqual([span('A'), span('B')]); - }); + const errorBoundary = React.createRef(); + function App({renderContent}) { + return ( + }> + {renderContent ? ( + + + + ) : null} + + ); + } - it('tries rendering a lower priority pending update even if a higher priority one suspends', async () => { - function App(props) { - if (props.hide) { - return ; - } - return ( - - - - ); - } + ReactNoop.render(); + expect(Scheduler).toFlushAndYield([]); + expect(ReactNoop.getChildren()).toEqual([]); - // Schedule a high pri update and a low pri update, without rendering in - // between. - ReactNoop.discreteUpdates(() => { - // High pri - ReactNoop.render(); - }); - // Low pri - ReactNoop.render(); - - expect(Scheduler).toFlushAndYield([ - // The first update suspends - 'Suspend! [Async]', - // but we have another pending update that we can work on - '(empty)', - ]); - expect(ReactNoop.getChildren()).toEqual([span('(empty)')]); - }); + ReactNoop.render(); + expect(Scheduler).toFlushAndYield(['Suspend! [Result]', 'Loading...']); + expect(ReactNoop.getChildren()).toEqual([]); - it('tries each subsequent level after suspending', async () => { - const root = ReactNoop.createRoot(); - - function App({step, shouldSuspend}) { - return ( - - - {shouldSuspend ? ( - - ) : ( - - )} - - ); - } + textResourceShouldFail = true; + ReactNoop.expire(1000); + await advanceTimers(1000); + textResourceShouldFail = false; + + expect(Scheduler).toHaveYielded(['Promise rejected [Result]']); + + expect(Scheduler).toFlushAndYield([ + 'Error! [Result]', - function interrupt() { - // React has a heuristic to batch all updates that occur within the same - // event. This is a trick to circumvent that heuristic. - ReactNoop.flushSync(() => { - ReactNoop.renderToRootWithID(null, 'other-root'); + // React retries one more time + 'Error! [Result]', + + // Errored again on retry. Now handle it. + 'Caught error: Failed to load: Result', + ]); + expect(ReactNoop.getChildren()).toEqual([ + span('Caught error: Failed to load: Result'), + ]); }); - } - // Mount the Suspense boundary without suspending, so that the subsequent - // updates suspend with a delay. - await ReactNoop.act(async () => { - root.render(); - }); - await advanceTimers(1000); - expect(Scheduler).toHaveYielded(['Sibling', 'Step 0']); - - // Schedule an update at several distinct expiration times - await ReactNoop.act(async () => { - root.render(); - Scheduler.unstable_advanceTime(1000); - expect(Scheduler).toFlushAndYieldThrough(['Sibling']); - interrupt(); - - root.render(); - Scheduler.unstable_advanceTime(1000); - expect(Scheduler).toFlushAndYieldThrough(['Sibling']); - interrupt(); - - root.render(); - Scheduler.unstable_advanceTime(1000); - expect(Scheduler).toFlushAndYieldThrough(['Sibling']); - interrupt(); - - root.render(); - }); + it('retries on error after falling back to a placeholder', async () => { + class ErrorBoundary extends React.Component { + state = {error: null}; + componentDidCatch(error) { + this.setState({error}); + } + reset() { + this.setState({error: null}); + } + render() { + if (this.state.error !== null) { + return ( + + ); + } + return this.props.children; + } + } - // Should suspend at each distinct level - expect(Scheduler).toHaveYielded([ - 'Sibling', - 'Suspend! [Step 1]', - 'Sibling', - 'Suspend! [Step 2]', - 'Sibling', - 'Suspend! [Step 3]', - 'Sibling', - 'Step 4', - ]); - }); + const errorBoundary = React.createRef(); + function App() { + return ( + }> + + + + + ); + } - it('forces an expiration after an update times out', async () => { - ReactNoop.render( - - } /> - , - ); - expect(Scheduler).toFlushAndYield([]); - - ReactNoop.render( - - }> - - - - , - ); - - expect(Scheduler).toFlushAndYield([ - // The async child suspends - 'Suspend! [Async]', - // Render the placeholder - 'Loading...', - // Continue on the sibling - 'Sync', - ]); - // The update hasn't expired yet, so we commit nothing. - expect(ReactNoop.getChildren()).toEqual([]); - - // Advance both React's virtual time and Jest's timers by enough to expire - // the update, but not by enough to flush the suspending promise. - ReactNoop.expire(10000); - await advanceTimers(10000); - // No additional rendering work is required, since we already prepared - // the placeholder. - expect(Scheduler).toHaveYielded([]); - // Should have committed the placeholder. - expect(ReactNoop.getChildren()).toEqual([span('Loading...'), span('Sync')]); - - // Once the promise resolves, we render the suspended view - await advanceTimers(10000); - expect(Scheduler).toHaveYielded(['Promise resolved [Async]']); - expect(Scheduler).toFlushAndYield(['Async']); - expect(ReactNoop.getChildren()).toEqual([span('Async'), span('Sync')]); - }); + ReactNoop.render(); + expect(Scheduler).toFlushAndYield(['Suspend! [Result]', 'Loading...']); + expect(ReactNoop.getChildren()).toEqual([span('Loading...')]); - it('switches to an inner fallback after suspending for a while', async () => { - // Advance the virtual time so that we're closer to the edge of a bucket. - ReactNoop.expire(200); - - ReactNoop.render( - - - }> - - }> - - - - , - ); - - expect(Scheduler).toFlushAndYield([ - 'Sync', - // The async content suspends - 'Suspend! [Outer content]', - 'Suspend! [Inner content]', - 'Loading inner...', - 'Loading outer...', - ]); - // The outer loading state finishes immediately. - expect(ReactNoop.getChildren()).toEqual([ - span('Sync'), - span('Loading outer...'), - ]); - - // Resolve the outer promise. - ReactNoop.expire(300); - await advanceTimers(300); - expect(Scheduler).toHaveYielded(['Promise resolved [Outer content]']); - expect(Scheduler).toFlushAndYield([ - 'Outer content', - 'Suspend! [Inner content]', - 'Loading inner...', - ]); - // Don't commit the inner placeholder yet. - expect(ReactNoop.getChildren()).toEqual([ - span('Sync'), - span('Loading outer...'), - ]); - - // Expire the inner timeout. - ReactNoop.expire(500); - await advanceTimers(500); - // Now that 750ms have elapsed since the outer placeholder timed out, - // we can timeout the inner placeholder. - expect(ReactNoop.getChildren()).toEqual([ - span('Sync'), - span('Outer content'), - span('Loading inner...'), - ]); - - // Finally, flush the inner promise. We should see the complete screen. - ReactNoop.expire(1000); - await advanceTimers(1000); - expect(Scheduler).toHaveYielded(['Promise resolved [Inner content]']); - expect(Scheduler).toFlushAndYield(['Inner content']); - expect(ReactNoop.getChildren()).toEqual([ - span('Sync'), - span('Outer content'), - span('Inner content'), - ]); - }); + textResourceShouldFail = true; + ReactNoop.expire(3000); + await advanceTimers(3000); + textResourceShouldFail = false; - it('renders an expiration boundary synchronously', async () => { - spyOnDev(console, 'error'); - // Synchronously render a tree that suspends - ReactNoop.flushSync(() => - ReactNoop.render( - - }> - - - - , - ), - ); - expect(Scheduler).toHaveYielded([ - // The async child suspends - 'Suspend! [Async]', - // We immediately render the fallback UI - 'Loading...', - // Continue on the sibling - 'Sync', - ]); - // The tree commits synchronously - expect(ReactNoop.getChildren()).toEqual([span('Loading...'), span('Sync')]); - - // Once the promise resolves, we render the suspended view - await advanceTimers(0); - expect(Scheduler).toHaveYielded(['Promise resolved [Async]']); - expect(Scheduler).toFlushAndYield(['Async']); - expect(ReactNoop.getChildren()).toEqual([span('Async'), span('Sync')]); - }); + expect(Scheduler).toHaveYielded(['Promise rejected [Result]']); + expect(Scheduler).toFlushAndYield([ + 'Error! [Result]', + + // React retries one more time + 'Error! [Result]', + + // Errored again on retry. Now handle it. + + 'Caught error: Failed to load: Result', + ]); + expect(ReactNoop.getChildren()).toEqual([ + span('Caught error: Failed to load: Result'), + ]); + }); - it('suspending inside an expired expiration boundary will bubble to the next one', async () => { - ReactNoop.flushSync(() => - ReactNoop.render( - - }> - }> - + it('can update at a higher priority while in a suspended state', async () => { + function App(props) { + return ( + }> + + + + ); + } + + // Initial mount + ReactNoop.render(); + expect(Scheduler).toFlushAndYield(['A', 'Suspend! [1]', 'Loading...']); + await advanceTimers(0); + expect(Scheduler).toHaveYielded(['Promise resolved [1]']); + expect(Scheduler).toFlushAndYield(['A', '1']); + expect(ReactNoop.getChildren()).toEqual([span('A'), span('1')]); + + // Update the low-pri text + ReactNoop.render(); + expect(Scheduler).toFlushAndYield([ + 'A', + // Suspends + 'Suspend! [2]', + 'Loading...', + ]); + + // While we're still waiting for the low-pri update to complete, update the + // high-pri text at high priority. + ReactNoop.flushSync(() => { + ReactNoop.render(); + }); + expect(Scheduler).toHaveYielded(['B', '1']); + expect(ReactNoop.getChildren()).toEqual([span('B'), span('1')]); + + // Unblock the low-pri text and finish + await advanceTimers(0); + expect(Scheduler).toHaveYielded(['Promise resolved [2]']); + expect(ReactNoop.getChildren()).toEqual([span('B'), span('1')]); + }); + + it('keeps working on lower priority work after being pinged', async () => { + // Advance the virtual time so that we're close to the edge of a bucket. + ReactNoop.expire(149); + + function App(props) { + return ( + }> + {props.showA && } + {props.showB && } + + ); + } + + ReactNoop.render(); + expect(Scheduler).toFlushAndYield([]); + expect(ReactNoop.getChildren()).toEqual([]); + + ReactNoop.render(); + expect(Scheduler).toFlushAndYield(['Suspend! [A]', 'Loading...']); + expect(ReactNoop.getChildren()).toEqual([]); + + // Advance React's virtual time by enough to fall into a new async bucket, + // but not enough to expire the suspense timeout. + ReactNoop.expire(120); + ReactNoop.render(); + expect(Scheduler).toFlushAndYield(['Suspend! [A]', 'B', 'Loading...']); + expect(ReactNoop.getChildren()).toEqual([]); + + await advanceTimers(0); + expect(Scheduler).toHaveYielded(['Promise resolved [A]']); + expect(Scheduler).toFlushAndYield(['A', 'B']); + expect(ReactNoop.getChildren()).toEqual([span('A'), span('B')]); + }); + + it('tries rendering a lower priority pending update even if a higher priority one suspends', async () => { + function App(props) { + if (props.hide) { + return ; + } + return ( + + + + ); + } + + // Schedule a high pri update and a low pri update, without rendering in + // between. + ReactNoop.discreteUpdates(() => { + // High pri + ReactNoop.render(); + }); + // Low pri + ReactNoop.render(); + + expect(Scheduler).toFlushAndYield([ + // The first update suspends + 'Suspend! [Async]', + // but we have another pending update that we can work on + '(empty)', + ]); + expect(ReactNoop.getChildren()).toEqual([span('(empty)')]); + }); + + it('tries each subsequent level after suspending', async () => { + const root = ReactNoop.createRoot(); + + function App({step, shouldSuspend}) { + return ( + + + {shouldSuspend ? ( + + ) : ( + + )} + + ); + } + + function interrupt() { + // React has a heuristic to batch all updates that occur within the same + // event. This is a trick to circumvent that heuristic. + ReactNoop.flushSync(() => { + ReactNoop.renderToRootWithID(null, 'other-root'); + }); + } + + // Mount the Suspense boundary without suspending, so that the subsequent + // updates suspend with a delay. + await ReactNoop.act(async () => { + root.render(); + }); + await advanceTimers(1000); + expect(Scheduler).toHaveYielded(['Sibling', 'Step 0']); + + // Schedule an update at several distinct expiration times + await ReactNoop.act(async () => { + root.render(); + Scheduler.unstable_advanceTime(1000); + expect(Scheduler).toFlushAndYieldThrough(['Sibling']); + interrupt(); + + root.render(); + Scheduler.unstable_advanceTime(1000); + expect(Scheduler).toFlushAndYieldThrough(['Sibling']); + interrupt(); + + root.render(); + Scheduler.unstable_advanceTime(1000); + expect(Scheduler).toFlushAndYieldThrough(['Sibling']); + interrupt(); + + root.render(); + }); + + // Should suspend at each distinct level + expect(Scheduler).toHaveYielded([ + 'Sibling', + 'Suspend! [Step 1]', + 'Sibling', + 'Suspend! [Step 2]', + 'Sibling', + 'Suspend! [Step 3]', + 'Sibling', + 'Step 4', + ]); + }); + + it('forces an expiration after an update times out', async () => { + ReactNoop.render( + + } /> + , + ); + expect(Scheduler).toFlushAndYield([]); + + ReactNoop.render( + + }> + - - , - ), - ); - expect(Scheduler).toHaveYielded([ - 'Suspend! [Async]', - 'Suspend! [Loading (inner)...]', - 'Sync', - 'Loading (outer)...', - ]); - // The tree commits synchronously - expect(ReactNoop.getChildren()).toEqual([span('Loading (outer)...')]); - }); + , + ); - it('expires early by default', async () => { - ReactNoop.render( - - } /> - , - ); - expect(Scheduler).toFlushAndYield([]); - - ReactNoop.render( - - }> - - - - , - ); - - expect(Scheduler).toFlushAndYield([ - // The async child suspends - 'Suspend! [Async]', - 'Loading...', - // Continue on the sibling - 'Sync', - ]); - // The update hasn't expired yet, so we commit nothing. - expect(ReactNoop.getChildren()).toEqual([]); - - // Advance both React's virtual time and Jest's timers by enough to trigger - // the timeout, but not by enough to flush the promise or reach the true - // expiration time. - ReactNoop.expire(2000); - await advanceTimers(2000); - expect(Scheduler).toFlushWithoutYielding(); - expect(ReactNoop.getChildren()).toEqual([span('Loading...'), span('Sync')]); - - // Once the promise resolves, we render the suspended view - await advanceTimers(1000); - expect(Scheduler).toHaveYielded(['Promise resolved [Async]']); - expect(Scheduler).toFlushAndYield(['Async']); - expect(ReactNoop.getChildren()).toEqual([span('Async'), span('Sync')]); - }); + expect(Scheduler).toFlushAndYield([ + // The async child suspends + 'Suspend! [Async]', + // Render the placeholder + 'Loading...', + // Continue on the sibling + 'Sync', + ]); + // The update hasn't expired yet, so we commit nothing. + expect(ReactNoop.getChildren()).toEqual([]); + + // Advance both React's virtual time and Jest's timers by enough to expire + // the update, but not by enough to flush the suspending promise. + ReactNoop.expire(10000); + await advanceTimers(10000); + // No additional rendering work is required, since we already prepared + // the placeholder. + expect(Scheduler).toHaveYielded([]); + // Should have committed the placeholder. + expect(ReactNoop.getChildren()).toEqual([ + span('Loading...'), + span('Sync'), + ]); - it('resolves successfully even if fallback render is pending', async () => { - ReactNoop.render( - <> - } /> - , - ); - expect(Scheduler).toFlushAndYield([]); - expect(ReactNoop.getChildren()).toEqual([]); - ReactNoop.render( - <> - }> - - - , - ); - expect(ReactNoop.flushNextYield()).toEqual(['Suspend! [Async]']); - await advanceTimers(1500); - expect(Scheduler).toHaveYielded([]); - expect(ReactNoop.getChildren()).toEqual([]); - // Before we have a chance to flush, the promise resolves. - await advanceTimers(2000); - expect(Scheduler).toHaveYielded(['Promise resolved [Async]']); - expect(Scheduler).toFlushAndYield([ - // We've now pinged the boundary but we don't know if we should restart yet, - // because we haven't completed the suspense boundary. - 'Loading...', - // Once we've completed the boundary we restarted. - 'Async', - ]); - expect(ReactNoop.getChildren()).toEqual([span('Async')]); - }); + // Once the promise resolves, we render the suspended view + await advanceTimers(10000); + expect(Scheduler).toHaveYielded(['Promise resolved [Async]']); + expect(Scheduler).toFlushAndYield(['Async']); + expect(ReactNoop.getChildren()).toEqual([span('Async'), span('Sync')]); + }); - it('throws a helpful error when an update is suspends without a placeholder', () => { - ReactNoop.render(); - expect(Scheduler).toFlushAndThrow( - 'AsyncText suspended while rendering, but no fallback UI was specified.', - ); - }); + it('switches to an inner fallback after suspending for a while', async () => { + // Advance the virtual time so that we're closer to the edge of a bucket. + ReactNoop.expire(200); - it('a Suspense component correctly handles more than one suspended child', async () => { - ReactNoop.render( - }> - - - , - ); - Scheduler.unstable_advanceTime(10000); - expect(Scheduler).toFlushExpired([ - 'Suspend! [A]', - 'Suspend! [B]', - 'Loading...', - ]); - expect(ReactNoop.getChildren()).toEqual([span('Loading...')]); - - await advanceTimers(100); - - expect(Scheduler).toHaveYielded([ - 'Promise resolved [A]', - 'Promise resolved [B]', - ]); - expect(Scheduler).toFlushAndYield(['A', 'B']); - expect(ReactNoop.getChildren()).toEqual([span('A'), span('B')]); - }); + ReactNoop.render( + + + }> + + }> + + + + , + ); - it('can resume rendering earlier than a timeout', async () => { - ReactNoop.render(} />); - expect(Scheduler).toFlushAndYield([]); - - ReactNoop.render( - }> - - , - ); - expect(Scheduler).toFlushAndYield(['Suspend! [Async]', 'Loading...']); - expect(ReactNoop.getChildren()).toEqual([]); - - // Advance time by an amount slightly smaller than what's necessary to - // resolve the promise - await advanceTimers(99); - - // Nothing has rendered yet - expect(Scheduler).toFlushWithoutYielding(); - expect(ReactNoop.getChildren()).toEqual([]); - - // Resolve the promise - await advanceTimers(1); - // We can now resume rendering - expect(Scheduler).toHaveYielded(['Promise resolved [Async]']); - expect(Scheduler).toFlushAndYield(['Async']); - expect(ReactNoop.getChildren()).toEqual([span('Async')]); - }); + expect(Scheduler).toFlushAndYield([ + 'Sync', + // The async content suspends + 'Suspend! [Outer content]', + 'Suspend! [Inner content]', + 'Loading inner...', + 'Loading outer...', + ]); + // The outer loading state finishes immediately. + expect(ReactNoop.getChildren()).toEqual([ + span('Sync'), + span('Loading outer...'), + ]); - it('starts working on an update even if its priority falls between two suspended levels', async () => { - function App(props) { - return ( - }> - {props.text === 'C' || props.text === 'S' ? ( - - ) : ( - - )} - - ); - } - - // First mount without suspending. This ensures we already have content - // showing so that subsequent updates will suspend. - ReactNoop.render(); - expect(Scheduler).toFlushAndYield(['S']); - - // Schedule an update, and suspend for up to 5 seconds. - React.unstable_withSuspenseConfig( - () => ReactNoop.render(), - { - timeoutMs: 5000, - }, - ); - // The update should suspend. - expect(Scheduler).toFlushAndYield(['Suspend! [A]', 'Loading...']); - expect(ReactNoop.getChildren()).toEqual([span('S')]); - - // Advance time until right before it expires. - await advanceTimers(4999); - ReactNoop.expire(4999); - expect(Scheduler).toFlushWithoutYielding(); - expect(ReactNoop.getChildren()).toEqual([span('S')]); - - // Schedule another low priority update. - React.unstable_withSuspenseConfig( - () => ReactNoop.render(), - { - timeoutMs: 10000, - }, - ); - // This update should also suspend. - expect(Scheduler).toFlushAndYield(['Suspend! [B]', 'Loading...']); - expect(ReactNoop.getChildren()).toEqual([span('S')]); - - // Schedule a regular update. Its expiration time will fall between - // the expiration times of the previous two updates. - ReactNoop.render(); - expect(Scheduler).toFlushAndYield(['C']); - expect(ReactNoop.getChildren()).toEqual([span('C')]); - - await advanceTimers(10000); - // Flush the remaining work. - expect(Scheduler).toHaveYielded([ - 'Promise resolved [A]', - 'Promise resolved [B]', - ]); - // Nothing else to render. - expect(Scheduler).toFlushWithoutYielding(); - expect(ReactNoop.getChildren()).toEqual([span('C')]); - }); + // Resolve the outer promise. + ReactNoop.expire(300); + await advanceTimers(300); + expect(Scheduler).toHaveYielded(['Promise resolved [Outer content]']); + expect(Scheduler).toFlushAndYield([ + 'Outer content', + 'Suspend! [Inner content]', + 'Loading inner...', + ]); + // Don't commit the inner placeholder yet. + expect(ReactNoop.getChildren()).toEqual([ + span('Sync'), + span('Loading outer...'), + ]); - it('flushes all expired updates in a single batch', async () => { - class Foo extends React.Component { - componentDidUpdate() { - Scheduler.unstable_yieldValue('Commit: ' + this.props.text); - } - componentDidMount() { - Scheduler.unstable_yieldValue('Commit: ' + this.props.text); - } - render() { - return ( + // Expire the inner timeout. + ReactNoop.expire(500); + await advanceTimers(500); + // Now that 750ms have elapsed since the outer placeholder timed out, + // we can timeout the inner placeholder. + expect(ReactNoop.getChildren()).toEqual([ + span('Sync'), + span('Outer content'), + span('Loading inner...'), + ]); + + // Finally, flush the inner promise. We should see the complete screen. + ReactNoop.expire(1000); + await advanceTimers(1000); + expect(Scheduler).toHaveYielded(['Promise resolved [Inner content]']); + expect(Scheduler).toFlushAndYield(['Inner content']); + expect(ReactNoop.getChildren()).toEqual([ + span('Sync'), + span('Outer content'), + span('Inner content'), + ]); + }); + + it('renders an expiration boundary synchronously', async () => { + spyOnDev(console, 'error'); + // Synchronously render a tree that suspends + ReactNoop.flushSync(() => + ReactNoop.render( + + }> + + + + , + ), + ); + expect(Scheduler).toHaveYielded([ + // The async child suspends + 'Suspend! [Async]', + // We immediately render the fallback UI + 'Loading...', + // Continue on the sibling + 'Sync', + ]); + // The tree commits synchronously + expect(ReactNoop.getChildren()).toEqual([ + span('Loading...'), + span('Sync'), + ]); + + // Once the promise resolves, we render the suspended view + await advanceTimers(0); + expect(Scheduler).toHaveYielded(['Promise resolved [Async]']); + expect(Scheduler).toFlushAndYield(['Async']); + expect(ReactNoop.getChildren()).toEqual([span('Async'), span('Sync')]); + }); + + it('suspending inside an expired expiration boundary will bubble to the next one', async () => { + ReactNoop.flushSync(() => + ReactNoop.render( + + }> + }> + + + + + , + ), + ); + expect(Scheduler).toHaveYielded([ + 'Suspend! [Async]', + 'Suspend! [Loading (inner)...]', + 'Sync', + 'Loading (outer)...', + ]); + // The tree commits synchronously + expect(ReactNoop.getChildren()).toEqual([span('Loading (outer)...')]); + }); + + it('expires early by default', async () => { + ReactNoop.render( + + } /> + , + ); + expect(Scheduler).toFlushAndYield([]); + + ReactNoop.render( + + }> + + + + , + ); + + expect(Scheduler).toFlushAndYield([ + // The async child suspends + 'Suspend! [Async]', + 'Loading...', + // Continue on the sibling + 'Sync', + ]); + // The update hasn't expired yet, so we commit nothing. + expect(ReactNoop.getChildren()).toEqual([]); + + // Advance both React's virtual time and Jest's timers by enough to trigger + // the timeout, but not by enough to flush the promise or reach the true + // expiration time. + ReactNoop.expire(2000); + await advanceTimers(2000); + expect(Scheduler).toFlushWithoutYielding(); + expect(ReactNoop.getChildren()).toEqual([ + span('Loading...'), + span('Sync'), + ]); + + // Once the promise resolves, we render the suspended view + await advanceTimers(1000); + expect(Scheduler).toHaveYielded(['Promise resolved [Async]']); + expect(Scheduler).toFlushAndYield(['Async']); + expect(ReactNoop.getChildren()).toEqual([span('Async'), span('Sync')]); + }); + + it('resolves successfully even if fallback render is pending', async () => { + ReactNoop.render( + <> + } /> + , + ); + expect(Scheduler).toFlushAndYield([]); + expect(ReactNoop.getChildren()).toEqual([]); + ReactNoop.render( + <> + }> + + + , + ); + expect(ReactNoop.flushNextYield()).toEqual(['Suspend! [Async]']); + await advanceTimers(1500); + expect(Scheduler).toHaveYielded([]); + expect(ReactNoop.getChildren()).toEqual([]); + // Before we have a chance to flush, the promise resolves. + await advanceTimers(2000); + expect(Scheduler).toHaveYielded(['Promise resolved [Async]']); + expect(Scheduler).toFlushAndYield([ + // We've now pinged the boundary but we don't know if we should restart yet, + // because we haven't completed the suspense boundary. + 'Loading...', + // Once we've completed the boundary we restarted. + 'Async', + ]); + expect(ReactNoop.getChildren()).toEqual([span('Async')]); + }); + + it('throws a helpful error when an update is suspends without a placeholder', () => { + ReactNoop.render(); + expect(Scheduler).toFlushAndThrow( + 'AsyncText suspended while rendering, but no fallback UI was specified.', + ); + }); + + it('a Suspense component correctly handles more than one suspended child', async () => { + ReactNoop.render( }> - - + + + , ); - } - } - - ReactNoop.render(); - ReactNoop.expire(1000); - jest.advanceTimersByTime(1000); - ReactNoop.render(); - ReactNoop.expire(1000); - jest.advanceTimersByTime(1000); - ReactNoop.render(); - ReactNoop.expire(1000); - jest.advanceTimersByTime(1000); - ReactNoop.render(); - - Scheduler.unstable_advanceTime(10000); - jest.advanceTimersByTime(10000); - - expect(Scheduler).toFlushExpired([ - 'Suspend! [goodbye]', - 'Loading...', - 'Commit: goodbye', - ]); - expect(ReactNoop.getChildren()).toEqual([span('Loading...')]); - - Scheduler.unstable_advanceTime(20000); - await advanceTimers(20000); - expect(Scheduler).toHaveYielded(['Promise resolved [goodbye]']); - expect(ReactNoop.getChildren()).toEqual([span('Loading...')]); - - expect(Scheduler).toFlushAndYield(['goodbye']); - expect(ReactNoop.getChildren()).toEqual([span('goodbye')]); - }); + Scheduler.unstable_advanceTime(10000); + expect(Scheduler).toFlushExpired([ + 'Suspend! [A]', + 'Suspend! [B]', + 'Loading...', + ]); + expect(ReactNoop.getChildren()).toEqual([span('Loading...')]); - it('a suspended update that expires', async () => { - // Regression test. This test used to fall into an infinite loop. - function ExpensiveText({text}) { - // This causes the update to expire. - Scheduler.unstable_advanceTime(10000); - // Then something suspends. - return ; - } - - function App() { - return ( - - - - - - ); - } - - ReactNoop.render(); - expect(Scheduler).toFlushAndYield([ - 'Suspend! [A]', - 'Suspend! [B]', - 'Suspend! [C]', - ]); - expect(ReactNoop).toMatchRenderedOutput('Loading...'); - - await advanceTimers(200000); - expect(Scheduler).toHaveYielded([ - 'Promise resolved [A]', - 'Promise resolved [B]', - 'Promise resolved [C]', - ]); - - expect(Scheduler).toFlushAndYield(['A', 'B', 'C']); - expect(ReactNoop).toMatchRenderedOutput( - <> - - - - , - ); - }); + await advanceTimers(100); + + expect(Scheduler).toHaveYielded([ + 'Promise resolved [A]', + 'Promise resolved [B]', + ]); + expect(Scheduler).toFlushAndYield(['A', 'B']); + expect(ReactNoop.getChildren()).toEqual([span('A'), span('B')]); + }); - describe('legacy mode mode', () => { - it('times out immediately', async () => { - function App() { - return ( + it('can resume rendering earlier than a timeout', async () => { + ReactNoop.render(} />); + expect(Scheduler).toFlushAndYield([]); + + ReactNoop.render( }> - - + + , ); - } + expect(Scheduler).toFlushAndYield(['Suspend! [Async]', 'Loading...']); + expect(ReactNoop.getChildren()).toEqual([]); + + // Advance time by an amount slightly smaller than what's necessary to + // resolve the promise + await advanceTimers(99); + + // Nothing has rendered yet + expect(Scheduler).toFlushWithoutYielding(); + expect(ReactNoop.getChildren()).toEqual([]); + + // Resolve the promise + await advanceTimers(1); + // We can now resume rendering + expect(Scheduler).toHaveYielded(['Promise resolved [Async]']); + expect(Scheduler).toFlushAndYield(['Async']); + expect(ReactNoop.getChildren()).toEqual([span('Async')]); + }); + + it('starts working on an update even if its priority falls between two suspended levels', async () => { + function App(props) { + return ( + }> + {props.text === 'C' || props.text === 'S' ? ( + + ) : ( + + )} + + ); + } + + // First mount without suspending. This ensures we already have content + // showing so that subsequent updates will suspend. + ReactNoop.render(); + expect(Scheduler).toFlushAndYield(['S']); + + // Schedule an update, and suspend for up to 5 seconds. + React.unstable_withSuspenseConfig( + () => ReactNoop.render(), + { + timeoutMs: 5000, + }, + ); + // The update should suspend. + expect(Scheduler).toFlushAndYield(['Suspend! [A]', 'Loading...']); + expect(ReactNoop.getChildren()).toEqual([span('S')]); + + // Advance time until right before it expires. + await advanceTimers(4999); + ReactNoop.expire(4999); + expect(Scheduler).toFlushWithoutYielding(); + expect(ReactNoop.getChildren()).toEqual([span('S')]); + + // Schedule another low priority update. + React.unstable_withSuspenseConfig( + () => ReactNoop.render(), + { + timeoutMs: 10000, + }, + ); + // This update should also suspend. + expect(Scheduler).toFlushAndYield(['Suspend! [B]', 'Loading...']); + expect(ReactNoop.getChildren()).toEqual([span('S')]); + + // Schedule a regular update. Its expiration time will fall between + // the expiration times of the previous two updates. + ReactNoop.render(); + expect(Scheduler).toFlushAndYield(['C']); + expect(ReactNoop.getChildren()).toEqual([span('C')]); + + await advanceTimers(10000); + // Flush the remaining work. + expect(Scheduler).toHaveYielded([ + 'Promise resolved [A]', + 'Promise resolved [B]', + ]); + // Nothing else to render. + expect(Scheduler).toFlushWithoutYielding(); + expect(ReactNoop.getChildren()).toEqual([span('C')]); + }); + + it('flushes all expired updates in a single batch', async () => { + class Foo extends React.Component { + componentDidUpdate() { + Scheduler.unstable_yieldValue('Commit: ' + this.props.text); + } + componentDidMount() { + Scheduler.unstable_yieldValue('Commit: ' + this.props.text); + } + render() { + return ( + }> + + + ); + } + } + + ReactNoop.render(); + ReactNoop.expire(1000); + jest.advanceTimersByTime(1000); + ReactNoop.render(); + ReactNoop.expire(1000); + jest.advanceTimersByTime(1000); + ReactNoop.render(); + ReactNoop.expire(1000); + jest.advanceTimersByTime(1000); + ReactNoop.render(); + + Scheduler.unstable_advanceTime(10000); + jest.advanceTimersByTime(10000); + + expect(Scheduler).toFlushExpired([ + 'Suspend! [goodbye]', + 'Loading...', + 'Commit: goodbye', + ]); + expect(ReactNoop.getChildren()).toEqual([span('Loading...')]); + + Scheduler.unstable_advanceTime(20000); + await advanceTimers(20000); + expect(Scheduler).toHaveYielded(['Promise resolved [goodbye]']); + expect(ReactNoop.getChildren()).toEqual([span('Loading...')]); + + expect(Scheduler).toFlushAndYield(['goodbye']); + expect(ReactNoop.getChildren()).toEqual([span('goodbye')]); + }); + + it('a suspended update that expires', async () => { + // Regression test. This test used to fall into an infinite loop. + function ExpensiveText({text}) { + // This causes the update to expire. + Scheduler.unstable_advanceTime(10000); + // Then something suspends. + return ; + } + + function App() { + return ( + + + + + + ); + } + + ReactNoop.render(); + expect(Scheduler).toFlushAndYield([ + 'Suspend! [A]', + 'Suspend! [B]', + 'Suspend! [C]', + ]); + expect(ReactNoop).toMatchRenderedOutput('Loading...'); + + await advanceTimers(200000); + expect(Scheduler).toHaveYielded([ + 'Promise resolved [A]', + 'Promise resolved [B]', + 'Promise resolved [C]', + ]); + + expect(Scheduler).toFlushAndYield(['A', 'B', 'C']); + expect(ReactNoop).toMatchRenderedOutput( + <> + + + + , + ); + }); + + describe('legacy mode mode', () => { + it('times out immediately', async () => { + function App() { + return ( + }> + + + ); + } + + // Times out immediately, ignoring the specified threshold. + ReactNoop.renderLegacySyncRoot(); + expect(Scheduler).toHaveYielded(['Suspend! [Result]', 'Loading...']); + expect(ReactNoop.getChildren()).toEqual([span('Loading...')]); + + ReactNoop.expire(100); + await advanceTimers(100); + + expect(Scheduler).toHaveYielded(['Promise resolved [Result]']); + expect(Scheduler).toFlushExpired(['Result']); + expect(ReactNoop.getChildren()).toEqual([span('Result')]); + }); + + it('times out immediately when Suspense is in legacy mode', async () => { + class UpdatingText extends React.Component { + state = {step: 1}; + render() { + return ; + } + } + + function Spinner() { + return ( + + + + + + ); + } + + const text = React.createRef(null); + function App() { + return ( + }> + + + + ); + } + + // Initial mount. + ReactNoop.renderLegacySyncRoot(); + await advanceTimers(100); + expect(Scheduler).toHaveYielded([ + 'Suspend! [Step: 1]', + 'Sibling', + 'Loading (1)', + 'Loading (2)', + 'Loading (3)', + 'Promise resolved [Step: 1]', + ]); + expect(Scheduler).toFlushExpired(['Step: 1']); + expect(ReactNoop).toMatchRenderedOutput( + <> + + + , + ); + + // Update. + text.current.setState({step: 2}, () => + Scheduler.unstable_yieldValue('Update did commit'), + ); + + expect(ReactNoop.flushNextYield()).toEqual([ + 'Suspend! [Step: 2]', + 'Loading (1)', + 'Loading (2)', + 'Loading (3)', + 'Update did commit', + ]); + expect(ReactNoop).toMatchRenderedOutput( + <> +