/**
* 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';
describe('ReactMultiChild', () => {
let React;
let ReactDOMClient;
let act;
let assertConsoleErrorDev;
beforeEach(() => {
jest.resetModules();
React = require('react');
ReactDOMClient = require('react-dom/client');
act = require('internal-test-utils').act;
assertConsoleErrorDev =
require('internal-test-utils').assertConsoleErrorDev;
});
describe('reconciliation', () => {
it('should update children when possible', async () => {
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
const mockMount = jest.fn();
const mockUpdate = jest.fn();
const mockUnmount = jest.fn();
class MockComponent extends React.Component {
componentDidMount = mockMount;
componentDidUpdate = mockUpdate;
componentWillUnmount = mockUnmount;
render() {
return ;
}
}
expect(mockMount).toHaveBeenCalledTimes(0);
expect(mockUpdate).toHaveBeenCalledTimes(0);
expect(mockUnmount).toHaveBeenCalledTimes(0);
await act(async () => {
root.render(
,
);
});
expect(mockMount).toHaveBeenCalledTimes(1);
expect(mockUpdate).toHaveBeenCalledTimes(0);
expect(mockUnmount).toHaveBeenCalledTimes(0);
await act(async () => {
root.render(
,
);
});
expect(mockMount).toHaveBeenCalledTimes(1);
expect(mockUpdate).toHaveBeenCalledTimes(1);
expect(mockUnmount).toHaveBeenCalledTimes(0);
});
it('should replace children with different constructors', async () => {
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
const mockMount = jest.fn();
const mockUnmount = jest.fn();
class MockComponent extends React.Component {
componentDidMount = mockMount;
componentWillUnmount = mockUnmount;
render() {
return ;
}
}
expect(mockMount).toHaveBeenCalledTimes(0);
expect(mockUnmount).toHaveBeenCalledTimes(0);
await act(async () => {
root.render(
,
);
});
expect(mockMount).toHaveBeenCalledTimes(1);
expect(mockUnmount).toHaveBeenCalledTimes(0);
await act(async () => {
root.render(
,
);
});
expect(mockMount).toHaveBeenCalledTimes(1);
expect(mockUnmount).toHaveBeenCalledTimes(1);
});
it('should NOT replace children with different owners', async () => {
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
const mockMount = jest.fn();
const mockUnmount = jest.fn();
class MockComponent extends React.Component {
componentDidMount = mockMount;
componentWillUnmount = mockUnmount;
render() {
return ;
}
}
class WrapperComponent extends React.Component {
render() {
return this.props.children || ;
}
}
expect(mockMount).toHaveBeenCalledTimes(0);
expect(mockUnmount).toHaveBeenCalledTimes(0);
await act(async () => {
root.render();
});
expect(mockMount).toHaveBeenCalledTimes(1);
expect(mockUnmount).toHaveBeenCalledTimes(0);
await act(async () => {
root.render(
,
);
});
expect(mockMount).toHaveBeenCalledTimes(1);
expect(mockUnmount).toHaveBeenCalledTimes(0);
});
it('should replace children with different keys', async () => {
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
const mockMount = jest.fn();
const mockUnmount = jest.fn();
class MockComponent extends React.Component {
componentDidMount = mockMount;
componentWillUnmount = mockUnmount;
render() {
return ;
}
}
expect(mockMount).toHaveBeenCalledTimes(0);
expect(mockUnmount).toHaveBeenCalledTimes(0);
await act(async () => {
root.render(
,
);
});
expect(mockMount).toHaveBeenCalledTimes(1);
expect(mockUnmount).toHaveBeenCalledTimes(0);
await act(async () => {
root.render(
,
);
});
expect(mockMount).toHaveBeenCalledTimes(2);
expect(mockUnmount).toHaveBeenCalledTimes(1);
});
it('should warn for duplicated array keys with component stack info', async () => {
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
class WrapperComponent extends React.Component {
render() {
return {this.props.children}
;
}
}
class Parent extends React.Component {
render() {
return (
{this.props.children}
);
}
}
await act(async () => {
root.render({[]});
});
await act(async () => {
root.render({[, ]});
});
assertConsoleErrorDev([
'Encountered two children with the same key, `1`. ' +
'Keys should be unique so that components maintain their identity ' +
'across updates. Non-unique keys may cause children to be ' +
'duplicated and/or omitted — the behavior is unsupported and ' +
'could change in a future version.\n' +
' in div (at **)',
]);
});
it('should warn for duplicated iterable keys with component stack info', async () => {
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
class WrapperComponent extends React.Component {
render() {
return {this.props.children}
;
}
}
class Parent extends React.Component {
render() {
return (
{this.props.children}
);
}
}
function createIterable(array) {
return {
'@@iterator': function () {
let i = 0;
return {
next() {
const next = {
value: i < array.length ? array[i] : undefined,
done: i === array.length,
};
i++;
return next;
},
};
},
};
}
await act(async () => {
root.render({createIterable([])});
});
await act(async () => {
root.render(
{createIterable([, ])},
);
});
assertConsoleErrorDev([
'Encountered two children with the same key, `1`. ' +
'Keys should be unique so that components maintain their identity ' +
'across updates. Non-unique keys may cause children to be ' +
'duplicated and/or omitted — the behavior is unsupported and ' +
'could change in a future version.\n' +
' in div (at **)',
]);
});
});
it('should warn for using maps as children with owner info', async () => {
class Parent extends React.Component {
render() {
return (
{
new Map([
['foo', 0],
['bar', 1],
])
}
);
}
}
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
await act(async () => {
root.render();
});
assertConsoleErrorDev([
'Using Maps as children is not supported. ' +
'Use an array of keyed ReactElements instead.\n' +
' in div (at **)\n' +
' in Parent (at **)',
]);
});
it('should NOT warn for using generator functions as components', async () => {
function* Foo() {
yield Hello
;
yield World
;
}
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
await act(async () => {
root.render();
});
expect(container.textContent).toBe('HelloWorld');
});
it('should warn for using generators as children props', async () => {
function* getChildren() {
yield Hello
;
yield World
;
}
function Foo() {
const children = getChildren();
return {children}
;
}
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
await act(async () => {
root.render();
});
assertConsoleErrorDev([
'Using Iterators as children is unsupported and will likely yield ' +
'unexpected results because enumerating a generator mutates it. ' +
'You may convert it to an array with `Array.from()` or the ' +
'`[...spread]` operator before rendering. You can also use an ' +
'Iterable that can iterate multiple times over the same items.\n' +
' in div (at **)\n' +
' in Foo (at **)',
]);
expect(container.textContent).toBe('HelloWorld');
// Test de-duplication
await act(async () => {
root.render();
});
});
it('should warn for using other types of iterators as children', async () => {
function Foo() {
let i = 0;
const iterator = {
[Symbol.iterator]() {
return iterator;
},
next() {
switch (i++) {
case 0:
return {done: false, value: Hello
};
case 1:
return {done: false, value: World
};
default:
return {done: true, value: undefined};
}
},
};
return iterator;
}
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
await act(async () => {
root.render();
});
assertConsoleErrorDev([
'Using Iterators as children is unsupported and will likely yield ' +
'unexpected results because enumerating a generator mutates it. ' +
'You may convert it to an array with `Array.from()` or the ' +
'`[...spread]` operator before rendering. You can also use an ' +
'Iterable that can iterate multiple times over the same items.\n' +
' in Foo (at **)',
]);
expect(container.textContent).toBe('HelloWorld');
// Test de-duplication
await act(async () => {
root.render();
});
});
it('should not warn for using generators in legacy iterables', async () => {
const fooIterable = {
'@@iterator': function* () {
yield Hello
;
yield World
;
},
};
function Foo() {
return fooIterable;
}
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
await act(async () => {
root.render();
});
expect(container.textContent).toBe('HelloWorld');
await act(async () => {
root.render();
});
expect(container.textContent).toBe('HelloWorld');
});
it('should not warn for using generators in modern iterables', async () => {
const fooIterable = {
[Symbol.iterator]: function* () {
yield Hello
;
yield World
;
},
};
function Foo() {
return fooIterable;
}
const div = document.createElement('div');
const root = ReactDOMClient.createRoot(div);
await act(async () => {
root.render();
});
expect(div.textContent).toBe('HelloWorld');
await act(async () => {
root.render();
});
expect(div.textContent).toBe('HelloWorld');
});
it('should reorder bailed-out children', async () => {
class LetterInner extends React.Component {
render() {
return {this.props.char}
;
}
}
class Letter extends React.Component {
render() {
return ;
}
shouldComponentUpdate() {
return false;
}
}
class Letters extends React.Component {
render() {
const letters = this.props.letters.split('');
return (
{letters.map(c => (
))}
);
}
}
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
// Two random strings -- some additions, some removals, some moves
await act(async () => {
root.render();
});
expect(container.textContent).toBe('XKwHomsNjIkBcQWFbiZU');
await act(async () => {
root.render();
});
expect(container.textContent).toBe('EHCjpdTUuiybDvhRJwZt');
});
it('prepares new children before unmounting old', async () => {
const log = [];
class Spy extends React.Component {
UNSAFE_componentWillMount() {
log.push(this.props.name + ' componentWillMount');
}
render() {
log.push(this.props.name + ' render');
return ;
}
componentDidMount() {
log.push(this.props.name + ' componentDidMount');
}
componentWillUnmount() {
log.push(this.props.name + ' componentWillUnmount');
}
}
// These are reference-unequal so they will be swapped even if they have
// matching keys
const SpyA = props => ;
const SpyB = props => ;
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
await act(async () => {
root.render(
,
);
});
await act(async () => {
root.render(
,
);
});
expect(log).toEqual([
'oneA componentWillMount',
'oneA render',
'twoA componentWillMount',
'twoA render',
'oneA componentDidMount',
'twoA componentDidMount',
'oneB componentWillMount',
'oneB render',
'twoB componentWillMount',
'twoB render',
'oneA componentWillUnmount',
'twoA componentWillUnmount',
'oneB componentDidMount',
'twoB componentDidMount',
]);
});
});