/*** 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 ReactFeatureFlags;
let ReactTestRenderer;
let Scheduler;
let ReactDOMServer;
let act;
let assertLog;
let waitForAll;
let waitForThrow;
describe('ReactHooks', () => {
beforeEach(() => {
jest.resetModules();
ReactFeatureFlags = require('shared/ReactFeatureFlags');
React = require('react');
ReactTestRenderer = require('react-test-renderer');
Scheduler = require('scheduler');
ReactDOMServer = require('react-dom/server');
act = require('internal-test-utils').act;
const InternalTestUtils = require('internal-test-utils');
assertLog = InternalTestUtils.assertLog;
waitForAll = InternalTestUtils.waitForAll;
waitForThrow = InternalTestUtils.waitForThrow;
});if (__DEV__) {
// useDebugValue is a DEV-only hook
it('useDebugValue throws when used in a class component', () => {
class Example extends React.Component {
render() {
React.useDebugValue('abc');
return null;
}}expect(() => {
ReactTestRenderer.create(<Example />);
}).toThrow(
'Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen' +
' for one of the following reasons:\n' +
'1. You might have mismatching versions of React and the renderer (such as React DOM)\n' +
'2. You might be breaking the Rules of Hooks\n' +
'3. You might have more than one copy of React in the same app\n' +
'See https://reactjs.org/link/invalid-hook-call for tips about how to debug and fix this problem.',
);});}it('bails out in the render phase if all of the state is the same', async () => {
const {useState, useLayoutEffect} = React;
function Child({text}) {
Scheduler.log('Child: ' + text);
return text;
}let setCounter1;
let setCounter2;
function Parent() {
const [counter1, _setCounter1] = useState(0);
setCounter1 = _setCounter1;
const [counter2, _setCounter2] = useState(0);
setCounter2 = _setCounter2;
const text = `${counter1}, ${counter2}`;
Scheduler.log(`Parent: ${text}`);
useLayoutEffect(() => {
Scheduler.log(`Effect: ${text}`);
});return <Child text={text} />;
}const root = ReactTestRenderer.create(null, {unstable_isConcurrent: true});
root.update(<Parent />);
await waitForAll(['Parent: 0, 0', 'Child: 0, 0', 'Effect: 0, 0']);
expect(root).toMatchRenderedOutput('0, 0');
// Normal update
await act(() => {
setCounter1(1);
setCounter2(1);
});assertLog(['Parent: 1, 1', 'Child: 1, 1', 'Effect: 1, 1']);
// Update that bails out.
await act(() => setCounter1(1));
assertLog(['Parent: 1, 1']);
// This time, one of the state updates but the other one doesn't. So we
// can't bail out.
await act(() => {
setCounter1(1);
setCounter2(2);
});assertLog(['Parent: 1, 2', 'Child: 1, 2', 'Effect: 1, 2']);
// Lots of updates that eventually resolve to the current values.
await act(() => {
setCounter1(9);
setCounter2(3);
setCounter1(4);
setCounter2(7);
setCounter1(1);
setCounter2(2);
});// Because the final values are the same as the current values, the
// component bails out.
assertLog(['Parent: 1, 2']);
// prepare to check SameValue
await act(() => {
setCounter1(0 / -1);
setCounter2(NaN);
});assertLog(['Parent: 0, NaN', 'Child: 0, NaN', 'Effect: 0, NaN']);
// check if re-setting to negative 0 / NaN still bails out
await act(() => {
setCounter1(0 / -1);
setCounter2(NaN);
setCounter2(Infinity);
setCounter2(NaN);
});assertLog(['Parent: 0, NaN']);
// check if changing negative 0 to positive 0 does not bail out
await act(() => {
setCounter1(0);
});assertLog(['Parent: 0, NaN', 'Child: 0, NaN', 'Effect: 0, NaN']);
});it('bails out in render phase if all the state is the same and props bail out with memo', async () => {
const {useState, memo} = React;
function Child({text}) {
Scheduler.log('Child: ' + text);
return text;
}let setCounter1;
let setCounter2;
function Parent({theme}) {
const [counter1, _setCounter1] = useState(0);
setCounter1 = _setCounter1;
const [counter2, _setCounter2] = useState(0);
setCounter2 = _setCounter2;
const text = `${counter1}, ${counter2} (${theme})`;
Scheduler.log(`Parent: ${text}`);
return <Child text={text} />;
}Parent = memo(Parent);
const root = ReactTestRenderer.create(null, {unstable_isConcurrent: true});
root.update(<Parent theme="light" />);
await waitForAll(['Parent: 0, 0 (light)', 'Child: 0, 0 (light)']);
expect(root).toMatchRenderedOutput('0, 0 (light)');
// Normal update
await act(() => {
setCounter1(1);
setCounter2(1);
});assertLog(['Parent: 1, 1 (light)', 'Child: 1, 1 (light)']);
// Update that bails out.
await act(() => setCounter1(1));
assertLog(['Parent: 1, 1 (light)']);
// This time, one of the state updates but the other one doesn't. So we
// can't bail out.
await act(() => {
setCounter1(1);
setCounter2(2);
});assertLog(['Parent: 1, 2 (light)', 'Child: 1, 2 (light)']);
// Updates bail out, but component still renders because props
// have changed
await act(() => {
setCounter1(1);
setCounter2(2);
root.update(<Parent theme="dark" />);
});assertLog(['Parent: 1, 2 (dark)', 'Child: 1, 2 (dark)']);
// Both props and state bail out
await act(() => {
setCounter1(1);
setCounter2(2);
root.update(<Parent theme="dark" />);
});assertLog(['Parent: 1, 2 (dark)']);
});it('warns about setState second argument', async () => {
const {useState} = React;
let setCounter;
function Counter() {
const [counter, _setCounter] = useState(0);
setCounter = _setCounter;
Scheduler.log(`Count: ${counter}`);
return counter;
}const root = ReactTestRenderer.create(null, {unstable_isConcurrent: true});
root.update(<Counter />);
await waitForAll(['Count: 0']);
expect(root).toMatchRenderedOutput('0');
await expect(async () => {
await act(() =>
setCounter(1, () => {
throw new Error('Expected to ignore the callback.');
}),);}).toErrorDev(
'State updates from the useState() and useReducer() Hooks ' +
"don't support the second callback argument. " +
'To execute a side effect after rendering, ' +
'declare it in the component body with useEffect().',
{withoutStack: true},
);assertLog(['Count: 1']);
expect(root).toMatchRenderedOutput('1');
});it('warns about dispatch second argument', async () => {
const {useReducer} = React;
let dispatch;
function Counter() {
const [counter, _dispatch] = useReducer((s, a) => a, 0);
dispatch = _dispatch;
Scheduler.log(`Count: ${counter}`);
return counter;
}const root = ReactTestRenderer.create(null, {unstable_isConcurrent: true});
root.update(<Counter />);
await waitForAll(['Count: 0']);
expect(root).toMatchRenderedOutput('0');
await expect(async () => {
await act(() =>
dispatch(1, () => {
throw new Error('Expected to ignore the callback.');
}),);}).toErrorDev(
'State updates from the useState() and useReducer() Hooks ' +
"don't support the second callback argument. " +
'To execute a side effect after rendering, ' +
'declare it in the component body with useEffect().',
{withoutStack: true},
);assertLog(['Count: 1']);
expect(root).toMatchRenderedOutput('1');
});it('never bails out if context has changed', async () => {
const {useState, useLayoutEffect, useContext} = React;
const ThemeContext = React.createContext('light');
let setTheme;
function ThemeProvider({children}) {
const [theme, _setTheme] = useState('light');
Scheduler.log('Theme: ' + theme);
setTheme = _setTheme;
return (
<ThemeContext.Provider value={theme}>{children}</ThemeContext.Provider>
);}function Child({text}) {Scheduler.log('Child: ' + text);
return text;}let setCounter;function Parent() {const [counter, _setCounter] = useState(0);
setCounter = _setCounter;const theme = useContext(ThemeContext);const text = `${counter} (${theme})`;
Scheduler.log(`Parent: ${text}`);
useLayoutEffect(() => {Scheduler.log(`Effect: ${text}`);
});return <Child text={text} />;
}const root = ReactTestRenderer.create(null, {unstable_isConcurrent: true});
await act(() => {
root.update(
<ThemeProvider>
<Parent />
</ThemeProvider>,
);});assertLog([
'Theme: light','Parent: 0 (light)','Child: 0 (light)','Effect: 0 (light)',]);
expect(root).toMatchRenderedOutput('0 (light)');
// Updating the theme to the same value doesn't cause the consumers
// to re-render.
setTheme('light');
await waitForAll([]);
expect(root).toMatchRenderedOutput('0 (light)');
// Normal update
await act(() => setCounter(1));
assertLog(['Parent: 1 (light)', 'Child: 1 (light)', 'Effect: 1 (light)']);
expect(root).toMatchRenderedOutput('1 (light)');
// Update that doesn't change state, so it bails out
await act(() => setCounter(1));
assertLog(['Parent: 1 (light)']);
expect(root).toMatchRenderedOutput('1 (light)');
// Update that doesn't change state, but the context changes, too, so it
// can't bail out
await act(() => {
setCounter(1);
setTheme('dark');
});assertLog([
'Theme: dark',
'Parent: 1 (dark)',
'Child: 1 (dark)',
'Effect: 1 (dark)',
]);expect(root).toMatchRenderedOutput('1 (dark)');
});it('can bail out without calling render phase (as an optimization) if queue is known to be empty', async () => {
const {useState, useLayoutEffect} = React;
function Child({text}) {
Scheduler.log('Child: ' + text);
return text;
}let setCounter;
function Parent() {
const [counter, _setCounter] = useState(0);
setCounter = _setCounter;
Scheduler.log('Parent: ' + counter);
useLayoutEffect(() => {
Scheduler.log('Effect: ' + counter);
});return <Child text={counter} />;
}const root = ReactTestRenderer.create(null, {unstable_isConcurrent: true});
root.update(<Parent />);
await waitForAll(['Parent: 0', 'Child: 0', 'Effect: 0']);
expect(root).toMatchRenderedOutput('0');
// Normal update
await act(() => setCounter(1));
assertLog(['Parent: 1', 'Child: 1', 'Effect: 1']);
expect(root).toMatchRenderedOutput('1');
// Update to the same state. React doesn't know if the queue is empty
// because the alternate fiber has pending update priority, so we have to
// enter the render phase before we can bail out. But we bail out before
// rendering the child, and we don't fire any effects.
await act(() => setCounter(1));
assertLog(['Parent: 1']);
expect(root).toMatchRenderedOutput('1');
// Update to the same state again. This times, neither fiber has pending
// update priority, so we can bail out before even entering the render phase.
await act(() => setCounter(1));
await waitForAll([]);
expect(root).toMatchRenderedOutput('1');
// This changes the state to something different so it renders normally.
await act(() => setCounter(2));
assertLog(['Parent: 2', 'Child: 2', 'Effect: 2']);
expect(root).toMatchRenderedOutput('2');
// prepare to check SameValue
await act(() => {
setCounter(0);
});assertLog(['Parent: 0', 'Child: 0', 'Effect: 0']);
expect(root).toMatchRenderedOutput('0');
// Update to the same state for the first time to flush the queue
await act(() => {
setCounter(0);
});assertLog(['Parent: 0']);
expect(root).toMatchRenderedOutput('0');
// Update again to the same state. Should bail out.
await act(() => {
setCounter(0);
});await waitForAll([]);
expect(root).toMatchRenderedOutput('0');
// Update to a different state (positive 0 to negative 0)
await act(() => {
setCounter(0 / -1);
});assertLog(['Parent: 0', 'Child: 0', 'Effect: 0']);
expect(root).toMatchRenderedOutput('0');
});it('bails out multiple times in a row without entering render phase', async () => {
const {useState} = React;
function Child({text}) {
Scheduler.log('Child: ' + text);
return text;
}let setCounter;
function Parent() {
const [counter, _setCounter] = useState(0);
setCounter = _setCounter;
Scheduler.log('Parent: ' + counter);
return <Child text={counter} />;
}const root = ReactTestRenderer.create(null, {unstable_isConcurrent: true});
root.update(<Parent />);
await waitForAll(['Parent: 0', 'Child: 0']);
expect(root).toMatchRenderedOutput('0');
const update = value => {
setCounter(previous => {
Scheduler.log(`Compute state (${previous} -> ${value})`);
return value;
});};ReactTestRenderer.unstable_batchedUpdates(() => {
update(0);
update(0);
update(0);
update(1);
update(2);
update(3);
});assertLog([
// The first four updates were eagerly computed, because the queue is
// empty before each one.
'Compute state (0 -> 0)',
'Compute state (0 -> 0)',
'Compute state (0 -> 0)',
// The fourth update doesn't bail out
'Compute state (0 -> 1)',
// so subsequent updates can't be eagerly computed.
]);// Now let's enter the render phase
await waitForAll([
// We don't need to re-compute the first four updates. Only the final two.
'Compute state (1 -> 2)',
'Compute state (2 -> 3)',
'Parent: 3',
'Child: 3',
]);expect(root).toMatchRenderedOutput('3');
});it('can rebase on top of a previously skipped update', async () => {
const {useState} = React;
function Child({text}) {
Scheduler.log('Child: ' + text);
return text;
}let setCounter;
function Parent() {
const [counter, _setCounter] = useState(1);
setCounter = _setCounter;
Scheduler.log('Parent: ' + counter);
return <Child text={counter} />;
}const root = ReactTestRenderer.create(null, {unstable_isConcurrent: true});
root.update(<Parent />);
await waitForAll(['Parent: 1', 'Child: 1']);
expect(root).toMatchRenderedOutput('1');
const update = compute => {
setCounter(previous => {
const value = compute(previous);
Scheduler.log(`Compute state (${previous} -> ${value})`);
return value;
});};if (gate(flags => flags.enableUnifiedSyncLane)) {
// Update at transition priority
React.startTransition(() => update(n => n * 100));
} else {
// Update at normal priority
ReactTestRenderer.unstable_batchedUpdates(() => update(n => n * 100));
}// The new state is eagerly computed.
assertLog(['Compute state (1 -> 100)']);
// but before it's flushed, a higher priority update interrupts it.
root.unstable_flushSync(() => {
update(n => n + 5);
});assertLog([
// The eagerly computed state was completely skipped
'Compute state (1 -> 6)',
'Parent: 6',
'Child: 6',
]);expect(root).toMatchRenderedOutput('6');
// Now when we finish the first update, the second update is rebased on top.
// Notice we didn't have to recompute the first update even though it was
// skipped in the previous render.
await waitForAll([
'Compute state (100 -> 105)',
'Parent: 105',
'Child: 105',
]);expect(root).toMatchRenderedOutput('105');
});it('warns about variable number of dependencies', () => {
const {useLayoutEffect} = React;
function App(props) {
useLayoutEffect(() => {
Scheduler.log('Did commit: ' + props.dependencies.join(', '));
}, props.dependencies);
return props.dependencies;
}const root = ReactTestRenderer.create(<App dependencies={['A']} />);
assertLog(['Did commit: A']);
expect(() => {
root.update(<App dependencies={['A', 'B']} />);
}).toErrorDev([
'Warning: The final argument passed to useLayoutEffect changed size ' +
'between renders. The order and size of this array must remain ' +
'constant.\n\n' +
'Previous: [A]\n' +
'Incoming: [A, B]\n',
]);});it('warns if switching from dependencies to no dependencies', () => {
const {useMemo} = React;
function App({text, hasDeps}) {
const resolvedText = useMemo(
() => {
Scheduler.log('Compute');
return text.toUpperCase();
},hasDeps ? null : [text],
);return resolvedText;
}const root = ReactTestRenderer.create(null);
root.update(<App text="Hello" hasDeps={true} />);
assertLog(['Compute']);
expect(root).toMatchRenderedOutput('HELLO');
expect(() => {
root.update(<App text="Hello" hasDeps={false} />);
}).toErrorDev([
'Warning: useMemo received a final argument during this render, but ' +
'not during the previous render. Even though the final argument is ' +
'optional, its type cannot change between renders.',
]);});it('warns if deps is not an array', async () => {
const {useEffect, useLayoutEffect, useMemo, useCallback} = React;
function App(props) {
useEffect(() => {}, props.deps);
useLayoutEffect(() => {}, props.deps);
useMemo(() => {}, props.deps);
useCallback(() => {}, props.deps);
return null;
}await expect(async () => {
await act(() => {
ReactTestRenderer.create(<App deps={'hello'} />);
});}).toErrorDev([
'Warning: useEffect received a final argument that is not an array (instead, received `string`). ' +
'When specified, the final argument must be an array.',
'Warning: useLayoutEffect received a final argument that is not an array (instead, received `string`). ' +
'When specified, the final argument must be an array.',
'Warning: useMemo received a final argument that is not an array (instead, received `string`). ' +
'When specified, the final argument must be an array.',
'Warning: useCallback received a final argument that is not an array (instead, received `string`). ' +
'When specified, the final argument must be an array.',
]);await expect(async () => {
await act(() => {
ReactTestRenderer.create(<App deps={100500} />);
});}).toErrorDev([
'Warning: useEffect received a final argument that is not an array (instead, received `number`). ' +
'When specified, the final argument must be an array.',
'Warning: useLayoutEffect received a final argument that is not an array (instead, received `number`). ' +
'When specified, the final argument must be an array.',
'Warning: useMemo received a final argument that is not an array (instead, received `number`). ' +
'When specified, the final argument must be an array.',
'Warning: useCallback received a final argument that is not an array (instead, received `number`). ' +
'When specified, the final argument must be an array.',
]);await expect(async () => {
await act(() => {
ReactTestRenderer.create(<App deps={{}} />);
});}).toErrorDev([
'Warning: useEffect received a final argument that is not an array (instead, received `object`). ' +
'When specified, the final argument must be an array.',
'Warning: useLayoutEffect received a final argument that is not an array (instead, received `object`). ' +
'When specified, the final argument must be an array.',
'Warning: useMemo received a final argument that is not an array (instead, received `object`). ' +
'When specified, the final argument must be an array.',
'Warning: useCallback received a final argument that is not an array (instead, received `object`). ' +
'When specified, the final argument must be an array.',
]);await act(() => {
ReactTestRenderer.create(<App deps={[]} />);
ReactTestRenderer.create(<App deps={null} />);
ReactTestRenderer.create(<App deps={undefined} />);
});});it('warns if deps is not an array for useImperativeHandle', () => {
const {useImperativeHandle} = React;
const App = React.forwardRef((props, ref) => {
useImperativeHandle(ref, () => {}, props.deps);
return null;
});expect(() => {
ReactTestRenderer.create(<App deps={'hello'} />);
}).toErrorDev([
'Warning: useImperativeHandle received a final argument that is not an array (instead, received `string`). ' +
'When specified, the final argument must be an array.',
]);ReactTestRenderer.create(<App deps={[]} />);
ReactTestRenderer.create(<App deps={null} />);
ReactTestRenderer.create(<App deps={undefined} />);
});it('does not forget render phase useState updates inside an effect', async () => {
const {useState, useEffect} = React;
function Counter() {
const [counter, setCounter] = useState(0);
if (counter === 0) {
setCounter(x => x + 1);
setCounter(x => x + 1);
}useEffect(() => {
setCounter(x => x + 1);
setCounter(x => x + 1);
}, []);return counter;
}const root = ReactTestRenderer.create(null);
await act(() => {
root.update(<Counter />);
});expect(root).toMatchRenderedOutput('4');
});it('does not forget render phase useReducer updates inside an effect with hoisted reducer', async () => {
const {useReducer, useEffect} = React;
const reducer = x => x + 1;
function Counter() {
const [counter, increment] = useReducer(reducer, 0);
if (counter === 0) {
increment();
increment();
}useEffect(() => {
increment();
increment();
}, []);return counter;
}const root = ReactTestRenderer.create(null);
await act(() => {
root.update(<Counter />);
});expect(root).toMatchRenderedOutput('4');
});it('does not forget render phase useReducer updates inside an effect with inline reducer', async () => {
const {useReducer, useEffect} = React;
function Counter() {
const [counter, increment] = useReducer(x => x + 1, 0);
if (counter === 0) {
increment();
increment();
}useEffect(() => {
increment();
increment();
}, []);return counter;
}const root = ReactTestRenderer.create(null);
await act(() => {
root.update(<Counter />);
});expect(root).toMatchRenderedOutput('4');
});it('warns for bad useImperativeHandle first arg', () => {
const {useImperativeHandle} = React;
function App() {
useImperativeHandle({
focus() {},
});return null;
}expect(() => {
expect(() => {
ReactTestRenderer.create(<App />);
}).toThrow('create is not a function');
}).toErrorDev([
'Expected useImperativeHandle() first argument to either be a ' +
'ref callback or React.createRef() object. ' +
'Instead received: an object with keys {focus}.',
'Expected useImperativeHandle() second argument to be a function ' +
'that creates a handle. Instead received: undefined.',
]);});it('warns for bad useImperativeHandle second arg', () => {
const {useImperativeHandle} = React;
const App = React.forwardRef((props, ref) => {
useImperativeHandle(ref, {
focus() {},
});return null;
});expect(() => {
ReactTestRenderer.create(<App />);
}).toErrorDev([
'Expected useImperativeHandle() second argument to be a function ' +
'that creates a handle. Instead received: object.',
]);});// https://github.com/facebook/react/issues/14022
it('works with ReactDOMServer calls inside a component', () => {
const {useState} = React;
function App(props) {
const markup1 = ReactDOMServer.renderToString(<p>hello</p>);
const markup2 = ReactDOMServer.renderToStaticMarkup(<p>bye</p>);
const [counter] = useState(0);
return markup1 + counter + markup2;
}const root = ReactTestRenderer.create(<App />);
expect(root.toJSON()).toMatchSnapshot();
});it("throws when calling hooks inside .memo's compare function", () => {
const {useState} = React;function App() {useState(0);return null;}const MemoApp = React.memo(App, () => {
useState(0);return false;});const root = ReactTestRenderer.create(<MemoApp />);
// trying to render again should trigger comparison and throwexpect(() => root.update(<MemoApp />)).toThrow(
'Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for' +
' one of the following reasons:\n' +
'1. You might have mismatching versions of React and the renderer (such as React DOM)\n' +
'2. You might be breaking the Rules of Hooks\n' +
'3. You might have more than one copy of React in the same app\n' +
'See https://reactjs.org/link/invalid-hook-call for tips about how to debug and fix this problem.',
);// the next round, it does a fresh mount, so should renderexpect(() => root.update(<MemoApp />)).not.toThrow(
'Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for' +
' one of the following reasons:\n' +
'1. You might have mismatching versions of React and the renderer (such as React DOM)\n' +
'2. You might be breaking the Rules of Hooks\n' +
'3. You might have more than one copy of React in the same app\n' +
'See https://reactjs.org/link/invalid-hook-call for tips about how to debug and fix this problem.',
);// and then again, failexpect(() => root.update(<MemoApp />)).toThrow(
'Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for' +
' one of the following reasons:\n' +
'1. You might have mismatching versions of React and the renderer (such as React DOM)\n' +
'2. You might be breaking the Rules of Hooks\n' +
'3. You might have more than one copy of React in the same app\n' +
'See https://reactjs.org/link/invalid-hook-call for tips about how to debug and fix this problem.',
);});it('warns when calling hooks inside useMemo', () => {const {useMemo, useState} = React;function App() {useMemo(() => {useState(0);});return null;}expect(() => ReactTestRenderer.create(<App />)).toErrorDev(
'Do not call Hooks inside useEffect(...), useMemo(...), or other built-in Hooks.',
);});it('warns when reading context inside useMemo', () => {const {useMemo, createContext} = React;const ReactCurrentDispatcher =React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED
.ReactCurrentDispatcher;
const ThemeContext = createContext('light');function App() {return useMemo(() => {return ReactCurrentDispatcher.current.readContext(ThemeContext);
}, []);
}expect(() => ReactTestRenderer.create(<App />)).toErrorDev(
'Context can only be read while React is rendering',);});it('warns when reading context inside useMemo after reading outside it', () => {const {useMemo, createContext} = React;const ReactCurrentDispatcher =React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED
.ReactCurrentDispatcher;
const ThemeContext = createContext('light');let firstRead, secondRead;function App() {firstRead = ReactCurrentDispatcher.current.readContext(ThemeContext);
useMemo(() => {});secondRead = ReactCurrentDispatcher.current.readContext(ThemeContext);
return useMemo(() => {return ReactCurrentDispatcher.current.readContext(ThemeContext);
}, []);
}expect(() => ReactTestRenderer.create(<App />)).toErrorDev(
'Context can only be read while React is rendering',);expect(firstRead).toBe('light');
expect(secondRead).toBe('light');
});// Throws because there's no runtime cost for being strict here.
it('throws when reading context inside useEffect', async () => {
const {useEffect, createContext} = React;
const ReactCurrentDispatcher =
React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED
.ReactCurrentDispatcher;const ThemeContext = createContext('light');
function App() {
useEffect(() => {
ReactCurrentDispatcher.current.readContext(ThemeContext);
});return null;
}await act(async () => {
ReactTestRenderer.create(<App />);
// The exact message doesn't matter, just make sure we don't allow this
await waitForThrow('Context can only be read while React is rendering');
});});// Throws because there's no runtime cost for being strict here.
it('throws when reading context inside useLayoutEffect', () => {
const {useLayoutEffect, createContext} = React;
const ReactCurrentDispatcher =
React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED
.ReactCurrentDispatcher;const ThemeContext = createContext('light');
function App() {
useLayoutEffect(() => {
ReactCurrentDispatcher.current.readContext(ThemeContext);
});return null;
}expect(() => ReactTestRenderer.create(<App />)).toThrow(
// The exact message doesn't matter, just make sure we don't allow this
'Context can only be read while React is rendering',
);});it('warns when reading context inside useReducer', () => {
const {useReducer, createContext} = React;
const ReactCurrentDispatcher =
React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED
.ReactCurrentDispatcher;const ThemeContext = createContext('light');
function App() {
const [state, dispatch] = useReducer((s, action) => {
ReactCurrentDispatcher.current.readContext(ThemeContext);
return action;
}, 0);
if (state === 0) {
dispatch(1);
}return null;
}expect(() => ReactTestRenderer.create(<App />)).toErrorDev([
'Context can only be read while React is rendering',
]);});// Edge case.
it('warns when reading context inside eager useReducer', () => {
const {useState, createContext} = React;
const ThemeContext = createContext('light');
const ReactCurrentDispatcher =
React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED
.ReactCurrentDispatcher;let _setState;
function Fn() {
const [, setState] = useState(0);
_setState = setState;
return null;
}class Cls extends React.Component {
render() {
_setState(() =>
ReactCurrentDispatcher.current.readContext(ThemeContext),
);return null;
}}expect(() =>
ReactTestRenderer.create(
<><Fn />
<Cls />
</>,
),).toErrorDev([
'Context can only be read while React is rendering','Cannot update a component (`Fn`) while rendering a different component (`Cls`).',]);
});it('warns when calling hooks inside useReducer', () => {const {useReducer, useState, useRef} = React;function App() {const [value, dispatch] = useReducer((state, action) => {
useRef(0);return state + 1;
}, 0);if (value === 0) {dispatch('foo');}useState();return value;}expect(() => {expect(() => {ReactTestRenderer.create(<App />);
}).toThrow(
'Update hook called on initial render. This is likely a bug in React. Please file an issue.',
);}).toErrorDev([
'Do not call Hooks inside useEffect(...), useMemo(...), or other built-in Hooks','Do not call Hooks inside useEffect(...), useMemo(...), or other built-in Hooks','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. useReducer useReducer\n' +
'2. useState useRef\n' +
' ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n',
]);
});it("warns when calling hooks inside useState's initialize function", () => {const {useState, useRef} = React;function App() {useState(() => {useRef(0);return 0;});return null;}expect(() => ReactTestRenderer.create(<App />)).toErrorDev(
'Do not call Hooks inside useEffect(...), useMemo(...), or other built-in Hooks.',
);});it('resets warning internal state when interrupted by an error', async () => {const ReactCurrentDispatcher =React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED
.ReactCurrentDispatcher;
const ThemeContext = React.createContext('light');
function App() {React.useMemo(() => {
// Trigger warningsReactCurrentDispatcher.current.readContext(ThemeContext);
React.useRef();
// Interrupt exit from a Hookthrow new Error('No.');
}, []);
}class Boundary extends React.Component {
state = {};static getDerivedStateFromError(error) {return {err: true};}render() {if (this.state.err) {
return 'Oops';}return this.props.children;
}}expect(() => {ReactTestRenderer.create(
<Boundary><App /></Boundary>,);}).toErrorDev([
// We see it twice due to replay'Context can only be read while React is rendering','Do not call Hooks inside useEffect(...), useMemo(...), or other built-in Hooks','Context can only be read while React is rendering','Do not call Hooks inside useEffect(...), useMemo(...), or other built-in Hooks',]);
function Valid() {React.useState();
React.useMemo(() => {});
React.useReducer(() => {});
React.useEffect(() => {});
React.useLayoutEffect(() => {});
React.useCallback(() => {});
React.useRef();
React.useImperativeHandle(
() => {},() => {},);if (__DEV__) {React.useDebugValue();
}return null;}// Verify it doesn't think we're still inside a Hook.
// Should have no warnings.
await act(() => {ReactTestRenderer.create(<Valid />);
});// Verify warnings don't get permanently disabled.
expect(() => {ReactTestRenderer.create(
<Boundary><App /></Boundary>,);}).toErrorDev([
// We see it twice due to replay'Context can only be read while React is rendering','Do not call Hooks inside useEffect(...), useMemo(...), or other built-in Hooks','Context can only be read while React is rendering','Do not call Hooks inside useEffect(...), useMemo(...), or other built-in Hooks',]);
});it('warns when reading context inside useMemo', () => {const {useMemo, createContext} = React;const ReactCurrentDispatcher =React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED
.ReactCurrentDispatcher;
const ThemeContext = createContext('light');function App() {return useMemo(() => {return ReactCurrentDispatcher.current.readContext(ThemeContext);
}, []);
}expect(() => ReactTestRenderer.create(<App />)).toErrorDev(
'Context can only be read while React is rendering',);});it('double-invokes components with Hooks in Strict Mode', () => {ReactFeatureFlags.debugRenderPhaseSideEffectsForStrictMode = true;
const {useState, StrictMode} = React;let renderCount = 0;function NoHooks() {renderCount++;
return <div />;}function HasHooks() {useState(0);renderCount++;
return <div />;}const FwdRef = React.forwardRef((props, ref) => {
renderCount++;
return <div />;});const FwdRefHasHooks = React.forwardRef((props, ref) => {
useState(0);renderCount++;
return <div />;});const Memo = React.memo(props => {
renderCount++;
return <div />;});const MemoHasHooks = React.memo(props => {
useState(0);renderCount++;
return <div />;});function Factory() {return {state: {},render() {renderCount++;
return <div />;},};}const renderer = ReactTestRenderer.create(null);
renderCount = 0;renderer.update(<NoHooks />);
expect(renderCount).toBe(1);
renderCount = 0;renderer.update(<NoHooks />);
expect(renderCount).toBe(1);
renderCount = 0;renderer.update(
<StrictMode><NoHooks /></StrictMode>,);expect(renderCount).toBe(__DEV__ ? 2 : 1);
renderCount = 0;renderer.update(
<StrictMode><NoHooks /></StrictMode>,);expect(renderCount).toBe(__DEV__ ? 2 : 1);
renderCount = 0;renderer.update(<FwdRef />);
expect(renderCount).toBe(1);
renderCount = 0;renderer.update(<FwdRef />);
expect(renderCount).toBe(1);
renderCount = 0;renderer.update(
<StrictMode><FwdRef /></StrictMode>,);expect(renderCount).toBe(__DEV__ ? 2 : 1);
renderCount = 0;renderer.update(
<StrictMode><FwdRef /></StrictMode>,);expect(renderCount).toBe(__DEV__ ? 2 : 1);
renderCount = 0;renderer.update(<Memo arg={1} />);
expect(renderCount).toBe(1);
renderCount = 0;renderer.update(<Memo arg={2} />);
expect(renderCount).toBe(1);
renderCount = 0;renderer.update(
<StrictMode><Memo arg={1} />
</StrictMode>,);expect(renderCount).toBe(__DEV__ ? 2 : 1);
renderCount = 0;renderer.update(
<StrictMode><Memo arg={2} />
</StrictMode>,);expect(renderCount).toBe(__DEV__ ? 2 : 1);
if (!require('shared/ReactFeatureFlags').disableModulePatternComponents) {
renderCount = 0;expect(() => renderer.update(<Factory />)).toErrorDev(
'Warning: The <Factory /> component appears to be a function component that returns a class instance. ' +
'Change Factory 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. " +
'`Factory.prototype = React.Component.prototype`. ' +
"Don't use an arrow function since it cannot be called with `new` by React.",
);expect(renderCount).toBe(1);
renderCount = 0;renderer.update(<Factory />);
expect(renderCount).toBe(1);
renderCount = 0;renderer.update(
<StrictMode><Factory /></StrictMode>,);expect(renderCount).toBe(__DEV__ ? 2 : 1); // Treated like a class
renderCount = 0;renderer.update(
<StrictMode><Factory /></StrictMode>,);expect(renderCount).toBe(__DEV__ ? 2 : 1); // Treated like a class
}renderCount = 0;renderer.update(<HasHooks />);
expect(renderCount).toBe(1);
renderCount = 0;renderer.update(<HasHooks />);
expect(renderCount).toBe(1);
renderCount = 0;renderer.update(
<StrictMode><HasHooks /></StrictMode>,);expect(renderCount).toBe(__DEV__ ? 2 : 1); // Has Hooks
renderCount = 0;renderer.update(
<StrictMode><HasHooks /></StrictMode>,);expect(renderCount).toBe(__DEV__ ? 2 : 1); // Has Hooks
renderCount = 0;renderer.update(<FwdRefHasHooks />);
expect(renderCount).toBe(1);
renderCount = 0;renderer.update(<FwdRefHasHooks />);
expect(renderCount).toBe(1);
renderCount = 0;renderer.update(
<StrictMode><FwdRefHasHooks /></StrictMode>,);expect(renderCount).toBe(__DEV__ ? 2 : 1); // Has Hooks
renderCount = 0;renderer.update(
<StrictMode><FwdRefHasHooks /></StrictMode>,);expect(renderCount).toBe(__DEV__ ? 2 : 1); // Has Hooks
renderCount = 0;renderer.update(<MemoHasHooks arg={1} />);
expect(renderCount).toBe(1);
renderCount = 0;renderer.update(<MemoHasHooks arg={2} />);
expect(renderCount).toBe(1);
renderCount = 0;renderer.update(
<StrictMode><MemoHasHooks arg={1} />
</StrictMode>,);expect(renderCount).toBe(__DEV__ ? 2 : 1); // Has Hooks
renderCount = 0;renderer.update(
<StrictMode><MemoHasHooks arg={2} />
</StrictMode>,);expect(renderCount).toBe(__DEV__ ? 2 : 1); // Has Hooks
});it('double-invokes useMemo in DEV StrictMode despite []', () => {
ReactFeatureFlags.debugRenderPhaseSideEffectsForStrictMode = true;
const {useMemo, StrictMode} = React;let useMemoCount = 0;function BadUseMemo() {useMemo(() => {useMemoCount++;
}, []);
return <div />;}useMemoCount = 0;ReactTestRenderer.create(
<StrictMode><BadUseMemo /></StrictMode>,);expect(useMemoCount).toBe(__DEV__ ? 2 : 1); // Has Hooks
});describe('hook ordering', () => {const useCallbackHelper = () => React.useCallback(() => {}, []);
const useContextHelper = () => React.useContext(React.createContext());
const useDebugValueHelper = () => React.useDebugValue('abc');
const useEffectHelper = () => React.useEffect(() => () => {}, []);
const useImperativeHandleHelper = () => {React.useImperativeHandle({current: null}, () => ({}), []);
};const useLayoutEffectHelper = () =>React.useLayoutEffect(() => () => {}, []);
const useMemoHelper = () => React.useMemo(() => 123, []);
const useReducerHelper = () => React.useReducer((s, a) => a, 0);
const useRefHelper = () => React.useRef(null);
const useStateHelper = () => React.useState(0);
// We don't include useImperativeHandleHelper in this set,// because it generates an additional warning about the inputs length changing.
// We test it below with its own test.
const orderedHooks = [
useCallbackHelper,useContextHelper,useDebugValueHelper,useEffectHelper,useLayoutEffectHelper,useMemoHelper,useReducerHelper,useRefHelper,useStateHelper,];
// We don't include useContext or useDebugValue in this set,// because they aren't added to the hooks list and so won't throw.
const hooksInList = [
useCallbackHelper,useEffectHelper,useImperativeHandleHelper,useLayoutEffectHelper,useMemoHelper,useReducerHelper,useRefHelper,useStateHelper,];
if (__EXPERIMENTAL__) {const useTransitionHelper = () => React.useTransition();
const useDeferredValueHelper = () =>React.useDeferredValue(0, {timeoutMs: 1000});
orderedHooks.push(useTransitionHelper);
orderedHooks.push(useDeferredValueHelper);
hooksInList.push(useTransitionHelper);
hooksInList.push(useDeferredValueHelper);
}const formatHookNamesToMatchErrorMessage = (hookNameA, hookNameB) => {return `use${hookNameA}${' '.repeat(24 - hookNameA.length)}${
hookNameB ? `use${hookNameB}` : undefined
}`;};orderedHooks.forEach((firstHelper, index) => {
const secondHelper =index > 0? orderedHooks[index - 1]
: orderedHooks[orderedHooks.length - 1];
const hookNameA = firstHelper.name
.replace('use', '')
.replace('Helper', '');
const hookNameB = secondHelper.name
.replace('use', '')
.replace('Helper', '');
it(`warns on using differently ordered hooks (${hookNameA}, ${hookNameB}) on subsequent renders`, async () => {
function App(props) {/* eslint-disable no-unused-vars */
if (props.update) {
secondHelper();firstHelper();} else {firstHelper();secondHelper();}// This should not appear in the warning message because it occurs after the first mismatchuseRefHelper();return null;/* eslint-enable no-unused-vars */
}let root;await act(() => {root = ReactTestRenderer.create(<App update={false} />);
});await expect(async () => {try {await act(() => {root.update(<App update={true} />);
});} catch (error) {// Swapping certain types of hooks will cause runtime errors.
// This is okay as far as this test is concerned.
// We just want to verify that warnings are always logged.
}}).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. ${formatHookNamesToMatchErrorMessage(hookNameA, hookNameB)}\n` +
' ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n' +
' in App (at **)',]);
// further warnings for this component are silencedtry {await act(() => {root.update(<App update={false} />);
});} catch (error) {// Swapping certain types of hooks will cause runtime errors.
// This is okay as far as this test is concerned.
// We just want to verify that warnings are always logged.
}});it(`warns when more hooks (${hookNameA}, ${hookNameB}) are used during update than mount`, async () => {
function App(props) {/* eslint-disable no-unused-vars */
if (props.update) {
firstHelper();secondHelper();} else {firstHelper();}return null;/* eslint-enable no-unused-vars */
}let root;await act(() => {root = ReactTestRenderer.create(<App update={false} />);
});await expect(async () => {try {await act(() => {root.update(<App update={true} />);
});} catch (error) {// Swapping certain types of hooks will cause runtime errors.
// This is okay as far as this test is concerned.
// We just want to verify that warnings are always logged.
}}).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. ${formatHookNamesToMatchErrorMessage(hookNameA, hookNameA)}\n` +
`2. undefined use${hookNameB}\n` +
' ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n' +
' in App (at **)',]);
});});hooksInList.forEach((firstHelper, index) => {
const secondHelper =index > 0? hooksInList[index - 1]
: hooksInList[hooksInList.length - 1];
const hookNameA = firstHelper.name
.replace('use', '')
.replace('Helper', '');
const hookNameB = secondHelper.name
.replace('use', '')
.replace('Helper', '');
it(`warns when fewer hooks (${hookNameA}, ${hookNameB}) are used during update than mount`, async () => {
function App(props) {/* eslint-disable no-unused-vars */
if (props.update) {
firstHelper();} else {firstHelper();secondHelper();}return null;/* eslint-enable no-unused-vars */
}let root;await act(() => {root = ReactTestRenderer.create(<App update={false} />);
});await act(() => {expect(() => {root.update(<App update={true} />);
}).toThrow('Rendered fewer hooks than expected. ');
});});});it('warns on using differently ordered hooks ' +
'(useImperativeHandleHelper, useMemoHelper) on subsequent renders',() => {function App(props) {/* eslint-disable no-unused-vars */
if (props.update) {
useMemoHelper();useImperativeHandleHelper();} else {useImperativeHandleHelper();useMemoHelper();}// This should not appear in the warning message because it occurs after the first mismatchuseRefHelper();return null;/* eslint-enable no-unused-vars */
}const root = ReactTestRenderer.create(<App update={false} />);
expect(() => {try {root.update(<App update={true} />);
} catch (error) {// Swapping certain types of hooks will cause runtime errors.
// This is okay as far as this test is concerned.
// We just want to verify that warnings are always logged.
}}).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. ${formatHookNamesToMatchErrorMessage('ImperativeHandle','Memo',)}\n` +
' ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n' +
' in App (at **)',]);
// further warnings for this component are silencedroot.update(<App update={false} />);
},);it('detects a bad hook order even if the component throws', () => {const {useState, useReducer} = React;function useCustomHook() {useState(0);}function App(props) {/* eslint-disable no-unused-vars */
if (props.update) {
useCustomHook();useReducer((s, a) => a, 0);throw new Error('custom error');} else {useReducer((s, a) => a, 0);useCustomHook();}return null;/* eslint-enable no-unused-vars */
}const root = ReactTestRenderer.create(<App update={false} />);
expect(() => {expect(() => root.update(<App update={true} />)).toThrow(
'custom error',);}).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. useReducer useState\n' +
' ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n',
]);
});});// Regression test for #14674
it('does not swallow original error when updating another component in render phase', async () => {
const {useState} = React;
spyOnDev(console, 'error').mockImplementation(() => {});
let _setState;
function A() {
const [, setState] = useState(0);
_setState = setState;
return null;
}function B() {
_setState(() => {
throw new Error('Hello');
});return null;
}expect(() => {
ReactTestRenderer.create(
<><A />
<B />
</>,
);}).toThrow('Hello');
if (__DEV__) {expect(console.error).toHaveBeenCalledTimes(2);
expect(console.error.mock.calls[0][0]).toContain(
'Warning: Cannot update a component (`%s`) while rendering ' +
'a different component (`%s`).',
);}});// Regression test for https://github.com/facebook/react/issues/15057
it('does not fire a false positive warning when previous effect unmounts the component', async () => {
const {useState, useEffect} = React;
let globalListener;
function A() {
const [show, setShow] = useState(true);
function hideMe() {
setShow(false);
}return show ? <B hideMe={hideMe} /> : null;
}function B(props) {
return <C {...props} />;
}function C({hideMe}) {const [, setState] = useState();
useEffect(() => {let isStale = false;globalListener = () => {if (!isStale) {setState('hello');}};return () => {isStale = true;hideMe();};});return null;}await act(() => {ReactTestRenderer.create(<A />);
});expect(() => {globalListener();globalListener();}).toErrorDev([
'An update to C inside a test was not wrapped in act','An update to C inside a test was not wrapped in act',// Note: should *not* warn about updates on unmounted component.// Because there's no way for component to know it got unmounted.]);
});// Regression test for https://github.com/facebook/react/issues/14790
it('does not fire a false positive warning when suspending memo', async () => {
const {Suspense, useState} = React;
let wasSuspended = false;
function trySuspend() {
if (!wasSuspended) {
throw new Promise(resolve => {
wasSuspended = true;
resolve();
});}}function Child() {
useState();
trySuspend();
return 'hello';
}const Wrapper = React.memo(Child);
const root = ReactTestRenderer.create(
<Suspense fallback="loading">
<Wrapper />
</Suspense>,
);expect(root).toMatchRenderedOutput('loading');
await Promise.resolve();
await waitForAll([]);
expect(root).toMatchRenderedOutput('hello');
});// Regression test for https://github.com/facebook/react/issues/14790
it('does not fire a false positive warning when suspending forwardRef', async () => {
const {Suspense, useState} = React;
let wasSuspended = false;
function trySuspend() {
if (!wasSuspended) {
throw new Promise(resolve => {
wasSuspended = true;
resolve();
});}}function render(props, ref) {
useState();
trySuspend();
return 'hello';
}const Wrapper = React.forwardRef(render);
const root = ReactTestRenderer.create(
<Suspense fallback="loading">
<Wrapper />
</Suspense>,
);expect(root).toMatchRenderedOutput('loading');
await Promise.resolve();
await waitForAll([]);
expect(root).toMatchRenderedOutput('hello');
});// Regression test for https://github.com/facebook/react/issues/14790
it('does not fire a false positive warning when suspending memo(forwardRef)', async () => {
const {Suspense, useState} = React;
let wasSuspended = false;
function trySuspend() {
if (!wasSuspended) {
throw new Promise(resolve => {
wasSuspended = true;
resolve();
});}}function render(props, ref) {
useState();
trySuspend();
return 'hello';
}const Wrapper = React.memo(React.forwardRef(render));
const root = ReactTestRenderer.create(
<Suspense fallback="loading">
<Wrapper />
</Suspense>,
);expect(root).toMatchRenderedOutput('loading');
await Promise.resolve();
await waitForAll([]);
expect(root).toMatchRenderedOutput('hello');
});// Regression test for https://github.com/facebook/react/issues/15732
it('resets hooks when an error is thrown in the middle of a list of hooks', async () => {
const {useEffect, useState} = React;
class ErrorBoundary extends React.Component {
state = {hasError: false};
static getDerivedStateFromError() {
return {hasError: true};
}render() {
return (
<Wrapper>
{this.state.hasError ? 'Error!' : this.props.children}
</Wrapper>
);}}function Wrapper({children}) {return children;}let setShouldThrow;function Thrower() {const [shouldThrow, _setShouldThrow] = useState(false);
setShouldThrow = _setShouldThrow;if (shouldThrow) {throw new Error('Throw!');}useEffect(() => {}, []);
return 'Throw!';}let root;await act(() => {root = ReactTestRenderer.create(
<ErrorBoundary><Thrower /></ErrorBoundary>,);});expect(root).toMatchRenderedOutput('Throw!');
await act(() => setShouldThrow(true));expect(root).toMatchRenderedOutput('Error!');
});});