react/packages/react-dom/src/__tests__/ReactUpdates-test.js
Sebastian Markbåge d6cfa0f295
[Fiber] Use Owner/JSX Stack When Appending Stacks to Console (#29206)
This one should be fully behind the `enableOwnerStacks` flag.

Instead of printing the parent Component stack all the way to the root,
this now prints the owner stack of every JSX callsite. It also includes
intermediate callsites between the Component and the JSX call so it has
potentially more frames. Mainly it provides the line number of the JSX
callsite. In terms of the number of components is a subset of the parent
component stack so it's less information in that regard. This is usually
better since it's more focused on components that might affect the
output but if it's contextual based on rendering it's still good to have
parent stack. Therefore, I still use the parent stack when printing DOM
nesting warnings but I plan on switching that format to a diff view
format instead (Next.js already reformats the parent stack like this).

__Follow ups__

- Server Components show up in the owner stack for client logs but logs
done by Server Components don't yet get their owner stack printed as
they're replayed. They're also not yet printed in the server logs of the
RSC server.

- Server Component stack frames are formatted as the server and added to
the end but this might be a different format than the browser. E.g. if
server is running V8 and browser is running JSC or vice versa. Ideally
we can reformat them in terms of the client formatting.

- This doesn't yet update Fizz or DevTools. Those will be follow ups.
Fizz still prints parent stacks in the server side logs. The stacks
added to user space `console.error` calls by DevTools still get the
parent stacks instead.

- It also doesn't yet expose these to user space so there's no way to
get them inside `onCaughtError` for example or inside a custom
`console.error` override.

- In another follow up I'll use `console.createTask` instead and
completely remove these stacks if it's available.
2024-05-25 11:58:17 -04:00

1977 lines
49 KiB
JavaScript

/**
* 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
*/
'use strict';
let React;
let ReactDOM;
let findDOMNode;
let ReactDOMClient;
let act;
let Scheduler;
let waitForAll;
let waitFor;
let assertLog;
describe('ReactUpdates', () => {
beforeEach(() => {
jest.resetModules();
React = require('react');
ReactDOM = require('react-dom');
ReactDOMClient = require('react-dom/client');
findDOMNode =
ReactDOM.__DOM_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE
.findDOMNode;
act = require('internal-test-utils').act;
Scheduler = require('scheduler');
const InternalTestUtils = require('internal-test-utils');
waitForAll = InternalTestUtils.waitForAll;
waitFor = InternalTestUtils.waitFor;
assertLog = InternalTestUtils.assertLog;
});
// Note: This is based on a similar component we use in www. We can delete
// once the extra div wrapper is no longer necessary.
function LegacyHiddenDiv({children, mode}) {
return (
<div hidden={mode === 'hidden'}>
<React.unstable_LegacyHidden
mode={mode === 'hidden' ? 'unstable-defer-without-hiding' : mode}>
{children}
</React.unstable_LegacyHidden>
</div>
);
}
it('should batch state when updating state twice', async () => {
let componentState;
let setState;
function Component() {
const [state, _setState] = React.useState(0);
componentState = state;
setState = _setState;
React.useLayoutEffect(() => {
Scheduler.log('Commit');
});
return <div>{state}</div>;
}
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
await act(() => {
root.render(<Component />);
});
assertLog(['Commit']);
expect(container.firstChild.textContent).toBe('0');
await act(() => {
setState(1);
setState(2);
expect(componentState).toBe(0);
expect(container.firstChild.textContent).toBe('0');
assertLog([]);
});
expect(componentState).toBe(2);
assertLog(['Commit']);
expect(container.firstChild.textContent).toBe('2');
});
it('should batch state when updating two different states', async () => {
let componentStateA;
let componentStateB;
let setStateA;
let setStateB;
function Component() {
const [stateA, _setStateA] = React.useState(0);
const [stateB, _setStateB] = React.useState(0);
componentStateA = stateA;
componentStateB = stateB;
setStateA = _setStateA;
setStateB = _setStateB;
React.useLayoutEffect(() => {
Scheduler.log('Commit');
});
return (
<div>
{stateA} {stateB}
</div>
);
}
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
await act(() => {
root.render(<Component />);
});
assertLog(['Commit']);
expect(container.firstChild.textContent).toBe('0 0');
await act(() => {
setStateA(1);
setStateB(2);
expect(componentStateA).toBe(0);
expect(componentStateB).toBe(0);
expect(container.firstChild.textContent).toBe('0 0');
assertLog([]);
});
expect(componentStateA).toBe(1);
expect(componentStateB).toBe(2);
assertLog(['Commit']);
expect(container.firstChild.textContent).toBe('1 2');
});
it('should batch state and props together', async () => {
let setState;
let componentProp;
let componentState;
function Component({prop}) {
const [state, _setState] = React.useState(0);
componentProp = prop;
componentState = state;
setState = _setState;
React.useLayoutEffect(() => {
Scheduler.log('Commit');
});
return (
<div>
{prop} {state}
</div>
);
}
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
await act(() => {
root.render(<Component prop={0} />);
});
assertLog(['Commit']);
expect(container.firstChild.textContent).toBe('0 0');
await act(() => {
root.render(<Component prop={1} />);
setState(2);
expect(componentProp).toBe(0);
expect(componentState).toBe(0);
expect(container.firstChild.textContent).toBe('0 0');
assertLog([]);
});
expect(componentProp).toBe(1);
expect(componentState).toBe(2);
assertLog(['Commit']);
expect(container.firstChild.textContent).toBe('1 2');
});
it('should batch parent/child state updates together', async () => {
let childRef;
let parentState;
let childState;
let setParentState;
let setChildState;
function Parent() {
const [state, _setState] = React.useState(0);
parentState = state;
setParentState = _setState;
React.useLayoutEffect(() => {
Scheduler.log('Parent Commit');
});
return (
<div>
<Child prop={state} />
</div>
);
}
function Child({prop}) {
const [state, _setState] = React.useState(0);
childState = state;
setChildState = _setState;
React.useLayoutEffect(() => {
Scheduler.log('Child Commit');
});
return (
<div
ref={ref => {
childRef = ref;
}}>
{prop} {state}
</div>
);
}
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
await act(() => {
root.render(<Parent />);
});
assertLog(['Child Commit', 'Parent Commit']);
expect(childRef.textContent).toBe('0 0');
await act(() => {
// Parent update first.
setParentState(1);
setChildState(2);
expect(parentState).toBe(0);
expect(childState).toBe(0);
expect(childRef.textContent).toBe('0 0');
assertLog([]);
});
expect(parentState).toBe(1);
expect(childState).toBe(2);
expect(childRef.textContent).toBe('1 2');
assertLog(['Child Commit', 'Parent Commit']);
});
it('should batch child/parent state updates together', async () => {
let childRef;
let parentState;
let childState;
let setParentState;
let setChildState;
function Parent() {
const [state, _setState] = React.useState(0);
parentState = state;
setParentState = _setState;
React.useLayoutEffect(() => {
Scheduler.log('Parent Commit');
});
return (
<div>
<Child prop={state} />
</div>
);
}
function Child({prop}) {
const [state, _setState] = React.useState(0);
childState = state;
setChildState = _setState;
React.useLayoutEffect(() => {
Scheduler.log('Child Commit');
});
return (
<div
ref={ref => {
childRef = ref;
}}>
{prop} {state}
</div>
);
}
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
await act(() => {
root.render(<Parent />);
});
assertLog(['Child Commit', 'Parent Commit']);
expect(childRef.textContent).toBe('0 0');
await act(() => {
// Child update first.
setChildState(2);
setParentState(1);
expect(parentState).toBe(0);
expect(childState).toBe(0);
expect(childRef.textContent).toBe('0 0');
assertLog([]);
});
expect(parentState).toBe(1);
expect(childState).toBe(2);
expect(childRef.textContent).toBe('1 2');
assertLog(['Child Commit', 'Parent Commit']);
});
it('should support chained state updates', async () => {
let instance;
class Component extends React.Component {
state = {x: 0};
constructor(props) {
super(props);
instance = this;
}
componentDidUpdate() {
Scheduler.log('Update');
}
render() {
return <div>{this.state.x}</div>;
}
}
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
await act(() => {
root.render(<Component />);
});
expect(instance.state.x).toBe(0);
expect(container.firstChild.textContent).toBe('0');
let innerCallbackRun = false;
await act(() => {
instance.setState({x: 1}, function () {
instance.setState({x: 2}, function () {
innerCallbackRun = true;
expect(instance.state.x).toBe(2);
expect(container.firstChild.textContent).toBe('2');
assertLog(['Update']);
});
expect(instance.state.x).toBe(1);
expect(container.firstChild.textContent).toBe('1');
assertLog(['Update']);
});
expect(instance.state.x).toBe(0);
expect(container.firstChild.textContent).toBe('0');
assertLog([]);
});
assertLog([]);
expect(instance.state.x).toBe(2);
expect(innerCallbackRun).toBeTruthy();
expect(container.firstChild.textContent).toBe('2');
});
it('should batch forceUpdate together', async () => {
let instance;
let shouldUpdateCount = 0;
class Component extends React.Component {
state = {x: 0};
constructor(props) {
super(props);
instance = this;
}
shouldComponentUpdate() {
shouldUpdateCount++;
}
componentDidUpdate() {
Scheduler.log('Update');
}
render() {
return <div>{this.state.x}</div>;
}
}
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
await act(() => {
root.render(<Component />);
});
assertLog([]);
expect(instance.state.x).toBe(0);
await act(() => {
instance.setState({x: 1}, function () {
Scheduler.log('callback');
});
instance.forceUpdate(function () {
Scheduler.log('forceUpdate');
});
assertLog([]);
expect(instance.state.x).toBe(0);
expect(container.firstChild.textContent).toBe('0');
});
// shouldComponentUpdate shouldn't be called since we're forcing
expect(shouldUpdateCount).toBe(0);
assertLog(['Update', 'callback', 'forceUpdate']);
expect(instance.state.x).toBe(1);
expect(container.firstChild.textContent).toBe('1');
});
it('should update children even if parent blocks updates', async () => {
let instance;
class Parent extends React.Component {
childRef = React.createRef();
constructor(props) {
super(props);
instance = this;
}
shouldComponentUpdate() {
return false;
}
render() {
Scheduler.log('Parent render');
return <Child ref={this.childRef} />;
}
}
class Child extends React.Component {
render() {
Scheduler.log('Child render');
return <div />;
}
}
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
await act(() => {
root.render(<Parent />);
});
assertLog(['Parent render', 'Child render']);
await act(() => {
instance.setState({x: 1});
});
assertLog([]);
await act(() => {
instance.childRef.current.setState({x: 1});
});
assertLog(['Child render']);
});
it('should not reconcile children passed via props', async () => {
class Top extends React.Component {
render() {
return (
<Middle>
<Bottom />
</Middle>
);
}
}
class Middle extends React.Component {
componentDidMount() {
this.forceUpdate();
}
render() {
Scheduler.log('Middle');
return React.Children.only(this.props.children);
}
}
class Bottom extends React.Component {
render() {
Scheduler.log('Bottom');
return null;
}
}
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
await act(() => {
root.render(<Top />);
});
assertLog(['Middle', 'Bottom', 'Middle']);
});
it('should flow updates correctly', async () => {
let willUpdates = [];
let didUpdates = [];
let instance;
const UpdateLoggingMixin = {
UNSAFE_componentWillUpdate: function () {
willUpdates.push(this.constructor.displayName);
},
componentDidUpdate: function () {
didUpdates.push(this.constructor.displayName);
},
};
class Box extends React.Component {
boxDivRef = React.createRef();
render() {
return <div ref={this.boxDivRef}>{this.props.children}</div>;
}
}
Object.assign(Box.prototype, UpdateLoggingMixin);
class Child extends React.Component {
spanRef = React.createRef();
render() {
return <span ref={this.spanRef}>child</span>;
}
}
Object.assign(Child.prototype, UpdateLoggingMixin);
class Switcher extends React.Component {
state = {tabKey: 'hello'};
boxRef = React.createRef();
switcherDivRef = React.createRef();
render() {
const child = this.props.children;
return (
<Box ref={this.boxRef}>
<div
ref={this.switcherDivRef}
style={{
display: this.state.tabKey === child.key ? '' : 'none',
}}>
{child}
</div>
</Box>
);
}
}
Object.assign(Switcher.prototype, UpdateLoggingMixin);
class App extends React.Component {
switcherRef = React.createRef();
childRef = React.createRef();
constructor(props) {
super(props);
instance = this;
}
render() {
return (
<Switcher ref={this.switcherRef}>
<Child key="hello" ref={this.childRef} />
</Switcher>
);
}
}
Object.assign(App.prototype, UpdateLoggingMixin);
const container = document.createElement('div');
await act(() => {
ReactDOMClient.createRoot(container).render(<App />);
});
function expectUpdates(desiredWillUpdates, desiredDidUpdates) {
let i;
for (i = 0; i < desiredWillUpdates; i++) {
expect(willUpdates).toContain(desiredWillUpdates[i]);
}
for (i = 0; i < desiredDidUpdates; i++) {
expect(didUpdates).toContain(desiredDidUpdates[i]);
}
willUpdates = [];
didUpdates = [];
}
function triggerUpdate(c) {
c.setState({x: 1});
}
async function testUpdates(
components,
desiredWillUpdates,
desiredDidUpdates,
) {
let i;
await act(() => {
for (i = 0; i < components.length; i++) {
triggerUpdate(components[i]);
}
});
expectUpdates(desiredWillUpdates, desiredDidUpdates);
// Try them in reverse order
await act(() => {
for (i = components.length - 1; i >= 0; i--) {
triggerUpdate(components[i]);
}
});
expectUpdates(desiredWillUpdates, desiredDidUpdates);
}
await testUpdates(
[
instance.switcherRef.current.boxRef.current,
instance.switcherRef.current,
],
// Owner-child relationships have inverse will and did
['Switcher', 'Box'],
['Box', 'Switcher'],
);
await testUpdates(
[instance.childRef.current, instance.switcherRef.current.boxRef.current],
// Not owner-child so reconcile independently
['Box', 'Child'],
['Box', 'Child'],
);
await testUpdates(
[instance.childRef.current, instance.switcherRef.current],
// Switcher owns Box and Child, Box does not own Child
['Switcher', 'Box', 'Child'],
['Box', 'Switcher', 'Child'],
);
});
it('should queue mount-ready handlers across different roots', async () => {
// We'll define two components A and B, then update both of them. When A's
// componentDidUpdate handlers is called, B's DOM should already have been
// updated.
const bContainer = document.createElement('div');
let a;
let b;
let aUpdated = false;
class A extends React.Component {
state = {x: 0};
constructor(props) {
super(props);
a = this;
}
componentDidUpdate() {
expect(findDOMNode(b).textContent).toBe('B1');
aUpdated = true;
}
render() {
let portal = null;
portal = ReactDOM.createPortal(<B ref={n => (b = n)} />, bContainer);
return (
<div>
A{this.state.x}
{portal}
</div>
);
}
}
class B extends React.Component {
state = {x: 0};
render() {
return <div>B{this.state.x}</div>;
}
}
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
await act(() => {
root.render(<A />);
});
await act(() => {
a.setState({x: 1});
b.setState({x: 1});
});
expect(aUpdated).toBe(true);
});
it('should flush updates in the correct order', async () => {
const updates = [];
let instance;
class Outer extends React.Component {
state = {x: 0};
innerRef = React.createRef();
constructor(props) {
super(props);
instance = this;
}
render() {
updates.push('Outer-render-' + this.state.x);
return (
<div>
<Inner x={this.state.x} ref={this.innerRef} />
</div>
);
}
componentDidUpdate() {
const x = this.state.x;
updates.push('Outer-didUpdate-' + x);
updates.push('Inner-setState-' + x);
this.innerRef.current.setState({x: x}, function () {
updates.push('Inner-callback-' + x);
});
}
}
class Inner extends React.Component {
state = {x: 0};
render() {
updates.push('Inner-render-' + this.props.x + '-' + this.state.x);
return <div />;
}
componentDidUpdate() {
updates.push('Inner-didUpdate-' + this.props.x + '-' + this.state.x);
}
}
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
await act(() => {
root.render(<Outer />);
});
await act(() => {
updates.push('Outer-setState-1');
instance.setState({x: 1}, function () {
updates.push('Outer-callback-1');
updates.push('Outer-setState-2');
instance.setState({x: 2}, function () {
updates.push('Outer-callback-2');
});
});
});
/* eslint-disable indent */
expect(updates).toEqual([
'Outer-render-0',
'Inner-render-0-0',
'Outer-setState-1',
'Outer-render-1',
'Inner-render-1-0',
'Inner-didUpdate-1-0',
'Outer-didUpdate-1',
// Happens in a batch, so don't re-render yet
'Inner-setState-1',
'Outer-callback-1',
// Happens in a batch
'Outer-setState-2',
// Flush batched updates all at once
'Outer-render-2',
'Inner-render-2-1',
'Inner-didUpdate-2-1',
'Inner-callback-1',
'Outer-didUpdate-2',
'Inner-setState-2',
'Outer-callback-2',
'Inner-render-2-2',
'Inner-didUpdate-2-2',
'Inner-callback-2',
]);
/* eslint-enable indent */
});
it('should flush updates in the correct order across roots', async () => {
const instances = [];
const updates = [];
class MockComponent extends React.Component {
render() {
updates.push(this.props.depth);
return <div />;
}
componentDidMount() {
instances.push(this);
if (this.props.depth < this.props.count) {
const root = ReactDOMClient.createRoot(findDOMNode(this));
root.render(
<MockComponent
depth={this.props.depth + 1}
count={this.props.count}
/>,
);
}
}
}
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
await act(() => {
root.render(<MockComponent depth={0} count={2} />);
});
expect(updates).toEqual([0, 1, 2]);
await act(() => {
// Simulate update on each component from top to bottom.
instances.forEach(function (instance) {
instance.forceUpdate();
});
});
expect(updates).toEqual([0, 1, 2, 0, 1, 2]);
});
it('should queue nested updates', async () => {
// See https://github.com/facebook/react/issues/1147
class X extends React.Component {
state = {s: 0};
render() {
if (this.state.s === 0) {
return (
<div>
<span>0</span>
</div>
);
} else {
return <div>1</div>;
}
}
go = () => {
this.setState({s: 1});
this.setState({s: 0});
this.setState({s: 1});
};
}
class Y extends React.Component {
render() {
return (
<div>
<Z />
</div>
);
}
}
class Z extends React.Component {
render() {
return <div />;
}
UNSAFE_componentWillUpdate() {
x.go();
}
}
let container = document.createElement('div');
let root = ReactDOMClient.createRoot(container);
let x;
await act(() => {
root.render(<X ref={current => (x = current)} />);
});
container = document.createElement('div');
root = ReactDOMClient.createRoot(container);
let y;
await act(() => {
root.render(<Y ref={current => (y = current)} />);
});
expect(findDOMNode(x).textContent).toBe('0');
await act(() => {
y.forceUpdate();
});
expect(findDOMNode(x).textContent).toBe('1');
});
it('should queue updates from during mount', async () => {
// See https://github.com/facebook/react/issues/1353
let a;
class A extends React.Component {
state = {x: 0};
UNSAFE_componentWillMount() {
a = this;
}
render() {
return <div>A{this.state.x}</div>;
}
}
class B extends React.Component {
UNSAFE_componentWillMount() {
a.setState({x: 1});
}
render() {
return <div />;
}
}
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
await act(() => {
root.render(
<div>
<A />
<B />
</div>,
);
});
expect(container.firstChild.textContent).toBe('A1');
});
it('calls componentWillReceiveProps setState callback properly', async () => {
class A extends React.Component {
state = {x: this.props.x};
UNSAFE_componentWillReceiveProps(nextProps) {
const newX = nextProps.x;
this.setState({x: newX}, function () {
// State should have updated by the time this callback gets called
expect(this.state.x).toBe(newX);
Scheduler.log('Callback');
});
}
render() {
return <div>{this.state.x}</div>;
}
}
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
await act(() => {
root.render(<A x={1} />);
});
assertLog([]);
// Needs to be a separate act, or it will be batched.
await act(() => {
root.render(<A x={2} />);
});
assertLog(['Callback']);
});
it('does not call render after a component as been deleted', async () => {
let componentA = null;
let componentB = null;
class B extends React.Component {
state = {updates: 0};
componentDidMount() {
componentB = this;
}
render() {
Scheduler.log('B');
return <div />;
}
}
class A extends React.Component {
state = {showB: true};
componentDidMount() {
componentA = this;
}
render() {
return this.state.showB ? <B /> : <div />;
}
}
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
await act(() => {
root.render(<A />);
});
assertLog(['B']);
await act(() => {
// B will have scheduled an update but the batching should ensure that its
// update never fires.
componentB.setState({updates: 1});
componentA.setState({showB: false});
});
assertLog([]);
});
it('throws in setState if the update callback is not a function', async () => {
function Foo() {
this.a = 1;
this.b = 2;
}
class A extends React.Component {
state = {};
render() {
return <div />;
}
}
let container = document.createElement('div');
let root = ReactDOMClient.createRoot(container);
let component;
await act(() => {
root.render(<A ref={current => (component = current)} />);
});
await expect(
expect(async () => {
await act(() => {
component.setState({}, 'no');
});
}).toErrorDev(
'Expected the last optional `callback` argument to be ' +
'a function. Instead received: no.',
),
).rejects.toThrowError(
'Invalid argument passed as callback. Expected a function. Instead ' +
'received: no',
);
container = document.createElement('div');
root = ReactDOMClient.createRoot(container);
await act(() => {
root.render(<A ref={current => (component = current)} />);
});
await expect(
expect(async () => {
await act(() => {
component.setState({}, {foo: 'bar'});
});
}).toErrorDev(
'Expected the last optional `callback` argument to be ' +
'a function. Instead received: [object Object].',
),
).rejects.toThrowError(
'Invalid argument passed as callback. Expected a function. Instead ' +
'received: [object Object]',
);
container = document.createElement('div');
root = ReactDOMClient.createRoot(container);
await act(() => {
root.render(<A ref={current => (component = current)} />);
});
await expect(
act(() => {
component.setState({}, new Foo());
}),
).rejects.toThrowError(
'Invalid argument passed as callback. Expected a function. Instead ' +
'received: [object Object]',
);
});
it('throws in forceUpdate if the update callback is not a function', async () => {
function Foo() {
this.a = 1;
this.b = 2;
}
class A extends React.Component {
state = {};
render() {
return <div />;
}
}
let container = document.createElement('div');
let root = ReactDOMClient.createRoot(container);
let component;
await act(() => {
root.render(<A ref={current => (component = current)} />);
});
await expect(
expect(async () => {
await act(() => {
component.forceUpdate('no');
});
}).toErrorDev(
'Expected the last optional `callback` argument to be ' +
'a function. Instead received: no.',
),
).rejects.toThrowError(
'Invalid argument passed as callback. Expected a function. Instead ' +
'received: no',
);
container = document.createElement('div');
root = ReactDOMClient.createRoot(container);
await act(() => {
root.render(<A ref={current => (component = current)} />);
});
await expect(
expect(async () => {
await act(() => {
component.forceUpdate({foo: 'bar'});
});
}).toErrorDev(
'Expected the last optional `callback` argument to be ' +
'a function. Instead received: [object Object].',
),
).rejects.toThrowError(
'Invalid argument passed as callback. Expected a function. Instead ' +
'received: [object Object]',
);
// Make sure the warning is deduplicated and doesn't fire again
container = document.createElement('div');
root = ReactDOMClient.createRoot(container);
await act(() => {
root.render(<A ref={current => (component = current)} />);
});
await expect(
act(() => {
component.forceUpdate(new Foo());
}),
).rejects.toThrowError(
'Invalid argument passed as callback. Expected a function. Instead ' +
'received: [object Object]',
);
});
it('does not update one component twice in a batch (#2410)', async () => {
let parent;
class Parent extends React.Component {
childRef = React.createRef();
componentDidMount() {
parent = this;
}
getChild = () => {
return this.childRef.current;
};
render() {
return <Child ref={this.childRef} />;
}
}
let renderCount = 0;
let postRenderCount = 0;
let once = false;
class Child extends React.Component {
state = {updated: false};
UNSAFE_componentWillUpdate() {
if (!once) {
once = true;
this.setState({updated: true});
}
}
componentDidMount() {
expect(renderCount).toBe(postRenderCount + 1);
postRenderCount++;
}
componentDidUpdate() {
expect(renderCount).toBe(postRenderCount + 1);
postRenderCount++;
}
render() {
expect(renderCount).toBe(postRenderCount);
renderCount++;
return <div />;
}
}
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
await act(() => {
root.render(<Parent />);
});
const child = parent.getChild();
await act(() => {
parent.forceUpdate();
child.forceUpdate();
});
expect.assertions(6);
});
it('does not update one component twice in a batch (#6371)', async () => {
let callbacks = [];
function emitChange() {
callbacks.forEach(c => c());
}
class App extends React.Component {
constructor(props) {
super(props);
this.state = {showChild: true};
}
componentDidMount() {
this.setState({showChild: false});
}
render() {
return (
<div>
<ForceUpdatesOnChange />
{this.state.showChild && <EmitsChangeOnUnmount />}
</div>
);
}
}
class EmitsChangeOnUnmount extends React.Component {
componentWillUnmount() {
emitChange();
}
render() {
return null;
}
}
class ForceUpdatesOnChange extends React.Component {
componentDidMount() {
this.onChange = () => this.forceUpdate();
this.onChange();
callbacks.push(this.onChange);
}
componentWillUnmount() {
callbacks = callbacks.filter(c => c !== this.onChange);
}
render() {
return <div key={Math.random()} onClick={function () {}} />;
}
}
const root = ReactDOMClient.createRoot(document.createElement('div'));
await act(() => {
root.render(<App />);
});
// Error should not be thrown.
expect(true).toBe(true);
});
it('handles reentrant mounting in synchronous mode', async () => {
let onChangeCalled = false;
class Editor extends React.Component {
render() {
return <div>{this.props.text}</div>;
}
componentDidMount() {
Scheduler.log('Mount');
// This should be called only once but we guard just in case.
if (!this.props.rendered) {
this.props.onChange({rendered: true});
}
}
}
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
function render() {
root.render(
<Editor
onChange={newProps => {
onChangeCalled = true;
props = {...props, ...newProps};
render();
}}
{...props}
/>,
);
}
let props = {text: 'hello', rendered: false};
await act(() => {
render();
});
assertLog(['Mount']);
props = {...props, text: 'goodbye'};
await act(() => {
render();
});
assertLog([]);
expect(container.textContent).toBe('goodbye');
expect(onChangeCalled).toBeTruthy();
});
it('mounts and unmounts are batched', async () => {
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
await act(() => {
root.render(<div>Hello</div>);
expect(container.textContent).toBe('');
root.unmount(container);
expect(container.textContent).toBe('');
});
expect(container.textContent).toBe('');
});
it('uses correct base state for setState inside render phase', async () => {
class Foo extends React.Component {
state = {step: 0};
render() {
const memoizedStep = this.state.step;
this.setState(baseState => {
const baseStep = baseState.step;
Scheduler.log(`base: ${baseStep}, memoized: ${memoizedStep}`);
return baseStep === 0 ? {step: 1} : null;
});
return null;
}
}
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
await expect(async () => {
await act(() => {
root.render(<Foo />);
});
}).toErrorDev('Cannot update during an existing state transition');
assertLog(['base: 0, memoized: 0', 'base: 1, memoized: 1']);
});
it('does not re-render if state update is null', async () => {
const container = document.createElement('div');
let instance;
class Foo extends React.Component {
render() {
instance = this;
Scheduler.log('render');
return <div />;
}
}
const root = ReactDOMClient.createRoot(container);
await act(() => {
root.render(<Foo />);
});
assertLog(['render']);
await act(() => {
instance.setState(() => null);
});
assertLog([]);
});
it('synchronously renders hidden subtrees', async () => {
const container = document.createElement('div');
function Baz() {
Scheduler.log('Baz');
return null;
}
function Bar() {
Scheduler.log('Bar');
return null;
}
function Foo() {
Scheduler.log('Foo');
return (
<div>
<div hidden={true}>
<Bar />
</div>
<Baz />
</div>
);
}
const root = ReactDOMClient.createRoot(container);
await act(() => {
// Mount
root.render(<Foo />);
});
assertLog(['Foo', 'Bar', 'Baz']);
await act(() => {
// Update
root.render(<Foo />);
});
assertLog(['Foo', 'Bar', 'Baz']);
});
// @gate www
it('delays sync updates inside hidden subtrees in Concurrent Mode', async () => {
const container = document.createElement('div');
function Baz() {
Scheduler.log('Baz');
return <p>baz</p>;
}
let setCounter;
function Bar() {
const [counter, _setCounter] = React.useState(0);
setCounter = _setCounter;
Scheduler.log('Bar');
return <p>bar {counter}</p>;
}
function Foo() {
Scheduler.log('Foo');
React.useEffect(() => {
Scheduler.log('Foo#effect');
});
return (
<div>
<LegacyHiddenDiv mode="hidden">
<Bar />
</LegacyHiddenDiv>
<Baz />
</div>
);
}
const root = ReactDOMClient.createRoot(container);
let hiddenDiv;
await act(async () => {
root.render(<Foo />);
await waitFor(['Foo', 'Baz', 'Foo#effect']);
hiddenDiv = container.firstChild.firstChild;
expect(hiddenDiv.hidden).toBe(true);
expect(hiddenDiv.innerHTML).toBe('');
// Run offscreen update
await waitForAll(['Bar']);
expect(hiddenDiv.hidden).toBe(true);
expect(hiddenDiv.innerHTML).toBe('<p>bar 0</p>');
});
ReactDOM.flushSync(() => {
setCounter(1);
});
// Should not flush yet
expect(hiddenDiv.innerHTML).toBe('<p>bar 0</p>');
// Run offscreen update
await waitForAll(['Bar']);
expect(hiddenDiv.innerHTML).toBe('<p>bar 1</p>');
});
it('can render ridiculously large number of roots without triggering infinite update loop error', async () => {
function Component({trigger}) {
const [state, setState] = React.useState(0);
React.useEffect(() => {
if (trigger) {
Scheduler.log('Trigger');
setState(c => c + 1);
}
}, [trigger]);
return <div>{state}</div>;
}
class Foo extends React.Component {
componentDidMount() {
const limit = 1200;
for (let i = 0; i < limit; i++) {
if (i < limit - 1) {
ReactDOMClient.createRoot(document.createElement('div')).render(
<Component />,
);
} else {
// The "nested update limit" error isn't thrown until setState
ReactDOMClient.createRoot(document.createElement('div')).render(
<Component trigger={true} />,
);
}
}
}
render() {
return null;
}
}
const root = ReactDOMClient.createRoot(document.createElement('div'));
await act(() => {
root.render(<Foo />);
});
// Make sure the setState trigger runs.
assertLog(['Trigger']);
});
it('resets the update counter for unrelated updates', async () => {
const container = document.createElement('div');
const ref = React.createRef();
class EventuallyTerminating extends React.Component {
state = {step: 0};
componentDidMount() {
this.setState({step: 1});
}
componentDidUpdate() {
if (this.state.step < limit) {
this.setState({step: this.state.step + 1});
}
}
render() {
return this.state.step;
}
}
let limit = 55;
const root = ReactDOMClient.createRoot(container);
await expect(async () => {
await act(() => {
root.render(<EventuallyTerminating ref={ref} />);
});
}).rejects.toThrow('Maximum');
// Verify that we don't go over the limit if these updates are unrelated.
limit -= 10;
await act(() => {
root.render(<EventuallyTerminating ref={ref} />);
});
expect(container.textContent).toBe(limit.toString());
await act(() => {
ref.current.setState({step: 0});
});
expect(container.textContent).toBe(limit.toString());
await act(() => {
ref.current.setState({step: 0});
});
expect(container.textContent).toBe(limit.toString());
limit += 10;
await expect(async () => {
await act(() => {
ref.current.setState({step: 0});
});
}).rejects.toThrow('Maximum');
expect(ref.current).toBe(null);
});
it('does not fall into an infinite update loop', async () => {
class NonTerminating extends React.Component {
state = {step: 0};
componentDidMount() {
this.setState({step: 1});
}
componentDidUpdate() {
this.setState({step: 2});
}
render() {
return (
<div>
Hello {this.props.name}
{this.state.step}
</div>
);
}
}
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
await expect(async () => {
await act(() => {
root.render(<NonTerminating />);
});
}).rejects.toThrow('Maximum');
});
it('does not fall into an infinite update loop with useLayoutEffect', async () => {
function NonTerminating() {
const [step, setStep] = React.useState(0);
React.useLayoutEffect(() => {
setStep(x => x + 1);
});
return step;
}
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
await expect(async () => {
await act(() => {
root.render(<NonTerminating />);
});
}).rejects.toThrow('Maximum');
});
it('can recover after falling into an infinite update loop', async () => {
class NonTerminating extends React.Component {
state = {step: 0};
componentDidMount() {
this.setState({step: 1});
}
componentDidUpdate() {
this.setState({step: 2});
}
render() {
return this.state.step;
}
}
class Terminating extends React.Component {
state = {step: 0};
componentDidMount() {
this.setState({step: 1});
}
render() {
return this.state.step;
}
}
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
await expect(async () => {
await act(() => {
root.render(<NonTerminating />);
});
}).rejects.toThrow('Maximum');
await act(() => {
root.render(<Terminating />);
});
expect(container.textContent).toBe('1');
await expect(async () => {
await act(() => {
root.render(<NonTerminating />);
});
}).rejects.toThrow('Maximum');
await act(() => {
root.render(<Terminating />);
});
expect(container.textContent).toBe('1');
});
it('does not fall into mutually recursive infinite update loop with same container', async () => {
// Note: this test would fail if there were two or more different roots.
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
class A extends React.Component {
componentDidMount() {
root.render(<B />);
}
render() {
return null;
}
}
class B extends React.Component {
componentDidMount() {
root.render(<A />);
}
render() {
return null;
}
}
await expect(async () => {
await act(() => {
root.render(<A />);
});
}).rejects.toThrow('Maximum');
});
it('does not fall into an infinite error loop', async () => {
function BadRender() {
throw new Error('error');
}
class ErrorBoundary extends React.Component {
componentDidCatch() {
// Schedule a no-op state update to avoid triggering a DEV warning in the test.
this.setState({});
this.props.parent.remount();
}
render() {
return <BadRender />;
}
}
class NonTerminating extends React.Component {
state = {step: 0};
remount() {
this.setState(state => ({step: state.step + 1}));
}
render() {
return <ErrorBoundary key={this.state.step} parent={this} />;
}
}
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
await expect(async () => {
await act(() => {
root.render(<NonTerminating />);
});
}).rejects.toThrow('Maximum');
});
it('can schedule ridiculously many updates within the same batch without triggering a maximum update error', async () => {
const subscribers = [];
const limit = 1200;
class Child extends React.Component {
state = {value: 'initial'};
componentDidMount() {
subscribers.push(this);
}
render() {
return null;
}
}
class App extends React.Component {
render() {
const children = [];
for (let i = 0; i < limit; i++) {
children.push(<Child key={i} />);
}
return children;
}
}
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
await act(() => {
root.render(<App />);
});
await act(() => {
subscribers.forEach(s => {
s.setState({value: 'update'});
});
});
expect(subscribers.length).toBe(limit);
});
it("does not infinite loop if there's a synchronous render phase update on another component", async () => {
if (gate(flags => !flags.enableInfiniteRenderLoopDetection)) {
return;
}
let setState;
function App() {
const [, _setState] = React.useState(0);
setState = _setState;
return <Child />;
}
function Child(step) {
// This will cause an infinite update loop, and a warning in dev.
setState(n => n + 1);
return null;
}
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
await expect(async () => {
await expect(async () => {
await act(() => ReactDOM.flushSync(() => root.render(<App />)));
}).rejects.toThrow('Maximum update depth exceeded');
}).toErrorDev(
'Warning: Cannot update a component (`App`) while rendering a different component (`Child`)',
);
});
it("does not infinite loop if there's an async render phase update on another component", async () => {
if (gate(flags => !flags.enableInfiniteRenderLoopDetection)) {
return;
}
let setState;
function App() {
const [, _setState] = React.useState(0);
setState = _setState;
return <Child />;
}
function Child(step) {
// This will cause an infinite update loop, and a warning in dev.
setState(n => n + 1);
return null;
}
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
await expect(async () => {
let error;
try {
await act(() => {
React.startTransition(() => root.render(<App />));
});
} catch (e) {
error = e;
}
expect(error.message).toMatch('Maximum update depth exceeded');
}).toErrorDev(
'Warning: Cannot update a component (`App`) while rendering a different component (`Child`)',
);
});
// TODO: Replace this branch with @gate pragmas
if (__DEV__) {
it('warns about a deferred infinite update loop with useEffect', async () => {
function NonTerminating() {
const [step, setStep] = React.useState(0);
React.useEffect(function myEffect() {
setStep(x => x + 1);
});
return step;
}
function App() {
return <NonTerminating />;
}
let error = null;
let stack = null;
let nativeStack = null;
const originalConsoleError = console.error;
console.error = (e, s) => {
error = e;
stack = s;
nativeStack = new Error().stack;
Scheduler.log('stop');
};
try {
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
root.render(<App />);
await waitFor(['stop']);
} finally {
console.error = originalConsoleError;
}
expect(error).toContain('Maximum update depth exceeded');
// The currently executing effect should be on the native stack
expect(nativeStack).toContain('at myEffect');
if (!gate(flags => flags.enableOwnerStacks)) {
// The currently running component's name is not in the owner
// stack because it's just its JSX callsite.
expect(stack).toContain('at NonTerminating');
}
expect(stack).toContain('at App');
});
it('can have nested updates if they do not cross the limit', async () => {
let _setStep;
const LIMIT = 50;
function Terminating() {
const [step, setStep] = React.useState(0);
_setStep = setStep;
React.useEffect(() => {
if (step < LIMIT) {
setStep(x => x + 1);
}
});
Scheduler.log(step);
return step;
}
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
await act(() => {
root.render(<Terminating />);
});
assertLog(Array.from({length: LIMIT + 1}, (_, k) => k));
expect(container.textContent).toBe('50');
await act(() => {
_setStep(0);
});
expect(container.textContent).toBe('50');
});
it('can have many updates inside useEffect without triggering a warning', async () => {
function Terminating() {
const [step, setStep] = React.useState(0);
React.useEffect(() => {
for (let i = 0; i < 1000; i++) {
setStep(x => x + 1);
}
Scheduler.log('Done');
}, []);
return step;
}
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
await act(() => {
root.render(<Terminating />);
});
assertLog(['Done']);
expect(container.textContent).toBe('1000');
});
}
it('prevents infinite update loop triggered by synchronous updates in useEffect', async () => {
// Ignore flushSync warning
spyOnDev(console, 'error').mockImplementation(() => {});
function NonTerminating() {
const [step, setStep] = React.useState(0);
React.useEffect(() => {
// Other examples of synchronous updates in useEffect are imperative
// event dispatches like `el.focus`, or `useSyncExternalStore`, which
// may schedule a synchronous update upon subscribing if it detects
// that the store has been mutated since the initial render.
//
// (Originally I wrote this test using `el.focus` but those errors
// get dispatched in a JSDOM event and I don't know how to "catch" those
// so that they don't fail the test.)
ReactDOM.flushSync(() => {
setStep(step + 1);
});
}, [step]);
return step;
}
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
await expect(async () => {
await act(() => {
ReactDOM.flushSync(() => {
root.render(<NonTerminating />);
});
});
}).rejects.toThrow('Maximum update depth exceeded');
});
});