/*** Copyright (c) Meta Platforms, Inc. and affiliates.** This source code is licensed under the MIT license found in the* LICENSE file in the root directory of this source tree.** @emails react-core* @jest-environment node*//* eslint-disable no-func-assign */'use strict';
let React;
let textCache;
let readText;
let resolveText;
let ReactNoop;
let Scheduler;
let Suspense;
let useState;
let useReducer;
let useEffect;
let useInsertionEffect;
let useLayoutEffect;
let useCallback;
let useMemo;
let useRef;
let useImperativeHandle;
let useTransition;
let useDeferredValue;
let forwardRef;
let memo;
let act;
let ContinuousEventPriority;
let SuspenseList;
let waitForAll;
let waitFor;
let waitForThrow;
let waitForPaint;
let assertLog;
describe('ReactHooksWithNoopRenderer', () => {
beforeEach(() => {
jest.resetModules();
jest.useFakeTimers();
React = require('react');
ReactNoop = require('react-noop-renderer');
Scheduler = require('scheduler');
act = require('internal-test-utils').act;
useState = React.useState;
useReducer = React.useReducer;
useEffect = React.useEffect;
useInsertionEffect = React.useInsertionEffect;
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;
ContinuousEventPriority =
require('react-reconciler/constants').ContinuousEventPriority;
if (gate(flags => flags.enableSuspenseList)) {
SuspenseList = React.unstable_SuspenseList;
}const InternalTestUtils = require('internal-test-utils');
waitForAll = InternalTestUtils.waitForAll;
waitFor = InternalTestUtils.waitFor;
waitForThrow = InternalTestUtils.waitForThrow;
waitForPaint = InternalTestUtils.waitForPaint;
assertLog = InternalTestUtils.assertLog;
textCache = new Map();
readText = text => {
const record = textCache.get(text);
if (record !== undefined) {
switch (record.status) {
case 'pending':
throw record.promise;
case 'rejected':
throw Error('Failed to load: ' + text);
case 'resolved':
return text;
}} else {
let ping;
const promise = new Promise(resolve => (ping = resolve));
const newRecord = {
status: 'pending',
ping: ping,
promise,
};textCache.set(text, newRecord);
throw promise;
}};resolveText = text => {
const record = textCache.get(text);
if (record !== undefined) {
if (record.status === 'pending') {
Scheduler.log(`Promise resolved [${text}]`);
record.ping();
record.ping = null;
record.status = 'resolved';
clearTimeout(record.promise._timer);
record.promise = null;
}} else {
const newRecord = {
ping: null,
status: 'resolved',
promise: null,
};textCache.set(text, newRecord);
}};});function Text(props) {
Scheduler.log(props.text);
return <span prop={props.text} />;
}function AsyncText(props) {
const text = props.text;
try {
readText(text);
Scheduler.log(text);
return <span prop={text} />;
} catch (promise) {
if (typeof promise.then === 'function') {
Scheduler.log(`Suspend! [${text}]`);
if (typeof props.ms === 'number' && promise._timer === undefined) {
promise._timer = setTimeout(() => {
resolveText(text);
}, props.ms);
}} else {
Scheduler.log(`Error! [${text}]`);
}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', async () => {
function Counter(props, ref) {
const [count, updateCount] = useState(0);
useImperativeHandle(ref, () => ({updateCount}));
return <Text text={props.label + ': ' + count} />;
}Counter = forwardRef(Counter);
// Initial mount
const counter = React.createRef(null);
ReactNoop.render(<Counter label="Count" ref={counter} />);
await waitForAll(['Count: 0']);
expect(ReactNoop).toMatchRenderedOutput(<span prop="Count: 0" />);
// Schedule some updates
await act(async () => {
React.startTransition(() => {
counter.current.updateCount(1);
counter.current.updateCount(count => count + 10);
});// Partially flush without committing
await waitFor(['Count: 11']);
expect(ReactNoop).toMatchRenderedOutput(<span prop="Count: 0" />);
// Interrupt with a high priority update
ReactNoop.flushSync(() => {
ReactNoop.render(<Counter label="Total" />);
});assertLog(['Total: 0']);
// Resume rendering
await waitForAll(['Total: 11']);
expect(ReactNoop).toMatchRenderedOutput(<span prop="Total: 11" />);
});});it('throws inside class components', async () => {
class BadCounter extends React.Component {
render() {
const [count] = useState(0);
return <Text text={this.props.label + ': ' + count} />;
}}ReactNoop.render(<BadCounter />);
await waitForThrow(
'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://reactjs.org/link/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 <Text text={count} />;
}ReactNoop.render(<GoodCounter initialCount={10} />);
await waitForAll([10]);
});// @gate !disableModulePatternComponents
it('throws inside module-style components', async () => {
function Counter() {
return {
render() {
const [count] = useState(0);
return <Text text={this.props.label + ': ' + count} />;
},};}ReactNoop.render(<Counter />);
await expect(
async () =>
await waitForThrow(
'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://reactjs.org/link/invalid-hook-call for tips about how to debug and fix this problem.',
),).toErrorDev(
'Warning: The <Counter /> 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 <Text text={count} />;
}ReactNoop.render(<GoodCounter initialCount={10} />);
await waitForAll([10]);
});it('throws when called outside the render phase', async () => {
expect(() => {
expect(() => useState(0)).toThrow(
"Cannot read property 'useState' of null",
);}).toErrorDev(
'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://reactjs.org/link/invalid-hook-call for tips about how to debug and fix this problem.',
{withoutStack: true},
);});describe('useState', () => {
it('simple mount and update', async () => {
function Counter(props, ref) {
const [count, updateCount] = useState(0);
useImperativeHandle(ref, () => ({updateCount}));
return <Text text={'Count: ' + count} />;
}Counter = forwardRef(Counter);
const counter = React.createRef(null);
ReactNoop.render(<Counter ref={counter} />);
await waitForAll(['Count: 0']);
expect(ReactNoop).toMatchRenderedOutput(<span prop="Count: 0" />);
await act(() => counter.current.updateCount(1));
assertLog(['Count: 1']);
expect(ReactNoop).toMatchRenderedOutput(<span prop="Count: 1" />);
await act(() => counter.current.updateCount(count => count + 10));
assertLog(['Count: 11']);
expect(ReactNoop).toMatchRenderedOutput(<span prop="Count: 11" />);
});it('lazy state initializer', async () => {
function Counter(props, ref) {
const [count, updateCount] = useState(() => {
Scheduler.log('getInitialState');
return props.initialState;
});useImperativeHandle(ref, () => ({updateCount}));
return <Text text={'Count: ' + count} />;
}Counter = forwardRef(Counter);
const counter = React.createRef(null);
ReactNoop.render(<Counter initialState={42} ref={counter} />);
await waitForAll(['getInitialState', 'Count: 42']);
expect(ReactNoop).toMatchRenderedOutput(<span prop="Count: 42" />);
await act(() => counter.current.updateCount(7));
assertLog(['Count: 7']);
expect(ReactNoop).toMatchRenderedOutput(<span prop="Count: 7" />);
});it('multiple states', async () => {
function Counter(props, ref) {
const [count, updateCount] = useState(0);
const [label, updateLabel] = useState('Count');
useImperativeHandle(ref, () => ({updateCount, updateLabel}));
return <Text text={label + ': ' + count} />;
}Counter = forwardRef(Counter);
const counter = React.createRef(null);
ReactNoop.render(<Counter ref={counter} />);
await waitForAll(['Count: 0']);
expect(ReactNoop).toMatchRenderedOutput(<span prop="Count: 0" />);
await act(() => counter.current.updateCount(7));
assertLog(['Count: 7']);
await act(() => counter.current.updateLabel('Total'));
assertLog(['Total: 7']);
});it('returns the same updater function every time', async () => {
let updater = null;
function Counter() {
const [count, updateCount] = useState(0);
updater = updateCount;
return <Text text={'Count: ' + count} />;
}ReactNoop.render(<Counter />);
await waitForAll(['Count: 0']);
expect(ReactNoop).toMatchRenderedOutput(<span prop="Count: 0" />);
const firstUpdater = updater;
await act(() => firstUpdater(1));
assertLog(['Count: 1']);
expect(ReactNoop).toMatchRenderedOutput(<span prop="Count: 1" />);
const secondUpdater = updater;
await act(() => firstUpdater(count => count + 10));
assertLog(['Count: 11']);
expect(ReactNoop).toMatchRenderedOutput(<span prop="Count: 11" />);
expect(firstUpdater).toBe(secondUpdater);
});it('does not warn on set after unmount', async () => {
let _updateCount;
function Counter(props, ref) {
const [, updateCount] = useState(0);
_updateCount = updateCount;
return null;
}ReactNoop.render(<Counter />);
await waitForAll([]);
ReactNoop.render(null);
await waitForAll([]);
await act(() => _updateCount(1));
});it('works with memo', async () => {
let _updateCount;
function Counter(props) {
const [count, updateCount] = useState(0);
_updateCount = updateCount;
return <Text text={'Count: ' + count} />;
}Counter = memo(Counter);
ReactNoop.render(<Counter />);
await waitForAll(['Count: 0']);
expect(ReactNoop).toMatchRenderedOutput(<span prop="Count: 0" />);
ReactNoop.render(<Counter />);
await waitForAll([]);
expect(ReactNoop).toMatchRenderedOutput(<span prop="Count: 0" />);
await act(() => _updateCount(1));
assertLog(['Count: 1']);
expect(ReactNoop).toMatchRenderedOutput(<span prop="Count: 1" />);
});});describe('updates during the render phase', () => {
it('restarts the render function and applies the new updates on top', async () => {
function ScrollView({row: newRow}) {
const [isScrollingDown, setIsScrollingDown] = useState(false);
const [row, setRow] = useState(null);
if (row !== newRow) {
// Row changed since last render. Update isScrollingDown.
setIsScrollingDown(row !== null && newRow > row);
setRow(newRow);
}return <Text text={`Scrolling down: ${isScrollingDown}`} />;
}ReactNoop.render(<ScrollView row={1} />);await waitForAll(['Scrolling down: false']);expect(ReactNoop).toMatchRenderedOutput(<span prop="Scrolling down: false" />,);ReactNoop.render(<ScrollView row={5} />);await waitForAll(['Scrolling down: true']);expect(ReactNoop).toMatchRenderedOutput(<span prop="Scrolling down: true" />,);ReactNoop.render(<ScrollView row={5} />);await waitForAll(['Scrolling down: true']);expect(ReactNoop).toMatchRenderedOutput(<span prop="Scrolling down: true" />,);ReactNoop.render(<ScrollView row={10} />);await waitForAll(['Scrolling down: true']);expect(ReactNoop).toMatchRenderedOutput(<span prop="Scrolling down: true" />,);ReactNoop.render(<ScrollView row={2} />);await waitForAll(['Scrolling down: false']);expect(ReactNoop).toMatchRenderedOutput(<span prop="Scrolling down: false" />,);ReactNoop.render(<ScrollView row={2} />);await waitForAll(['Scrolling down: false']);expect(ReactNoop).toMatchRenderedOutput(<span prop="Scrolling down: false" />,);});it('warns about render phase update on a different component', async () => {let setStep;function Foo() {const [step, _setStep] = useState(0);setStep = _setStep;return <Text text={`Foo [${step}]`} />;
}function Bar({triggerUpdate}) {if (triggerUpdate) {setStep(x => x + 1);}return <Text text="Bar" />;}const root = ReactNoop.createRoot();await act(() => {root.render(<><Foo /><Bar /></>,);});assertLog(['Foo [0]', 'Bar']);// Bar will update Foo during its render phase. React should warn.root.render(<><Foo /><Bar triggerUpdate={true} /></>,);await expect(async () => await waitForAll(['Foo [0]', 'Bar', 'Foo [1]']),).toErrorDev(['Cannot update a component (`Foo`) while rendering a ' +
'different component (`Bar`). To locate the bad setState() call inside `Bar`',
]);// It should not warn again (deduplication).await act(async () => {root.render(<><Foo /><Bar triggerUpdate={true} /></>,);await waitForAll(['Foo [1]', 'Bar', 'Foo [2]']);});});it('keeps restarting until there are no more new updates', async () => {function Counter({row: newRow}) {const [count, setCount] = useState(0);if (count < 3) {setCount(count + 1);}Scheduler.log('Render: ' + count);return <Text text={count} />;}ReactNoop.render(<Counter />);await waitForAll(['Render: 0', 'Render: 1', 'Render: 2', 'Render: 3', 3]);expect(ReactNoop).toMatchRenderedOutput(<span prop={3} />);});it('updates multiple times within same render function', async () => {function Counter({row: newRow}) {const [count, setCount] = useState(0);if (count < 12) {setCount(c => c + 1);setCount(c => c + 1);setCount(c => c + 1);}Scheduler.log('Render: ' + count);return <Text text={count} />;}ReactNoop.render(<Counter />);await waitForAll([// Should increase by three each time'Render: 0','Render: 3','Render: 6','Render: 9','Render: 12',12,]);expect(ReactNoop).toMatchRenderedOutput(<span prop={12} />);});it('throws after too many iterations', async () => {function Counter({row: newRow}) {const [count, setCount] = useState(0);setCount(count + 1);Scheduler.log('Render: ' + count);return <Text text={count} />;}ReactNoop.render(<Counter />);await waitForThrow('Too many re-renders. React limits the number of renders to prevent ' +'an infinite loop.',);});it('works with useReducer', async () => {function reducer(state, action) {return action === 'increment' ? state + 1 : state;}function Counter({row: newRow}) {const [count, dispatch] = useReducer(reducer, 0);if (count < 3) {dispatch('increment');}Scheduler.log('Render: ' + count);return <Text text={count} />;}ReactNoop.render(<Counter />);await waitForAll(['Render: 0', 'Render: 1', 'Render: 2', 'Render: 3', 3]);expect(ReactNoop).toMatchRenderedOutput(<span prop={3} />);});it('uses reducer passed at time of render, not time of dispatch', async () => {// 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;}}function Counter({row: newRow}, ref) {const [reducer, setReducer] = useState(() => reducerA);const [count, dispatch] = useReducer(reducer, 0);useImperativeHandle(ref, () => ({dispatch}));if (count < 20) {dispatch('increment');// Swap reducers each time we incrementif (reducer === reducerA) {setReducer(() => reducerB);} else {setReducer(() => reducerA);}}Scheduler.log('Render: ' + count);return <Text text={count} />;}Counter = forwardRef(Counter);const counter = React.createRef(null);ReactNoop.render(<Counter ref={counter} />);await waitForAll([// 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).toMatchRenderedOutput(<span prop={21} />);// Test that it works on update, too. This time the log is a bit different// because we started with reducerB instead of reducerA.await act(() => {counter.current.dispatch('reset');});ReactNoop.render(<Counter ref={counter} />);assertLog(['Render: 0','Render: 1','Render: 11','Render: 12','Render: 22',22,]);expect(ReactNoop).toMatchRenderedOutput(<span prop={22} />);});it('discards render phase updates if something suspends', async () => {const thenable = {then() {}};function Foo({signal}) {return (<Suspense fallback="Loading..."><Bar signal={signal} /></Suspense>);}function Bar({signal: newSignal}) {const [counter, setCounter] = useState(0);const [signal, setSignal] = useState(true);// Increment a counter every time the signal changesif (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.log('Suspend!');throw thenable;}}return <Text text={counter} />;}const root = ReactNoop.createRoot();root.render(<Foo signal={true} />);await waitForAll([0]);expect(root).toMatchRenderedOutput(<span prop={0} />);React.startTransition(() => {root.render(<Foo signal={false} />);});await waitForAll(['Suspend!']);expect(root).toMatchRenderedOutput(<span prop={0} />);// Rendering again should suspend again.React.startTransition(() => {root.render(<Foo signal={false} />);});await waitForAll(['Suspend!']);});it('discards render phase updates if something suspends, but not other updates in the same component', async () => {const thenable = {then() {}};function Foo({signal}) {return (<Suspense fallback="Loading..."><Bar signal={signal} /></Suspense>);}let setLabel;function Bar({signal: newSignal}) {const [counter, setCounter] = useState(0);if (counter === 1) {// We're suspending during a render that includes render phase// updates. Those updates should not persist to the next render.Scheduler.log('Suspend!');throw thenable;}const [signal, setSignal] = useState(true);// Increment a counter every time the signal changesif (signal !== newSignal) {setCounter(c => c + 1);setSignal(newSignal);}const [label, _setLabel] = useState('A');setLabel = _setLabel;return <Text text={`${label}:${counter}`} />;
}const root = ReactNoop.createRoot();root.render(<Foo signal={true} />);await waitForAll(['A:0']);expect(root).toMatchRenderedOutput(<span prop="A:0" />);await act(async () => {React.startTransition(() => {root.render(<Foo signal={false} />);setLabel('B');});await waitForAll(['Suspend!']);expect(root).toMatchRenderedOutput(<span prop="A:0" />);// Rendering again should suspend again.React.startTransition(() => {root.render(<Foo signal={false} />);});await waitForAll(['Suspend!']);// Flip the signal back to "cancel" the update. However, the update to// label should still proceed. It shouldn't have been dropped.React.startTransition(() => {root.render(<Foo signal={true} />);});await waitForAll(['B:0']);expect(root).toMatchRenderedOutput(<span prop="B:0" />);});});it('regression: render phase updates cause lower pri work to be dropped', async () => {let setRow;function ScrollView() {const [row, _setRow] = useState(10);setRow = _setRow;const [scrollDirection, setScrollDirection] = useState('Up');const [prevRow, setPrevRow] = useState(null);if (prevRow !== row) {setScrollDirection(prevRow !== null && row > prevRow ? 'Down' : 'Up');setPrevRow(row);}return <Text text={scrollDirection} />;}const root = ReactNoop.createRoot();await act(() => {root.render(<ScrollView row={10} />);});assertLog(['Up']);expect(root).toMatchRenderedOutput(<span prop="Up" />);await act(() => {ReactNoop.discreteUpdates(() => {setRow(5);});React.startTransition(() => {setRow(20);});});assertLog(['Up', 'Down']);expect(root).toMatchRenderedOutput(<span prop="Down" />);});// TODO: This should probably warnit('calling startTransition inside render phase', async () => {function App() {const [counter, setCounter] = useState(0);if (counter === 0) {React.startTransition(() => {setCounter(c => c + 1);});}return <Text text={counter} />;}const root = ReactNoop.createRoot();root.render(<App />);await waitForAll([1]);expect(root).toMatchRenderedOutput(<span prop={1} />);});});describe('useReducer', () => {it('simple mount and update', async () => {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 <Text text={'Count: ' + count} />;}Counter = forwardRef(Counter);const counter = React.createRef(null);ReactNoop.render(<Counter ref={counter} />);await waitForAll(['Count: 0']);expect(ReactNoop).toMatchRenderedOutput(<span prop="Count: 0" />);await act(() => counter.current.dispatch(INCREMENT));assertLog(['Count: 1']);expect(ReactNoop).toMatchRenderedOutput(<span prop="Count: 1" />);await act(() => {counter.current.dispatch(DECREMENT);counter.current.dispatch(DECREMENT);counter.current.dispatch(DECREMENT);});assertLog(['Count: -2']);expect(ReactNoop).toMatchRenderedOutput(<span prop="Count: -2" />);});it('lazy init', async () => {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, props, p => {Scheduler.log('Init');return p.initialCount;});useImperativeHandle(ref, () => ({dispatch}));return <Text text={'Count: ' + count} />;}Counter = forwardRef(Counter);const counter = React.createRef(null);ReactNoop.render(<Counter initialCount={10} ref={counter} />);await waitForAll(['Init', 'Count: 10']);expect(ReactNoop).toMatchRenderedOutput(<span prop="Count: 10" />);await act(() => counter.current.dispatch(INCREMENT));assertLog(['Count: 11']);expect(ReactNoop).toMatchRenderedOutput(<span prop="Count: 11" />);await act(() => {counter.current.dispatch(DECREMENT);counter.current.dispatch(DECREMENT);counter.current.dispatch(DECREMENT);});assertLog(['Count: 8']);expect(ReactNoop).toMatchRenderedOutput(<span prop="Count: 8" />);});// Regression test for https://github.com/facebook/react/issues/14360it('handles dispatches with mixed priorities', async () => {const INCREMENT = 'INCREMENT';function reducer(state, action) {return action === INCREMENT ? state + 1 : state;}function Counter(props, ref) {const [count, dispatch] = useReducer(reducer, 0);useImperativeHandle(ref, () => ({dispatch}));return <Text text={'Count: ' + count} />;}Counter = forwardRef(Counter);const counter = React.createRef(null);ReactNoop.render(<Counter ref={counter} />);await waitForAll(['Count: 0']);expect(ReactNoop).toMatchRenderedOutput(<span prop="Count: 0" />);ReactNoop.batchedUpdates(() => {counter.current.dispatch(INCREMENT);counter.current.dispatch(INCREMENT);counter.current.dispatch(INCREMENT);});ReactNoop.flushSync(() => {counter.current.dispatch(INCREMENT);});if (gate(flags => flags.enableUnifiedSyncLane)) {assertLog(['Count: 4']);expect(ReactNoop).toMatchRenderedOutput(<span prop="Count: 4" />);} else {assertLog(['Count: 1']);expect(ReactNoop).toMatchRenderedOutput(<span prop="Count: 1" />);await waitForAll(['Count: 4']);expect(ReactNoop).toMatchRenderedOutput(<span prop="Count: 4" />);}});});describe('useEffect', () => {it('simple mount and update', async () => {function Counter(props) {useEffect(() => {Scheduler.log(`Passive effect [${props.count}]`);
});return <Text text={'Count: ' + props.count} />;}await act(async () => {ReactNoop.render(<Counter count={0} />, () =>Scheduler.log('Sync effect'),);await waitFor(['Count: 0', 'Sync effect']);expect(ReactNoop).toMatchRenderedOutput(<span prop="Count: 0" />);// Effects are deferred until after the commitawait waitForAll(['Passive effect [0]']);});await act(async () => {ReactNoop.render(<Counter count={1} />, () =>Scheduler.log('Sync effect'),);await waitFor(['Count: 1', 'Sync effect']);expect(ReactNoop).toMatchRenderedOutput(<span prop="Count: 1" />);// Effects are deferred until after the commitawait waitForAll(['Passive effect [1]']);});});it('flushes passive effects even with sibling deletions', async () => {function LayoutEffect(props) {useLayoutEffect(() => {Scheduler.log(`Layout effect`);
});return <Text text="Layout" />;}function PassiveEffect(props) {useEffect(() => {Scheduler.log(`Passive effect`);
}, []);return <Text text="Passive" />;}const passive = <PassiveEffect key="p" />;await act(async () => {ReactNoop.render([<LayoutEffect key="l" />, passive]);await waitFor(['Layout', 'Passive', 'Layout effect']);expect(ReactNoop).toMatchRenderedOutput(<><span prop="Layout" /><span prop="Passive" /></>,);// Destroying the first child shouldn't prevent the passive effect from// being executedReactNoop.render([passive]);await waitForAll(['Passive effect']);expect(ReactNoop).toMatchRenderedOutput(<span prop="Passive" />);});// exiting act calls flushPassiveEffects(), but there are none left to flush.assertLog([]);});it('flushes passive effects even if siblings schedule an update', async () => {function PassiveEffect(props) {useEffect(() => {Scheduler.log('Passive effect');});return <Text text="Passive" />;}function LayoutEffect(props) {const [count, setCount] = useState(0);useLayoutEffect(() => {// Scheduling work shouldn't interfere with the queued passive effectif (count === 0) {setCount(1);}Scheduler.log('Layout effect ' + count);});return <Text text="Layout" />;}ReactNoop.render([<PassiveEffect key="p" />, <LayoutEffect key="l" />]);await act(async () => {await waitForAll(['Passive','Layout','Layout effect 0','Passive effect','Layout','Layout effect 1',]);});expect(ReactNoop).toMatchRenderedOutput(<><span prop="Passive" /><span prop="Layout" /></>,);});it('flushes passive effects even if siblings schedule a new root', async () => {function PassiveEffect(props) {useEffect(() => {Scheduler.log('Passive effect');}, []);return <Text text="Passive" />;}function LayoutEffect(props) {useLayoutEffect(() => {Scheduler.log('Layout effect');// Scheduling work shouldn't interfere with the queued passive effectReactNoop.renderToRootWithID(<Text text="New Root" />, 'root2');});return <Text text="Layout" />;}await act(async () => {ReactNoop.render([<PassiveEffect key="p" />, <LayoutEffect key="l" />]);await waitForAll(['Passive','Layout','Layout effect','Passive effect','New Root',]);expect(ReactNoop).toMatchRenderedOutput(<><span prop="Passive" /><span prop="Layout" /></>,);});});it('flushes effects serially by flushing old effects before flushing ' +"new ones, if they haven't already fired",async () => {function getCommittedText() {const children = ReactNoop.getChildrenAsJSX();if (children === null) {return null;}return children.props.prop;}function Counter(props) {useEffect(() => {Scheduler.log(`Committed state when effect was fired: ${getCommittedText()}`,
);});return <Text text={props.count} />;}await act(async () => {ReactNoop.render(<Counter count={0} />, () =>Scheduler.log('Sync effect'),);await waitFor([0, 'Sync effect']);expect(ReactNoop).toMatchRenderedOutput(<span prop={0} />);// Before the effects have a chance to flush, schedule another updateReactNoop.render(<Counter count={1} />, () =>Scheduler.log('Sync effect'),);await waitFor([// The previous effect flushes before the reconciliation'Committed state when effect was fired: 0',1,'Sync effect',]);expect(ReactNoop).toMatchRenderedOutput(<span prop={1} />);});assertLog(['Committed state when effect was fired: 1']);},);it('defers passive effect destroy functions during unmount', async () => {function Child({bar, foo}) {React.useEffect(() => {Scheduler.log('passive bar create');return () => {Scheduler.log('passive bar destroy');};}, [bar]);React.useLayoutEffect(() => {Scheduler.log('layout bar create');return () => {Scheduler.log('layout bar destroy');};}, [bar]);React.useEffect(() => {Scheduler.log('passive foo create');return () => {Scheduler.log('passive foo destroy');};}, [foo]);React.useLayoutEffect(() => {Scheduler.log('layout foo create');return () => {Scheduler.log('layout foo destroy');};}, [foo]);Scheduler.log('render');return null;}await act(async () => {ReactNoop.render(<Child bar={1} foo={1} />, () =>Scheduler.log('Sync effect'),);await waitFor(['render','layout bar create','layout foo create','Sync effect',]);// Effects are deferred until after the commitawait waitForAll(['passive bar create', 'passive foo create']);});// This update exists to test an internal implementation detail:// Effects without updating dependencies lose their layout/passive tag during an update.await act(async () => {ReactNoop.render(<Child bar={1} foo={2} />, () =>Scheduler.log('Sync effect'),);await waitFor(['render','layout foo destroy','layout foo create','Sync effect',]);// Effects are deferred until after the commitawait waitForAll(['passive foo destroy', 'passive foo create']);});// Unmount the component and verify that passive destroy functions are deferred until post-commit.await act(async () => {ReactNoop.render(null, () => Scheduler.log('Sync effect'));await waitFor(['layout bar destroy','layout foo destroy','Sync effect',]);// Effects are deferred until after the commitawait waitForAll(['passive bar destroy', 'passive foo destroy']);});});it('does not warn about state updates for unmounted components with pending passive unmounts', async () => {let completePendingRequest = null;function Component() {Scheduler.log('Component');const [didLoad, setDidLoad] = React.useState(false);React.useLayoutEffect(() => {Scheduler.log('layout create');return () => {Scheduler.log('layout destroy');};}, []);React.useEffect(() => {Scheduler.log('passive create');// Mimic an XHR request with a complete handler that updates state.completePendingRequest = () => setDidLoad(true);return () => {Scheduler.log('passive destroy');};}, []);return didLoad;}await act(async () => {ReactNoop.renderToRootWithID(<Component />, 'root', () =>Scheduler.log('Sync effect'),);await waitFor(['Component', 'layout create', 'Sync effect']);ReactNoop.flushPassiveEffects();assertLog(['passive create']);// Unmount but don't process pending passive destroy functionReactNoop.unmountRootWithID('root');await waitFor(['layout destroy']);// Simulate an XHR completing, which will cause a state update-// but should not log a warning.completePendingRequest();ReactNoop.flushPassiveEffects();assertLog(['passive destroy']);});});it('does not warn about state updates for unmounted components with pending passive unmounts for alternates', async () => {let setParentState = null;const setChildStates = [];function Parent() {const [state, setState] = useState(true);setParentState = setState;Scheduler.log(`Parent ${state} render`);
useLayoutEffect(() => {Scheduler.log(`Parent ${state} commit`);
});if (state) {return (<><Child label="one" /><Child label="two" /></>);} else {return null;}}function Child({label}) {const [state, setState] = useState(0);useLayoutEffect(() => {Scheduler.log(`Child ${label} commit`);
});useEffect(() => {setChildStates.push(setState);Scheduler.log(`Child ${label} passive create`);
return () => {Scheduler.log(`Child ${label} passive destroy`);
};}, []);Scheduler.log(`Child ${label} render`);
return state;}// Schedule debounced state update for child (prob a no-op for this test)// later tick: schedule unmount for parent// start process unmount (but don't flush passive effectS)// State update on childawait act(async () => {ReactNoop.render(<Parent />);await waitFor(['Parent true render','Child one render','Child two render','Child one commit','Child two commit','Parent true commit','Child one passive create','Child two passive create',]);// Update children.setChildStates.forEach(setChildState => setChildState(1));await waitFor(['Child one render','Child two render','Child one commit','Child two commit',]);// Schedule another update for children, and partially process it.React.startTransition(() => {setChildStates.forEach(setChildState => setChildState(2));});await waitFor(['Child one render']);// Schedule unmount for the parent that unmounts children with pending update.ReactNoop.unstable_runWithPriority(ContinuousEventPriority, () => {setParentState(false);});await waitForPaint(['Parent false render', 'Parent false commit']);// Schedule updates for children too (which should be ignored)setChildStates.forEach(setChildState => setChildState(2));await waitForAll(['Child one passive destroy','Child two passive destroy',]);});});it('does not warn about state updates for unmounted components with no pending passive unmounts', async () => {let completePendingRequest = null;function Component() {Scheduler.log('Component');const [didLoad, setDidLoad] = React.useState(false);React.useLayoutEffect(() => {Scheduler.log('layout create');// Mimic an XHR request with a complete handler that updates state.completePendingRequest = () => setDidLoad(true);return () => {Scheduler.log('layout destroy');};}, []);return didLoad;}await act(async () => {ReactNoop.renderToRootWithID(<Component />, 'root', () =>Scheduler.log('Sync effect'),);await waitFor(['Component', 'layout create', 'Sync effect']);// Unmount but don't process pending passive destroy functionReactNoop.unmountRootWithID('root');await waitFor(['layout destroy']);// Simulate an XHR completing.completePendingRequest();});});it('does not warn if there are pending passive unmount effects but not for the current fiber', async () => {let completePendingRequest = null;function ComponentWithXHR() {Scheduler.log('Component');const [didLoad, setDidLoad] = React.useState(false);React.useLayoutEffect(() => {Scheduler.log('a:layout create');return () => {Scheduler.log('a:layout destroy');};}, []);React.useEffect(() => {Scheduler.log('a:passive create');// Mimic an XHR request with a complete handler that updates state.completePendingRequest = () => setDidLoad(true);}, []);return didLoad;}function ComponentWithPendingPassiveUnmount() {React.useEffect(() => {Scheduler.log('b:passive create');return () => {Scheduler.log('b:passive destroy');};}, []);return null;}await act(async () => {ReactNoop.renderToRootWithID(<><ComponentWithXHR /><ComponentWithPendingPassiveUnmount /></>,'root',() => Scheduler.log('Sync effect'),);await waitFor(['Component', 'a:layout create', 'Sync effect']);ReactNoop.flushPassiveEffects();assertLog(['a:passive create', 'b:passive create']);// Unmount but don't process pending passive destroy functionReactNoop.unmountRootWithID('root');await waitFor(['a:layout destroy']);// Simulate an XHR completing in the component without a pending passive effect..completePendingRequest();});});it('does not warn if there are updates after pending passive unmount effects have been flushed', async () => {let updaterFunction;function Component() {Scheduler.log('Component');const [state, setState] = React.useState(false);updaterFunction = setState;React.useEffect(() => {Scheduler.log('passive create');return () => {Scheduler.log('passive destroy');};}, []);return state;}await act(() => {ReactNoop.renderToRootWithID(<Component />, 'root', () =>Scheduler.log('Sync effect'),);});assertLog(['Component', 'Sync effect', 'passive create']);ReactNoop.unmountRootWithID('root');await waitForAll(['passive destroy']);await act(() => {updaterFunction(true);});});it('does not show a warning when a component updates its own state from within passive unmount function', async () => {function Component() {Scheduler.log('Component');const [didLoad, setDidLoad] = React.useState(false);React.useEffect(() => {Scheduler.log('passive create');return () => {setDidLoad(true);Scheduler.log('passive destroy');};}, []);return didLoad;}await act(async () => {ReactNoop.renderToRootWithID(<Component />, 'root', () =>Scheduler.log('Sync effect'),);await waitFor(['Component', 'Sync effect', 'passive create']);// Unmount but don't process pending passive destroy functionReactNoop.unmountRootWithID('root');await waitForAll(['passive destroy']);});});it('does not show a warning when a component updates a child state from within passive unmount function', async () => {function Parent() {Scheduler.log('Parent');const updaterRef = useRef(null);React.useEffect(() => {Scheduler.log('Parent passive create');return () => {updaterRef.current(true);Scheduler.log('Parent passive destroy');};}, []);return <Child updaterRef={updaterRef} />;}function Child({updaterRef}) {Scheduler.log('Child');const [state, setState] = React.useState(false);React.useEffect(() => {Scheduler.log('Child passive create');updaterRef.current = setState;}, []);return state;}await act(async () => {ReactNoop.renderToRootWithID(<Parent />, 'root');await waitFor(['Parent','Child','Child passive create','Parent passive create',]);// Unmount but don't process pending passive destroy functionReactNoop.unmountRootWithID('root');await waitForAll(['Parent passive destroy']);});});it('does not show a warning when a component updates a parents state from within passive unmount function', async () => {function Parent() {const [state, setState] = React.useState(false);Scheduler.log('Parent');return <Child setState={setState} state={state} />;}function Child({setState, state}) {Scheduler.log('Child');React.useEffect(() => {Scheduler.log('Child passive create');return () => {Scheduler.log('Child passive destroy');setState(true);};}, []);return state;}await act(async () => {ReactNoop.renderToRootWithID(<Parent />, 'root');await waitFor(['Parent', 'Child', 'Child passive create']);// Unmount but don't process pending passive destroy functionReactNoop.unmountRootWithID('root');await waitForAll(['Child passive destroy']);});});it('updates have async priority', async () => {function Counter(props) {const [count, updateCount] = useState('(empty)');useEffect(() => {Scheduler.log(`Schedule update [${props.count}]`);
updateCount(props.count);}, [props.count]);return <Text text={'Count: ' + count} />;}await act(async () => {ReactNoop.render(<Counter count={0} />, () =>Scheduler.log('Sync effect'),);await waitFor(['Count: (empty)', 'Sync effect']);expect(ReactNoop).toMatchRenderedOutput(<span prop="Count: (empty)" />);ReactNoop.flushPassiveEffects();assertLog(['Schedule update [0]']);await waitForAll(['Count: 0']);});await act(async () => {ReactNoop.render(<Counter count={1} />, () =>Scheduler.log('Sync effect'),);await waitFor(['Count: 0', 'Sync effect']);expect(ReactNoop).toMatchRenderedOutput(<span prop="Count: 0" />);ReactNoop.flushPassiveEffects();assertLog(['Schedule update [1]']);await waitForAll(['Count: 1']);});});it('updates have async priority even if effects are flushed early', async () => {function Counter(props) {const [count, updateCount] = useState('(empty)');useEffect(() => {Scheduler.log(`Schedule update [${props.count}]`);
updateCount(props.count);}, [props.count]);return <Text text={'Count: ' + count} />;}await act(async () => {ReactNoop.render(<Counter count={0} />, () =>Scheduler.log('Sync effect'),);await waitFor(['Count: (empty)', 'Sync effect']);expect(ReactNoop).toMatchRenderedOutput(<span prop="Count: (empty)" />);// Rendering again should flush the previous commit's effectsif (gate(flags => flags.forceConcurrentByDefaultForTesting)) {ReactNoop.render(<Counter count={1} />, () =>Scheduler.log('Sync effect'),);} else {React.startTransition(() => {ReactNoop.render(<Counter count={1} />, () =>Scheduler.log('Sync effect'),);});}await waitFor(['Schedule update [0]', 'Count: 0']);if (gate(flags => flags.forceConcurrentByDefaultForTesting)) {expect(ReactNoop).toMatchRenderedOutput(<span prop="Count: (empty)" />,);await waitFor(['Sync effect']);expect(ReactNoop).toMatchRenderedOutput(<span prop="Count: 0" />);ReactNoop.flushPassiveEffects();assertLog(['Schedule update [1]']);await waitForAll(['Count: 1']);} else {expect(ReactNoop).toMatchRenderedOutput(<span prop="Count: 0" />);await waitFor(['Count: 0','Sync effect','Schedule update [1]','Count: 1',]);}expect(ReactNoop).toMatchRenderedOutput(<span prop="Count: 1" />);});});it('does not flush non-discrete passive effects when flushing sync', async () => {let _updateCount;function Counter(props) {const [count, updateCount] = useState(0);_updateCount = updateCount;useEffect(() => {Scheduler.log(`Will set count to 1`);
updateCount(1);}, []);return <Text text={'Count: ' + count} />;}ReactNoop.render(<Counter count={0} />, () =>Scheduler.log('Sync effect'),);await waitFor(['Count: 0', 'Sync effect']);expect(ReactNoop).toMatchRenderedOutput(<span prop="Count: 0" />);// A flush sync doesn't cause the passive effects to fire.// So we haven't added the other update yet.await act(() => {ReactNoop.flushSync(() => {_updateCount(2);});});// As a result we, somewhat surprisingly, commit them in the opposite order.// This should be fine because any non-discrete set of work doesn't guarantee order// and easily could've happened slightly later too.if (gate(flags => flags.enableUnifiedSyncLane)) {assertLog(['Will set count to 1', 'Count: 1']);} else {assertLog(['Will set count to 1', 'Count: 2', 'Count: 1']);}expect(ReactNoop).toMatchRenderedOutput(<span prop="Count: 1" />);});it('in legacy mode, useEffect is deferred and updates finish synchronously ' +'(in a single batch)',async () => {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 <Text text={'Count: ' + count} />;}await act(() => {ReactNoop.flushSync(() => {ReactNoop.renderLegacySyncRoot(<Counter count={0} />);});// Even in legacy mode, effects are deferred until after paintassertLog(['Count: (empty)']);expect(ReactNoop).toMatchRenderedOutput(<span prop="Count: (empty)" />,);});// effects get forced on exiting act()// There were multiple updates, but there should only be a// single renderassertLog(['Count: 0']);expect(ReactNoop).toMatchRenderedOutput(<span prop="Count: 0" />);},);it('flushSync is not allowed', async () => {function Counter(props) {const [count, updateCount] = useState('(empty)');useEffect(() => {Scheduler.log(`Schedule update [${props.count}]`);
ReactNoop.flushSync(() => {updateCount(props.count);});assertLog([`Schedule update [${props.count}]`]);
// This shouldn't flush synchronously.expect(ReactNoop).not.toMatchRenderedOutput(<span prop={`Count: ${props.count}`} />,
);}, [props.count]);return <Text text={'Count: ' + count} />;}await expect(async () => {await act(async () => {ReactNoop.render(<Counter count={0} />, () =>Scheduler.log('Sync effect'),);await waitFor(['Count: (empty)', 'Sync effect']);expect(ReactNoop).toMatchRenderedOutput(<span prop="Count: (empty)" />,);});}).toErrorDev('flushSync was called from inside a lifecycle method');assertLog([`Count: 0`]);
expect(ReactNoop).toMatchRenderedOutput(<span prop="Count: 0" />);});it('unmounts previous effect', async () => {function Counter(props) {useEffect(() => {Scheduler.log(`Did create [${props.count}]`);
return () => {Scheduler.log(`Did destroy [${props.count}]`);
};});return <Text text={'Count: ' + props.count} />;}await act(async () => {ReactNoop.render(<Counter count={0} />, () =>Scheduler.log('Sync effect'),);await waitFor(['Count: 0', 'Sync effect']);expect(ReactNoop).toMatchRenderedOutput(<span prop="Count: 0" />);});assertLog(['Did create [0]']);await act(async () => {ReactNoop.render(<Counter count={1} />, () =>Scheduler.log('Sync effect'),);await waitFor(['Count: 1', 'Sync effect']);expect(ReactNoop).toMatchRenderedOutput(<span prop="Count: 1" />);});assertLog(['Did destroy [0]', 'Did create [1]']);});it('unmounts on deletion', async () => {function Counter(props) {useEffect(() => {Scheduler.log(`Did create [${props.count}]`);
return () => {Scheduler.log(`Did destroy [${props.count}]`);
};});return <Text text={'Count: ' + props.count} />;}await act(async () => {ReactNoop.render(<Counter count={0} />, () =>Scheduler.log('Sync effect'),);await waitFor(['Count: 0', 'Sync effect']);expect(ReactNoop).toMatchRenderedOutput(<span prop="Count: 0" />);});assertLog(['Did create [0]']);ReactNoop.render(null);await waitForAll(['Did destroy [0]']);expect(ReactNoop).toMatchRenderedOutput(null);});it('unmounts on deletion after skipped effect', async () => {function Counter(props) {useEffect(() => {Scheduler.log(`Did create [${props.count}]`);
return () => {Scheduler.log(`Did destroy [${props.count}]`);
};}, []);return <Text text={'Count: ' + props.count} />;}await act(async () => {ReactNoop.render(<Counter count={0} />, () =>Scheduler.log('Sync effect'),);await waitFor(['Count: 0', 'Sync effect']);expect(ReactNoop).toMatchRenderedOutput(<span prop="Count: 0" />);});assertLog(['Did create [0]']);await act(async () => {ReactNoop.render(<Counter count={1} />, () =>Scheduler.log('Sync effect'),);await waitFor(['Count: 1', 'Sync effect']);expect(ReactNoop).toMatchRenderedOutput(<span prop="Count: 1" />);});assertLog([]);ReactNoop.render(null);await waitForAll(['Did destroy [0]']);expect(ReactNoop).toMatchRenderedOutput(null);});it('always fires effects if no dependencies are provided', async () => {function effect() {Scheduler.log(`Did create`);
return () => {Scheduler.log(`Did destroy`);
};}function Counter(props) {useEffect(effect);return <Text text={'Count: ' + props.count} />;}await act(async () => {ReactNoop.render(<Counter count={0} />, () =>Scheduler.log('Sync effect'),);await waitFor(['Count: 0', 'Sync effect']);expect(ReactNoop).toMatchRenderedOutput(<span prop="Count: 0" />);});assertLog(['Did create']);await act(async () => {ReactNoop.render(<Counter count={1} />, () =>Scheduler.log('Sync effect'),);await waitFor(['Count: 1', 'Sync effect']);expect(ReactNoop).toMatchRenderedOutput(<span prop="Count: 1" />);});assertLog(['Did destroy', 'Did create']);ReactNoop.render(null);await waitForAll(['Did destroy']);expect(ReactNoop).toMatchRenderedOutput(null);});it('skips effect if inputs have not changed', async () => {function Counter(props) {const text = `${props.label}: ${props.count}`;
useEffect(() => {Scheduler.log(`Did create [${text}]`);
return () => {Scheduler.log(`Did destroy [${text}]`);
};}, [props.label, props.count]);return <Text text={text} />;}await act(async () => {ReactNoop.render(<Counter label="Count" count={0} />, () =>Scheduler.log('Sync effect'),);await waitFor(['Count: 0', 'Sync effect']);});assertLog(['Did create [Count: 0]']);expect(ReactNoop).toMatchRenderedOutput(<span prop="Count: 0" />);await act(async () => {ReactNoop.render(<Counter label="Count" count={1} />, () =>Scheduler.log('Sync effect'),);// Count changedawait waitFor(['Count: 1', 'Sync effect']);expect(ReactNoop).toMatchRenderedOutput(<span prop="Count: 1" />);});assertLog(['Did destroy [Count: 0]', 'Did create [Count: 1]']);await act(async () => {ReactNoop.render(<Counter label="Count" count={1} />, () =>Scheduler.log('Sync effect'),);// Nothing changed, so no effect should have firedawait waitFor(['Count: 1', 'Sync effect']);});assertLog([]);expect(ReactNoop).toMatchRenderedOutput(<span prop="Count: 1" />);await act(async () => {ReactNoop.render(<Counter label="Total" count={1} />, () =>Scheduler.log('Sync effect'),);// Label changedawait waitFor(['Total: 1', 'Sync effect']);expect(ReactNoop).toMatchRenderedOutput(<span prop="Total: 1" />);});assertLog(['Did destroy [Count: 1]', 'Did create [Total: 1]']);});it('multiple effects', async () => {function Counter(props) {useEffect(() => {Scheduler.log(`Did commit 1 [${props.count}]`);
});useEffect(() => {Scheduler.log(`Did commit 2 [${props.count}]`);
});return <Text text={'Count: ' + props.count} />;}await act(async () => {ReactNoop.render(<Counter count={0} />, () =>Scheduler.log('Sync effect'),);await waitFor(['Count: 0', 'Sync effect']);expect(ReactNoop).toMatchRenderedOutput(<span prop="Count: 0" />);});assertLog(['Did commit 1 [0]', 'Did commit 2 [0]']);await act(async () => {ReactNoop.render(<Counter count={1} />, () =>Scheduler.log('Sync effect'),);await waitFor(['Count: 1', 'Sync effect']);expect(ReactNoop).toMatchRenderedOutput(<span prop="Count: 1" />);});assertLog(['Did commit 1 [1]', 'Did commit 2 [1]']);});it('unmounts all previous effects before creating any new ones', async () => {function Counter(props) {useEffect(() => {Scheduler.log(`Mount A [${props.count}]`);
return () => {Scheduler.log(`Unmount A [${props.count}]`);
};});useEffect(() => {Scheduler.log(`Mount B [${props.count}]`);
return () => {Scheduler.log(`Unmount B [${props.count}]`);
};});return <Text text={'Count: ' + props.count} />;}await act(async () => {ReactNoop.render(<Counter count={0} />, () =>Scheduler.log('Sync effect'),);await waitFor(['Count: 0', 'Sync effect']);expect(ReactNoop).toMatchRenderedOutput(<span prop="Count: 0" />);});assertLog(['Mount A [0]', 'Mount B [0]']);await act(async () => {ReactNoop.render(<Counter count={1} />, () =>Scheduler.log('Sync effect'),);await waitFor(['Count: 1', 'Sync effect']);expect(ReactNoop).toMatchRenderedOutput(<span prop="Count: 1" />);});assertLog(['Unmount A [0]','Unmount B [0]','Mount A [1]','Mount B [1]',]);});it('unmounts all previous effects between siblings before creating any new ones', async () => {function Counter({count, label}) {useEffect(() => {Scheduler.log(`Mount ${label} [${count}]`);
return () => {Scheduler.log(`Unmount ${label} [${count}]`);
};});return <Text text={`${label} ${count}`} />;
}await act(async () => {ReactNoop.render(<><Counter label="A" count={0} /><Counter label="B" count={0} /></>,() => Scheduler.log('Sync effect'),);await waitFor(['A 0', 'B 0', 'Sync effect']);expect(ReactNoop).toMatchRenderedOutput(<><span prop="A 0" /><span prop="B 0" /></>,);});assertLog(['Mount A [0]', 'Mount B [0]']);await act(async () => {ReactNoop.render(<><Counter label="A" count={1} /><Counter label="B" count={1} /></>,() => Scheduler.log('Sync effect'),);await waitFor(['A 1', 'B 1', 'Sync effect']);expect(ReactNoop).toMatchRenderedOutput(<><span prop="A 1" /><span prop="B 1" /></>,);});assertLog(['Unmount A [0]','Unmount B [0]','Mount A [1]','Mount B [1]',]);await act(async () => {ReactNoop.render(<><Counter label="B" count={2} /><Counter label="C" count={0} /></>,() => Scheduler.log('Sync effect'),);await waitFor(['B 2', 'C 0', 'Sync effect']);expect(ReactNoop).toMatchRenderedOutput(<><span prop="B 2" /><span prop="C 0" /></>,);});assertLog(['Unmount A [1]','Unmount B [1]','Mount B [2]','Mount C [0]',]);});it('handles errors in create on mount', async () => {function Counter(props) {useEffect(() => {Scheduler.log(`Mount A [${props.count}]`);
return () => {Scheduler.log(`Unmount A [${props.count}]`);
};});useEffect(() => {Scheduler.log('Oops!');throw new Error('Oops!');// eslint-disable-next-line no-unreachableScheduler.log(`Mount B [${props.count}]`);
return () => {Scheduler.log(`Unmount B [${props.count}]`);
};});return <Text text={'Count: ' + props.count} />;}await act(async () => {ReactNoop.render(<Counter count={0} />, () =>Scheduler.log('Sync effect'),);await waitFor(['Count: 0', 'Sync effect']);expect(ReactNoop).toMatchRenderedOutput(<span prop="Count: 0" />);expect(() => ReactNoop.flushPassiveEffects()).toThrow('Oops');});assertLog(['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).toMatchRenderedOutput(null);});it('handles errors in create on update', async () => {function Counter(props) {useEffect(() => {Scheduler.log(`Mount A [${props.count}]`);
return () => {Scheduler.log(`Unmount A [${props.count}]`);
};});useEffect(() => {if (props.count === 1) {Scheduler.log('Oops!');throw new Error('Oops!');}Scheduler.log(`Mount B [${props.count}]`);
return () => {Scheduler.log(`Unmount B [${props.count}]`);
};});return <Text text={'Count: ' + props.count} />;}await act(async () => {ReactNoop.render(<Counter count={0} />, () =>Scheduler.log('Sync effect'),);await waitFor(['Count: 0', 'Sync effect']);expect(ReactNoop).toMatchRenderedOutput(<span prop="Count: 0" />);ReactNoop.flushPassiveEffects();assertLog(['Mount A [0]', 'Mount B [0]']);});await act(async () => {// This update will trigger an errorReactNoop.render(<Counter count={1} />, () =>Scheduler.log('Sync effect'),);await waitFor(['Count: 1', 'Sync effect']);expect(ReactNoop).toMatchRenderedOutput(<span prop="Count: 1" />);expect(() => ReactNoop.flushPassiveEffects()).toThrow('Oops');assertLog(['Unmount A [0]', 'Unmount B [0]', 'Mount A [1]', 'Oops!']);expect(ReactNoop).toMatchRenderedOutput(null);});assertLog([// Clean up effect A runs passively on unmount.// There's no effect B to clean-up, because it never mounted.'Unmount A [1]',]);});it('handles errors in destroy on update', async () => {function Counter(props) {useEffect(() => {Scheduler.log(`Mount A [${props.count}]`);
return () => {Scheduler.log('Oops!');if (props.count === 0) {throw new Error('Oops!');}};});useEffect(() => {Scheduler.log(`Mount B [${props.count}]`);
return () => {Scheduler.log(`Unmount B [${props.count}]`);
};});return <Text text={'Count: ' + props.count} />;}await act(async () => {ReactNoop.render(<Counter count={0} />, () =>Scheduler.log('Sync effect'),);await waitFor(['Count: 0', 'Sync effect']);expect(ReactNoop).toMatchRenderedOutput(<span prop="Count: 0" />);ReactNoop.flushPassiveEffects();assertLog(['Mount A [0]', 'Mount B [0]']);});await act(async () => {// This update will trigger an error during passive effect unmountReactNoop.render(<Counter count={1} />, () =>Scheduler.log('Sync effect'),);await waitFor(['Count: 1', 'Sync effect']);expect(ReactNoop).toMatchRenderedOutput(<span prop="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.assertLog(['Oops!', 'Unmount B [0]', 'Mount A [1]', 'Mount B [1]']);});// <Counter> 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).assertLog(['Oops!', 'Unmount B [1]']);expect(ReactNoop).toMatchRenderedOutput(null);});it('works with memo', async () => {function Counter({count}) {useLayoutEffect(() => {Scheduler.log('Mount: ' + count);return () => Scheduler.log('Unmount: ' + count);});return <Text text={'Count: ' + count} />;}Counter = memo(Counter);ReactNoop.render(<Counter count={0} />, () =>Scheduler.log('Sync effect'),);await waitFor(['Count: 0', 'Mount: 0', 'Sync effect']);expect(ReactNoop).toMatchRenderedOutput(<span prop="Count: 0" />);ReactNoop.render(<Counter count={1} />, () =>Scheduler.log('Sync effect'),);await waitFor(['Count: 1', 'Unmount: 0', 'Mount: 1', 'Sync effect']);expect(ReactNoop).toMatchRenderedOutput(<span prop="Count: 1" />);ReactNoop.render(null);await waitFor(['Unmount: 1']);expect(ReactNoop).toMatchRenderedOutput(null);});describe('errors thrown in passive destroy function within unmounted trees', () => {let BrokenUseEffectCleanup;let ErrorBoundary;let LogOnlyErrorBoundary;beforeEach(() => {BrokenUseEffectCleanup = function () {useEffect(() => {Scheduler.log('BrokenUseEffectCleanup useEffect');return () => {Scheduler.log('BrokenUseEffectCleanup useEffect destroy');throw new Error('Expected error');};}, []);return 'inner child';};ErrorBoundary = class extends React.Component {state = {error: null};static getDerivedStateFromError(error) {Scheduler.log(`ErrorBoundary static getDerivedStateFromError`);
return {error};}componentDidCatch(error, info) {Scheduler.log(`ErrorBoundary componentDidCatch`);
}render() {if (this.state.error) {Scheduler.log('ErrorBoundary render error');return <span prop="ErrorBoundary fallback" />;}Scheduler.log('ErrorBoundary render success');return this.props.children || null;}};LogOnlyErrorBoundary = class extends React.Component {componentDidCatch(error, info) {Scheduler.log(`LogOnlyErrorBoundary componentDidCatch`);
}render() {Scheduler.log(`LogOnlyErrorBoundary render`);
return this.props.children || null;}};});it('should use the nearest still-mounted boundary if there are no unmounted boundaries', async () => {await act(() => {ReactNoop.render(<LogOnlyErrorBoundary><BrokenUseEffectCleanup /></LogOnlyErrorBoundary>,);});assertLog(['LogOnlyErrorBoundary render','BrokenUseEffectCleanup useEffect',]);await act(() => {ReactNoop.render(<LogOnlyErrorBoundary />);});assertLog(['LogOnlyErrorBoundary render','BrokenUseEffectCleanup useEffect destroy','LogOnlyErrorBoundary componentDidCatch',]);});it('should skip unmounted boundaries and use the nearest still-mounted boundary', async () => {function Conditional({showChildren}) {if (showChildren) {return (<ErrorBoundary><BrokenUseEffectCleanup /></ErrorBoundary>);} else {return null;}}await act(() => {ReactNoop.render(<LogOnlyErrorBoundary><Conditional showChildren={true} /></LogOnlyErrorBoundary>,);});assertLog(['LogOnlyErrorBoundary render','ErrorBoundary render success','BrokenUseEffectCleanup useEffect',]);await act(() => {ReactNoop.render(<LogOnlyErrorBoundary><Conditional showChildren={false} /></LogOnlyErrorBoundary>,);});assertLog(['LogOnlyErrorBoundary render','BrokenUseEffectCleanup useEffect destroy','LogOnlyErrorBoundary componentDidCatch',]);});it('should call getDerivedStateFromError in the nearest still-mounted boundary', async () => {function Conditional({showChildren}) {if (showChildren) {return <BrokenUseEffectCleanup />;} else {return null;}}await act(() => {ReactNoop.render(<ErrorBoundary><Conditional showChildren={true} /></ErrorBoundary>,);});assertLog(['ErrorBoundary render success','BrokenUseEffectCleanup useEffect',]);await act(() => {ReactNoop.render(<ErrorBoundary><Conditional showChildren={false} /></ErrorBoundary>,);});assertLog(['ErrorBoundary render success','BrokenUseEffectCleanup useEffect destroy','ErrorBoundary static getDerivedStateFromError','ErrorBoundary render error','ErrorBoundary componentDidCatch',]);expect(ReactNoop).toMatchRenderedOutput(<span prop="ErrorBoundary fallback" />,);});it('should rethrow error if there are no still-mounted boundaries', async () => {function Conditional({showChildren}) {if (showChildren) {return (<ErrorBoundary><BrokenUseEffectCleanup /></ErrorBoundary>);} else {return null;}}await act(() => {ReactNoop.render(<Conditional showChildren={true} />);});assertLog(['ErrorBoundary render success','BrokenUseEffectCleanup useEffect',]);await act(async () => {ReactNoop.render(<Conditional showChildren={false} />);await waitForThrow('Expected error');});assertLog(['BrokenUseEffectCleanup useEffect destroy']);expect(ReactNoop).toMatchRenderedOutput(null);});});it('calls passive effect destroy functions for memoized components', async () => {const Wrapper = ({children}) => children;function Child() {React.useEffect(() => {Scheduler.log('passive create');return () => {Scheduler.log('passive destroy');};}, []);React.useLayoutEffect(() => {Scheduler.log('layout create');return () => {Scheduler.log('layout destroy');};}, []);Scheduler.log('render');return null;}const isEqual = (prevProps, nextProps) =>prevProps.prop === nextProps.prop;const MemoizedChild = React.memo(Child, isEqual);await act(() => {ReactNoop.render(<Wrapper><MemoizedChild key={1} /></Wrapper>,);});assertLog(['render', 'layout create', 'passive create']);// Include at least one no-op (memoized) update to trigger original bug.await act(() => {ReactNoop.render(<Wrapper><MemoizedChild key={1} /></Wrapper>,);});assertLog([]);await act(() => {ReactNoop.render(<Wrapper><MemoizedChild key={2} /></Wrapper>,);});assertLog(['render','layout destroy','layout create','passive destroy','passive create',]);await act(() => {ReactNoop.render(null);});assertLog(['layout destroy', 'passive destroy']);});it('calls passive effect destroy functions for descendants of memoized components', async () => {const Wrapper = ({children}) => children;function Child() {return <Grandchild />;}function Grandchild() {React.useEffect(() => {Scheduler.log('passive create');return () => {Scheduler.log('passive destroy');};}, []);React.useLayoutEffect(() => {Scheduler.log('layout create');return () => {Scheduler.log('layout destroy');};}, []);Scheduler.log('render');return null;}const isEqual = (prevProps, nextProps) =>prevProps.prop === nextProps.prop;const MemoizedChild = React.memo(Child, isEqual);await act(() => {ReactNoop.render(<Wrapper><MemoizedChild key={1} /></Wrapper>,);});assertLog(['render', 'layout create', 'passive create']);// Include at least one no-op (memoized) update to trigger original bug.await act(() => {ReactNoop.render(<Wrapper><MemoizedChild key={1} /></Wrapper>,);});assertLog([]);await act(() => {ReactNoop.render(<Wrapper><MemoizedChild key={2} /></Wrapper>,);});assertLog(['render','layout destroy','layout create','passive destroy','passive create',]);await act(() => {ReactNoop.render(null);});assertLog(['layout destroy', 'passive destroy']);});it('assumes passive effect destroy function is either a function or undefined', async () => {function App(props) {useEffect(() => {return props.return;});return null;}const root1 = ReactNoop.createRoot();await expect(async () => {await act(() => {root1.render(<App return={17} />);});}).toErrorDev(['Warning: useEffect must not return anything besides a ' +'function, which is used for clean-up. You returned: 17',]);const root2 = ReactNoop.createRoot();await expect(async () => {await act(() => {root2.render(<App return={null} />);});}).toErrorDev(['Warning: useEffect must not return anything besides a ' +'function, which is used for clean-up. You returned null. If your ' +'effect does not require clean up, return undefined (or nothing).',]);const root3 = ReactNoop.createRoot();await expect(async () => {await act(() => {root3.render(<App return={Promise.resolve()} />);});}).toErrorDev(['Warning: useEffect must not return anything besides a ' +'function, which is used for clean-up.\n\n' +
'It looks like you wrote useEffect(async () => ...) or returned a Promise.',]);// Error on unmount because React assumes the value is a functionawait act(async () => {root3.render(null);await waitForThrow('is not a function');});});});describe('useInsertionEffect', () => {it('fires insertion effects after snapshots on update', async () => {function CounterA(props) {useInsertionEffect(() => {Scheduler.log(`Create insertion`);
return () => {Scheduler.log(`Destroy insertion`);
};});return null;}class CounterB extends React.Component {getSnapshotBeforeUpdate(prevProps, prevState) {Scheduler.log(`Get Snapshot`);
return null;}componentDidUpdate() {}render() {return null;}}await act(async () => {ReactNoop.render(<><CounterA /><CounterB /></>,);await waitForAll(['Create insertion']);});// Updateawait act(async () => {ReactNoop.render(<><CounterA /><CounterB /></>,);await waitForAll(['Get Snapshot','Destroy insertion','Create insertion',]);});// Unmount everythingawait act(async () => {ReactNoop.render(null);await waitForAll(['Destroy insertion']);});});it('fires insertion effects before layout effects', async () => {let committedText = '(empty)';function Counter(props) {useInsertionEffect(() => {Scheduler.log(`Create insertion [current: ${committedText}]`);
committedText = String(props.count);return () => {Scheduler.log(`Destroy insertion [current: ${committedText}]`);
};});useLayoutEffect(() => {Scheduler.log(`Create layout [current: ${committedText}]`);
return () => {Scheduler.log(`Destroy layout [current: ${committedText}]`);
};});useEffect(() => {Scheduler.log(`Create passive [current: ${committedText}]`);
return () => {Scheduler.log(`Destroy passive [current: ${committedText}]`);
};});return null;}await act(async () => {ReactNoop.render(<Counter count={0} />);await waitForPaint(['Create insertion [current: (empty)]','Create layout [current: 0]',]);expect(committedText).toEqual('0');});assertLog(['Create passive [current: 0]']);// Unmount everythingawait act(async () => {ReactNoop.render(null);await waitForPaint(['Destroy insertion [current: 0]','Destroy layout [current: 0]',]);});assertLog(['Destroy passive [current: 0]']);});it('force flushes passive effects before firing new insertion effects', async () => {let committedText = '(empty)';function Counter(props) {useInsertionEffect(() => {Scheduler.log(`Create insertion [current: ${committedText}]`);
committedText = String(props.count);return () => {Scheduler.log(`Destroy insertion [current: ${committedText}]`);
};});useLayoutEffect(() => {Scheduler.log(`Create layout [current: ${committedText}]`);
committedText = String(props.count);return () => {Scheduler.log(`Destroy layout [current: ${committedText}]`);
};});useEffect(() => {Scheduler.log(`Create passive [current: ${committedText}]`);
return () => {Scheduler.log(`Destroy passive [current: ${committedText}]`);
};});return null;}await act(async () => {React.startTransition(() => {ReactNoop.render(<Counter count={0} />);});await waitForPaint(['Create insertion [current: (empty)]','Create layout [current: 0]',]);expect(committedText).toEqual('0');React.startTransition(() => {ReactNoop.render(<Counter count={1} />);});await waitForPaint(['Create passive [current: 0]','Destroy insertion [current: 0]','Create insertion [current: 0]','Destroy layout [current: 1]','Create layout [current: 1]',]);expect(committedText).toEqual('1');});assertLog(['Destroy passive [current: 1]','Create passive [current: 1]',]);});it('fires all insertion effects (interleaved) before firing any layout effects', async () => {let committedA = '(empty)';let committedB = '(empty)';function CounterA(props) {useInsertionEffect(() => {Scheduler.log(`Create Insertion 1 for Component A [A: ${committedA}, B: ${committedB}]`,
);committedA = String(props.count);return () => {Scheduler.log(`Destroy Insertion 1 for Component A [A: ${committedA}, B: ${committedB}]`,
);};});useInsertionEffect(() => {Scheduler.log(`Create Insertion 2 for Component A [A: ${committedA}, B: ${committedB}]`,
);committedA = String(props.count);return () => {Scheduler.log(`Destroy Insertion 2 for Component A [A: ${committedA}, B: ${committedB}]`,
);};});useLayoutEffect(() => {Scheduler.log(`Create Layout 1 for Component A [A: ${committedA}, B: ${committedB}]`,
);return () => {Scheduler.log(`Destroy Layout 1 for Component A [A: ${committedA}, B: ${committedB}]`,
);};});useLayoutEffect(() => {Scheduler.log(`Create Layout 2 for Component A [A: ${committedA}, B: ${committedB}]`,
);return () => {Scheduler.log(`Destroy Layout 2 for Component A [A: ${committedA}, B: ${committedB}]`,
);};});return null;}function CounterB(props) {useInsertionEffect(() => {Scheduler.log(`Create Insertion 1 for Component B [A: ${committedA}, B: ${committedB}]`,
);committedB = String(props.count);return () => {Scheduler.log(`Destroy Insertion 1 for Component B [A: ${committedA}, B: ${committedB}]`,
);};});useInsertionEffect(() => {Scheduler.log(`Create Insertion 2 for Component B [A: ${committedA}, B: ${committedB}]`,
);committedB = String(props.count);return () => {Scheduler.log(`Destroy Insertion 2 for Component B [A: ${committedA}, B: ${committedB}]`,
);};});useLayoutEffect(() => {Scheduler.log(`Create Layout 1 for Component B [A: ${committedA}, B: ${committedB}]`,
);return () => {Scheduler.log(`Destroy Layout 1 for Component B [A: ${committedA}, B: ${committedB}]`,
);};});useLayoutEffect(() => {Scheduler.log(`Create Layout 2 for Component B [A: ${committedA}, B: ${committedB}]`,
);return () => {Scheduler.log(`Destroy Layout 2 for Component B [A: ${committedA}, B: ${committedB}]`,
);};});return null;}await act(async () => {ReactNoop.render(<React.Fragment><CounterA count={0} /><CounterB count={0} /></React.Fragment>,);await waitForAll([// All insertion effects fire before all layout effects'Create Insertion 1 for Component A [A: (empty), B: (empty)]','Create Insertion 2 for Component A [A: 0, B: (empty)]','Create Insertion 1 for Component B [A: 0, B: (empty)]','Create Insertion 2 for Component B [A: 0, B: 0]','Create Layout 1 for Component A [A: 0, B: 0]','Create Layout 2 for Component A [A: 0, B: 0]','Create Layout 1 for Component B [A: 0, B: 0]','Create Layout 2 for Component B [A: 0, B: 0]',]);expect([committedA, committedB]).toEqual(['0', '0']);});await act(async () => {ReactNoop.render(<React.Fragment><CounterA count={1} /><CounterB count={1} /></React.Fragment>,);await waitForAll(['Destroy Insertion 1 for Component A [A: 0, B: 0]','Destroy Insertion 2 for Component A [A: 0, B: 0]','Create Insertion 1 for Component A [A: 0, B: 0]','Create Insertion 2 for Component A [A: 1, B: 0]','Destroy Layout 1 for Component A [A: 1, B: 0]','Destroy Layout 2 for Component A [A: 1, B: 0]','Destroy Insertion 1 for Component B [A: 1, B: 0]','Destroy Insertion 2 for Component B [A: 1, B: 0]','Create Insertion 1 for Component B [A: 1, B: 0]','Create Insertion 2 for Component B [A: 1, B: 1]','Destroy Layout 1 for Component B [A: 1, B: 1]','Destroy Layout 2 for Component B [A: 1, B: 1]','Create Layout 1 for Component A [A: 1, B: 1]','Create Layout 2 for Component A [A: 1, B: 1]','Create Layout 1 for Component B [A: 1, B: 1]','Create Layout 2 for Component B [A: 1, B: 1]',]);expect([committedA, committedB]).toEqual(['1', '1']);// Unmount everythingawait act(async () => {ReactNoop.render(null);await waitForAll(['Destroy Insertion 1 for Component A [A: 1, B: 1]','Destroy Insertion 2 for Component A [A: 1, B: 1]','Destroy Layout 1 for Component A [A: 1, B: 1]','Destroy Layout 2 for Component A [A: 1, B: 1]','Destroy Insertion 1 for Component B [A: 1, B: 1]','Destroy Insertion 2 for Component B [A: 1, B: 1]','Destroy Layout 1 for Component B [A: 1, B: 1]','Destroy Layout 2 for Component B [A: 1, B: 1]',]);});});});it('assumes insertion effect destroy function is either a function or undefined', async () => {function App(props) {useInsertionEffect(() => {return props.return;});return null;}const root1 = ReactNoop.createRoot();await expect(async () => {await act(() => {root1.render(<App return={17} />);});}).toErrorDev(['Warning: useInsertionEffect must not return anything besides a ' +'function, which is used for clean-up. You returned: 17',]);const root2 = ReactNoop.createRoot();await expect(async () => {await act(() => {root2.render(<App return={null} />);});}).toErrorDev(['Warning: useInsertionEffect must not return anything besides a ' +'function, which is used for clean-up. You returned null. If your ' +'effect does not require clean up, return undefined (or nothing).',]);const root3 = ReactNoop.createRoot();await expect(async () => {await act(() => {root3.render(<App return={Promise.resolve()} />);});}).toErrorDev(['Warning: useInsertionEffect must not return anything besides a ' +'function, which is used for clean-up.\n\n' +
'It looks like you wrote useInsertionEffect(async () => ...) or returned a Promise.',]);// Error on unmount because React assumes the value is a functionawait act(async () => {root3.render(null);await waitForThrow('is not a function');});});it('warns when setState is called from insertion effect setup', async () => {function App(props) {const [, setX] = useState(0);useInsertionEffect(() => {setX(1);if (props.throw) {throw Error('No');}}, [props.throw]);return null;}const root = ReactNoop.createRoot();await expect(async () => {await act(() => {root.render(<App />);});}).toErrorDev(['Warning: useInsertionEffect must not schedule updates.']);await act(async () => {root.render(<App throw={true} />);await waitForThrow('No');});// Should not warn for regular effects after throw.function NotInsertion() {const [, setX] = useState(0);useEffect(() => {setX(1);}, []);return null;}await act(() => {root.render(<NotInsertion />);});});it('warns when setState is called from insertion effect cleanup', async () => {function App(props) {const [, setX] = useState(0);useInsertionEffect(() => {if (props.throw) {throw Error('No');}return () => {setX(1);};}, [props.throw, props.foo]);return null;}const root = ReactNoop.createRoot();await act(() => {root.render(<App foo="hello" />);});await expect(async () => {await act(() => {root.render(<App foo="goodbye" />);});}).toErrorDev(['Warning: useInsertionEffect must not schedule updates.']);await act(async () => {root.render(<App throw={true} />);await waitForThrow('No');});// Should not warn for regular effects after throw.function NotInsertion() {const [, setX] = useState(0);useEffect(() => {setX(1);}, []);return null;}await act(() => {root.render(<NotInsertion />);});});});describe('useLayoutEffect', () => {it('fires layout effects after the host has been mutated', async () => {function getCommittedText() {const yields = Scheduler.unstable_clearLog();const children = ReactNoop.getChildrenAsJSX();Scheduler.log(yields);if (children === null) {return null;}return children.props.prop;}function Counter(props) {useLayoutEffect(() => {Scheduler.log(`Current: ${getCommittedText()}`);
});return <Text text={props.count} />;}ReactNoop.render(<Counter count={0} />, () =>Scheduler.log('Sync effect'),);await waitFor([[0], 'Current: 0', 'Sync effect']);expect(ReactNoop).toMatchRenderedOutput(<span prop={0} />);ReactNoop.render(<Counter count={1} />, () =>Scheduler.log('Sync effect'),);await waitFor([[1], 'Current: 1', 'Sync effect']);expect(ReactNoop).toMatchRenderedOutput(<span prop={1} />);});it('force flushes passive effects before firing new layout effects', async () => {let committedText = '(empty)';function Counter(props) {useLayoutEffect(() => {// Normally this would go in a mutation effect, but this test// intentionally omits a mutation effect.committedText = String(props.count);Scheduler.log(`Mount layout [current: ${committedText}]`);
return () => {Scheduler.log(`Unmount layout [current: ${committedText}]`);
};});useEffect(() => {Scheduler.log(`Mount normal [current: ${committedText}]`);
return () => {Scheduler.log(`Unmount normal [current: ${committedText}]`);
};});return null;}await act(async () => {ReactNoop.render(<Counter count={0} />, () =>Scheduler.log('Sync effect'),);await waitFor(['Mount layout [current: 0]', 'Sync effect']);expect(committedText).toEqual('0');ReactNoop.render(<Counter count={1} />, () =>Scheduler.log('Sync effect'),);await waitFor(['Mount normal [current: 0]','Unmount layout [current: 0]','Mount layout [current: 1]','Sync effect',]);expect(committedText).toEqual('1');});assertLog(['Unmount normal [current: 1]', 'Mount normal [current: 1]']);});it('catches errors thrown in useLayoutEffect', async () => {class ErrorBoundary extends React.Component {state = {error: null};static getDerivedStateFromError(error) {Scheduler.log(`ErrorBoundary static getDerivedStateFromError`);
return {error};}render() {const {children, id, fallbackID} = this.props;const {error} = this.state;if (error) {Scheduler.log(`${id} render error`);
return <Component id={fallbackID} />;}Scheduler.log(`${id} render success`);
return children || null;}}function Component({id}) {Scheduler.log('Component render ' + id);return <span prop={id} />;}function BrokenLayoutEffectDestroy() {useLayoutEffect(() => {return () => {Scheduler.log('BrokenLayoutEffectDestroy useLayoutEffect destroy');throw Error('Expected');};}, []);Scheduler.log('BrokenLayoutEffectDestroy render');return <span prop="broken" />;}ReactNoop.render(<ErrorBoundary id="OuterBoundary" fallbackID="OuterFallback"><Component id="sibling" /><ErrorBoundary id="InnerBoundary" fallbackID="InnerFallback"><BrokenLayoutEffectDestroy /></ErrorBoundary></ErrorBoundary>,);await waitForAll(['OuterBoundary render success','Component render sibling','InnerBoundary render success','BrokenLayoutEffectDestroy render',]);expect(ReactNoop).toMatchRenderedOutput(<><span prop="sibling" /><span prop="broken" /></>,);ReactNoop.render(<ErrorBoundary id="OuterBoundary" fallbackID="OuterFallback"><Component id="sibling" /></ErrorBoundary>,);// React should skip over the unmounting boundary and find the nearest still-mounted boundary.await waitForAll(['OuterBoundary render success','Component render sibling','BrokenLayoutEffectDestroy useLayoutEffect destroy','ErrorBoundary static getDerivedStateFromError','OuterBoundary render error','Component render OuterFallback',]);expect(ReactNoop).toMatchRenderedOutput(<span prop="OuterFallback" />);});it('assumes layout effect destroy function is either a function or undefined', async () => {function App(props) {useLayoutEffect(() => {return props.return;});return null;}const root1 = ReactNoop.createRoot();await expect(async () => {await act(() => {root1.render(<App return={17} />);});}).toErrorDev(['Warning: useLayoutEffect must not return anything besides a ' +'function, which is used for clean-up. You returned: 17',]);const root2 = ReactNoop.createRoot();await expect(async () => {await act(() => {root2.render(<App return={null} />);});}).toErrorDev(['Warning: useLayoutEffect must not return anything besides a ' +'function, which is used for clean-up. You returned null. If your ' +'effect does not require clean up, return undefined (or nothing).',]);const root3 = ReactNoop.createRoot();await expect(async () => {await act(() => {root3.render(<App return={Promise.resolve()} />);});}).toErrorDev(['Warning: useLayoutEffect must not return anything besides a ' +'function, which is used for clean-up.\n\n' +
'It looks like you wrote useLayoutEffect(async () => ...) or returned a Promise.',]);// Error on unmount because React assumes the value is a functionawait act(async () => {root3.render(null);await waitForThrow('is not a function');});});});describe('useCallback', () => {it('memoizes callback by comparing inputs', async () => {class IncrementButton extends React.PureComponent {increment = () => {this.props.increment();};render() {return <Text text="Increment" />;}}function Counter({incrementBy}) {const [count, updateCount] = useState(0);const increment = useCallback(() => updateCount(c => c + incrementBy),[incrementBy],);return (<><IncrementButton increment={increment} ref={button} /><Text text={'Count: ' + count} /></>);}const button = React.createRef(null);ReactNoop.render(<Counter incrementBy={1} />);await waitForAll(['Increment', 'Count: 0']);expect(ReactNoop).toMatchRenderedOutput(<><span prop="Increment" /><span prop="Count: 0" /></>,);await act(() => button.current.increment());assertLog([// Button should not re-render, because its props haven't changed// 'Increment','Count: 1',]);expect(ReactNoop).toMatchRenderedOutput(<><span prop="Increment" /><span prop="Count: 1" /></>,);// Increase the increment amountReactNoop.render(<Counter incrementBy={10} />);await waitForAll([// Inputs did change this time'Increment','Count: 1',]);expect(ReactNoop).toMatchRenderedOutput(<><span prop="Increment" /><span prop="Count: 1" /></>,);// Callback should have updatedawait act(() => button.current.increment());assertLog(['Count: 11']);expect(ReactNoop).toMatchRenderedOutput(<><span prop="Increment" /><span prop="Count: 11" /></>,);});});describe('useMemo', () => {it('memoizes value by comparing to previous inputs', async () => {function CapitalizedText(props) {const text = props.text;const capitalizedText = useMemo(() => {Scheduler.log(`Capitalize '${text}'`);
return text.toUpperCase();}, [text]);return <Text text={capitalizedText} />;}ReactNoop.render(<CapitalizedText text="hello" />);await waitForAll(["Capitalize 'hello'", 'HELLO']);expect(ReactNoop).toMatchRenderedOutput(<span prop="HELLO" />);ReactNoop.render(<CapitalizedText text="hi" />);await waitForAll(["Capitalize 'hi'", 'HI']);expect(ReactNoop).toMatchRenderedOutput(<span prop="HI" />);ReactNoop.render(<CapitalizedText text="hi" />);await waitForAll(['HI']);expect(ReactNoop).toMatchRenderedOutput(<span prop="HI" />);ReactNoop.render(<CapitalizedText text="goodbye" />);await waitForAll(["Capitalize 'goodbye'", 'GOODBYE']);expect(ReactNoop).toMatchRenderedOutput(<span prop="GOODBYE" />);});it('always re-computes if no inputs are provided', async () => {function LazyCompute(props) {const computed = useMemo(props.compute);return <Text text={computed} />;}function computeA() {Scheduler.log('compute A');return 'A';}function computeB() {Scheduler.log('compute B');return 'B';}ReactNoop.render(<LazyCompute compute={computeA} />);await waitForAll(['compute A', 'A']);ReactNoop.render(<LazyCompute compute={computeA} />);await waitForAll(['compute A', 'A']);ReactNoop.render(<LazyCompute compute={computeA} />);await waitForAll(['compute A', 'A']);ReactNoop.render(<LazyCompute compute={computeB} />);await waitForAll(['compute B', 'B']);});it('should not invoke memoized function during re-renders unless inputs change', async () => {function LazyCompute(props) {const computed = useMemo(() => props.compute(props.input),[props.input],);const [count, setCount] = useState(0);if (count < 3) {setCount(count + 1);}return <Text text={computed} />;}function compute(val) {Scheduler.log('compute ' + val);return val;}ReactNoop.render(<LazyCompute compute={compute} input="A" />);await waitForAll(['compute A', 'A']);ReactNoop.render(<LazyCompute compute={compute} input="A" />);await waitForAll(['A']);ReactNoop.render(<LazyCompute compute={compute} input="B" />);await waitForAll(['compute B', 'B']);});});describe('useImperativeHandle', () => {it('does not update when deps are the same', async () => {const INCREMENT = 'INCREMENT';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 <Text text={'Count: ' + count} />;}Counter = forwardRef(Counter);const counter = React.createRef(null);ReactNoop.render(<Counter ref={counter} />);await waitForAll(['Count: 0']);expect(ReactNoop).toMatchRenderedOutput(<span prop="Count: 0" />);expect(counter.current.count).toBe(0);await act(() => {counter.current.dispatch(INCREMENT);});assertLog(['Count: 1']);expect(ReactNoop).toMatchRenderedOutput(<span prop="Count: 1" />);// Intentionally not updated because of [] deps:expect(counter.current.count).toBe(0);});// Regression test for https://github.com/facebook/react/issues/14782it('automatically updates when deps are not specified', async () => {const INCREMENT = 'INCREMENT';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 <Text text={'Count: ' + count} />;}Counter = forwardRef(Counter);const counter = React.createRef(null);ReactNoop.render(<Counter ref={counter} />);await waitForAll(['Count: 0']);expect(ReactNoop).toMatchRenderedOutput(<span prop="Count: 0" />);expect(counter.current.count).toBe(0);await act(() => {counter.current.dispatch(INCREMENT);});assertLog(['Count: 1']);expect(ReactNoop).toMatchRenderedOutput(<span prop="Count: 1" />);expect(counter.current.count).toBe(1);});it('updates when deps are different', async () => {const INCREMENT = 'INCREMENT';function reducer(state, action) {return action === INCREMENT ? state + 1 : state;}let totalRefUpdates = 0;function Counter(props, ref) {const [count, dispatch] = useReducer(reducer, 0);useImperativeHandle(ref,() => {totalRefUpdates++;return {count, dispatch};},[count],);return <Text text={'Count: ' + count} />;}Counter = forwardRef(Counter);const counter = React.createRef(null);ReactNoop.render(<Counter ref={counter} />);await waitForAll(['Count: 0']);expect(ReactNoop).toMatchRenderedOutput(<span prop="Count: 0" />);expect(counter.current.count).toBe(0);expect(totalRefUpdates).toBe(1);await act(() => {counter.current.dispatch(INCREMENT);});assertLog(['Count: 1']);expect(ReactNoop).toMatchRenderedOutput(<span prop="Count: 1" />);expect(counter.current.count).toBe(1);expect(totalRefUpdates).toBe(2);// Update that doesn't change the ref dependenciesReactNoop.render(<Counter ref={counter} />);await waitForAll(['Count: 1']);expect(ReactNoop).toMatchRenderedOutput(<span prop="Count: 1" />);expect(counter.current.count).toBe(1);expect(totalRefUpdates).toBe(2); // Should not increase since last time});});describe('useTransition', () => {it('delays showing loading state until after timeout', async () => {let transition;function App() {const [show, setShow] = useState(false);const [isPending, startTransition] = useTransition();transition = () => {startTransition(() => {setShow(true);});};return (<Suspensefallback={<Text text={`Loading... Pending: ${isPending}`} />}>
{show ? (<AsyncText text={`After... Pending: ${isPending}`} />
) : (<Text text={`Before... Pending: ${isPending}`} />
)}</Suspense>);}ReactNoop.render(<App />);await waitForAll(['Before... Pending: false']);expect(ReactNoop).toMatchRenderedOutput(<span prop="Before... Pending: false" />,);await act(async () => {transition();await waitForAll(['Before... Pending: true','Suspend! [After... Pending: false]','Loading... Pending: false',]);expect(ReactNoop).toMatchRenderedOutput(<span prop="Before... Pending: true" />,);Scheduler.unstable_advanceTime(500);await advanceTimers(500);// Even after a long amount of time, we still don't show a placeholder.Scheduler.unstable_advanceTime(100000);await advanceTimers(100000);expect(ReactNoop).toMatchRenderedOutput(<span prop="Before... Pending: true" />,);await resolveText('After... Pending: false');assertLog(['Promise resolved [After... Pending: false]']);await waitForAll(['After... Pending: false']);expect(ReactNoop).toMatchRenderedOutput(<span prop="After... Pending: false" />,);});});});describe('useDeferredValue', () => {it('defers text value', async () => {function TextBox({text}) {return <AsyncText text={text} />;}let _setText;function App() {const [text, setText] = useState('A');const deferredText = useDeferredValue(text);_setText = setText;return (<><Text text={text} /><Suspense fallback={<Text text={'Loading'} />}><TextBox text={deferredText} /></Suspense></>);}await act(() => {ReactNoop.render(<App />);});assertLog(['A', 'Suspend! [A]', 'Loading']);expect(ReactNoop).toMatchRenderedOutput(<><span prop="A" /><span prop="Loading" /></>,);await act(() => resolveText('A'));assertLog(['Promise resolved [A]', 'A']);expect(ReactNoop).toMatchRenderedOutput(<><span prop="A" /><span prop="A" /></>,);await act(async () => {_setText('B');await waitForAll(['B', 'A', 'B', 'Suspend! [B]', 'Loading']);await waitForAll([]);expect(ReactNoop).toMatchRenderedOutput(<><span prop="B" /><span prop="A" /></>,);});await act(async () => {Scheduler.unstable_advanceTime(250);await advanceTimers(250);});assertLog([]);expect(ReactNoop).toMatchRenderedOutput(<><span prop="B" /><span prop="A" /></>,);// Even after a long amount of time, we don't show a fallbackScheduler.unstable_advanceTime(100000);await advanceTimers(100000);await waitForAll([]);expect(ReactNoop).toMatchRenderedOutput(<><span prop="B" /><span prop="A" /></>,);await act(async () => {await resolveText('B');});assertLog(['Promise resolved [B]', 'B', 'B']);expect(ReactNoop).toMatchRenderedOutput(<><span prop="B" /><span prop="B" /></>,);});});describe('progressive enhancement (not supported)', () => {it('mount additional state', async () => {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 <Text text={`A: ${A}, B: ${B}, C: ${C}`} />;
}ReactNoop.render(<App loadC={false} />);await waitForAll(['A: 0, B: 0, C: [not loaded]']);expect(ReactNoop).toMatchRenderedOutput(<span prop="A: 0, B: 0, C: [not loaded]" />,);await act(() => {updateA(2);updateB(3);});assertLog(['A: 2, B: 3, C: [not loaded]']);expect(ReactNoop).toMatchRenderedOutput(<span prop="A: 2, B: 3, C: [not loaded]" />,);ReactNoop.render(<App loadC={true} />);await expect(async () => {await waitForThrow('Rendered more hooks than during the previous render.',);assertLog([]);}).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://reactjs.org/link/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).toMatchRenderedOutput(<span prop="A: 2, B: 3, C: 0" />]);// updateC(4);// expect(Scheduler).toFlushAndYield(['A: 2, B: 3, C: 4']);// expect(ReactNoop).toMatchRenderedOutput(<span prop="A: 2, B: 3, C: 4" />]);});it('unmount state', async () => {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 <Text text={`A: ${A}, B: ${B}, C: ${C}`} />;
}ReactNoop.render(<App loadC={true} />);await waitForAll(['A: 0, B: 0, C: 0']);expect(ReactNoop).toMatchRenderedOutput(<span prop="A: 0, B: 0, C: 0" />);await act(() => {updateA(2);updateB(3);updateC(4);});assertLog(['A: 2, B: 3, C: 4']);expect(ReactNoop).toMatchRenderedOutput(<span prop="A: 2, B: 3, C: 4" />);ReactNoop.render(<App loadC={false} />);await waitForThrow('Rendered fewer hooks than expected. This may be caused by an ' +'accidental early return statement.',);});it('unmount effects', async () => {function App(props) {useEffect(() => {Scheduler.log('Mount A');return () => {Scheduler.log('Unmount A');};}, []);if (props.showMore) {useEffect(() => {Scheduler.log('Mount B');return () => {Scheduler.log('Unmount B');};}, []);}return null;}await act(async () => {ReactNoop.render(<App showMore={false} />, () =>Scheduler.log('Sync effect'),);await waitFor(['Sync effect']);});assertLog(['Mount A']);await act(async () => {ReactNoop.render(<App showMore={true} />);await expect(async () => {await waitForThrow('Rendered more hooks than during the previous render.',);assertLog([]);}).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://reactjs.org/link/rules-of-hooks\n\n' +
' Previous render Next render\n' +
' ------------------------------------------------------\n' +
'1. useEffect useEffect\n' +
'2. undefined useEffect\n' +
' ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n',
]);});// Uncomment if/when we support this again// ReactNoop.flushPassiveEffects();// expect(Scheduler).toHaveYielded(['Mount B']);// ReactNoop.render(<App showMore={false} />);// expect(Scheduler).toFlushAndThrow(// 'Rendered fewer hooks than expected. This may be caused by an ' +// 'accidental early return statement.',// );});});it('useReducer does not eagerly bail out of state updates', async () => {// Edge case based on a bug reportlet setCounter;function App() {const [counter, _setCounter] = useState(1);setCounter = _setCounter;return <Component count={counter} />;}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.log('Reducer: ' + count);return count;}, -1);useEffect(() => {Scheduler.log('Effect: ' + count);dispatch();}, [count]);Scheduler.log('Render: ' + state);return count;}await act(async () => {ReactNoop.render(<App />);await waitForAll(['Render: -1', 'Effect: 1', 'Reducer: 1', 'Render: 1']);expect(ReactNoop).toMatchRenderedOutput('1');});await act(() => {setCounter(2);});assertLog(['Render: 1', 'Effect: 2', 'Reducer: 2', 'Render: 2']);expect(ReactNoop).toMatchRenderedOutput('2');});it('useReducer does not replay previous no-op actions when other state changes', async () => {let increment;let setDisabled;function Counter() {const [disabled, _setDisabled] = useState(true);const [count, dispatch] = useReducer((state, action) => {if (disabled) {return state;}if (action.type === 'increment') {return state + 1;}return state;}, 0);increment = () => dispatch({type: 'increment'});setDisabled = _setDisabled;Scheduler.log('Render disabled: ' + disabled);Scheduler.log('Render count: ' + count);return count;}ReactNoop.render(<Counter />);await waitForAll(['Render disabled: true', 'Render count: 0']);expect(ReactNoop).toMatchRenderedOutput('0');await act(() => {// These increments should have no effect, since disabled=trueincrement();increment();increment();});assertLog(['Render disabled: true', 'Render count: 0']);expect(ReactNoop).toMatchRenderedOutput('0');await act(() => {// Enabling the updater should *not* replay the previous increment() actionssetDisabled(false);});assertLog(['Render disabled: false', 'Render count: 0']);expect(ReactNoop).toMatchRenderedOutput('0');});it('useReducer does not replay previous no-op actions when props change', async () => {let setDisabled;let increment;function Counter({disabled}) {const [count, dispatch] = useReducer((state, action) => {if (disabled) {return state;}if (action.type === 'increment') {return state + 1;}return state;}, 0);increment = () => dispatch({type: 'increment'});Scheduler.log('Render count: ' + count);return count;}function App() {const [disabled, _setDisabled] = useState(true);setDisabled = _setDisabled;Scheduler.log('Render disabled: ' + disabled);return <Counter disabled={disabled} />;}ReactNoop.render(<App />);await waitForAll(['Render disabled: true', 'Render count: 0']);expect(ReactNoop).toMatchRenderedOutput('0');await act(() => {// These increments should have no effect, since disabled=trueincrement();increment();increment();});assertLog(['Render count: 0']);expect(ReactNoop).toMatchRenderedOutput('0');await act(() => {// Enabling the updater should *not* replay the previous increment() actionssetDisabled(false);});assertLog(['Render disabled: false', 'Render count: 0']);expect(ReactNoop).toMatchRenderedOutput('0');});it('useReducer applies potential no-op changes if made relevant by other updates in the batch', async () => {let setDisabled;let increment;function Counter({disabled}) {const [count, dispatch] = useReducer((state, action) => {if (disabled) {return state;}if (action.type === 'increment') {return state + 1;}return state;}, 0);increment = () => dispatch({type: 'increment'});Scheduler.log('Render count: ' + count);return count;}function App() {const [disabled, _setDisabled] = useState(true);setDisabled = _setDisabled;Scheduler.log('Render disabled: ' + disabled);return <Counter disabled={disabled} />;}ReactNoop.render(<App />);await waitForAll(['Render disabled: true', 'Render count: 0']);expect(ReactNoop).toMatchRenderedOutput('0');await act(() => {// Although the increment happens first (and would seem to do nothing since disabled=true),// because these calls are in a batch the parent updates first. This should cause the child// to re-render with disabled=false and *then* process the increment action, which now// increments the count and causes the component output to change.increment();setDisabled(false);});assertLog(['Render disabled: false', 'Render count: 1']);expect(ReactNoop).toMatchRenderedOutput('1');});// 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.log('Render A: ' + counter);useEffect(() => {Scheduler.log('Commit A: ' + counter);});return counter;}function CounterB() {const [counter, setCounter] = useState(0);setCounterB = setCounter;Scheduler.log('Render B: ' + counter);useEffect(() => {Scheduler.log('Commit B: ' + counter);});return counter;}const root = ReactNoop.createRoot(null);await act(() => {root.render(<><CounterA /><CounterB /></>,);});assertLog(['Render A: 0', 'Render B: 0', 'Commit A: 0', 'Commit B: 0']);await act(() => {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);});assertLog(['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', async () => {// 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);}Scheduler.log(`Step: ${step}, Shadow: ${shadow}`);
return shadow;}ReactNoop.render(<App />);await waitForAll(['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');await act(() => dispatch());assertLog(['Step: 5, Shadow: 5']);expect(ReactNoop).toMatchRenderedOutput('5');});it('should process the rest pending updates after a render phase update', async () => {// 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'}`;
}await act(() => ReactNoop.render(<App />));expect(ReactNoop).toMatchRenderedOutput('abc');await act(() => {updateA(true);// This update should not get dropped.updateC(true);});expect(ReactNoop).toMatchRenderedOutput('ABC');});it("regression test: don't unmount effects on siblings of deleted nodes", async () => {const root = ReactNoop.createRoot();function Child({label}) {useLayoutEffect(() => {Scheduler.log('Mount layout ' + label);return () => {Scheduler.log('Unmount layout ' + label);};}, [label]);useEffect(() => {Scheduler.log('Mount passive ' + label);return () => {Scheduler.log('Unmount passive ' + label);};}, [label]);return label;}await act(() => {root.render(<><Child key="A" label="A" /><Child key="B" label="B" /></>,);});assertLog(['Mount layout A','Mount layout B','Mount passive A','Mount passive B',]);// Delete A. This should only unmount the effect on A. In the regression,// B's effect would also unmount.await act(() => {root.render(<><Child key="B" label="B" /></>,);});assertLog(['Unmount layout A', 'Unmount passive A']);// Now delete and unmount B.await act(() => {root.render(null);});assertLog(['Unmount layout B', 'Unmount passive B']);});it('regression: deleting a tree and unmounting its effects after a reorder', async () => {const root = ReactNoop.createRoot();function Child({label}) {useEffect(() => {Scheduler.log('Mount ' + label);return () => {Scheduler.log('Unmount ' + label);};}, [label]);return label;}await act(() => {root.render(<><Child key="A" label="A" /><Child key="B" label="B" /></>,);});assertLog(['Mount A', 'Mount B']);await act(() => {root.render(<><Child key="B" label="B" /><Child key="A" label="A" /></>,);});assertLog([]);await act(() => {root.render(null);});assertLog(['Unmount B',// In the regression, the reorder would cause Child A to "forget" that it// contains passive effects. Then when we deleted the tree, A's unmount// effect would not fire.'Unmount A',]);});// @gate enableSuspenseListit('regression: SuspenseList causes unmounts to be dropped on deletion', async () => {function Row({label}) {useEffect(() => {Scheduler.log('Mount ' + label);return () => {Scheduler.log('Unmount ' + label);};}, [label]);return (<Suspense fallback="Loading..."><AsyncText text={label} /></Suspense>);}function App() {return (<SuspenseList revealOrder="together"><Row label="A" /><Row label="B" /></SuspenseList>);}const root = ReactNoop.createRoot();await act(() => {root.render(<App />);});assertLog(['Suspend! [A]', 'Suspend! [B]', 'Mount A', 'Mount B']);await act(async () => {await resolveText('A');});assertLog(['Promise resolved [A]', 'A', 'Suspend! [B]']);await act(() => {root.render(null);});// In the regression, SuspenseList would cause the children to "forget" that// it contains passive effects. Then when we deleted the tree, these unmount// effects would not fire.assertLog(['Unmount A', 'Unmount B']);});it('effect dependencies are persisted after a render phase update', async () => {let handleClick;function Test() {const [count, setCount] = useState(0);useEffect(() => {Scheduler.log(`Effect: ${count}`);
}, [count]);if (count > 0) {setCount(0);}handleClick = () => setCount(2);return <Text text={`Render: ${count}`} />;
}await act(() => {ReactNoop.render(<Test />);});assertLog(['Render: 0', 'Effect: 0']);await act(() => {handleClick();});assertLog(['Render: 0']);await act(() => {handleClick();});assertLog(['Render: 0']);await act(() => {handleClick();});assertLog(['Render: 0']);});});