'use strict';
let React;
let ReactDOM;
let findDOMNode;
let ReactDOMClient;
let TogglingComponent;
let act;
let Scheduler;
let assertLog;
let container;
describe('ReactEmptyComponent', () => {
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;
Scheduler = require('scheduler');
const InternalTestUtils = require('internal-test-utils');
act = InternalTestUtils.act;
assertLog = InternalTestUtils.assertLog;
container = document.createElement('div');
TogglingComponent = class extends React.Component {
state = {component: this.props.firstComponent};
componentDidMount() {
Scheduler.log('mount ' + findDOMNode(this)?.nodeName);
this.setState({component: this.props.secondComponent});
}
componentDidUpdate() {
Scheduler.log('update ' + findDOMNode(this)?.nodeName);
}
render() {
const Component = this.state.component;
return Component ? <Component /> : null;
}
};
});
describe.each([null, undefined])('when %s', nullORUndefined => {
it('should not throw when rendering', () => {
function EmptyComponent() {
return nullORUndefined;
}
const root = ReactDOMClient.createRoot(container);
expect(() => {
ReactDOM.flushSync(() => {
root.render(<EmptyComponent />);
});
}).not.toThrowError();
});
it('should not produce child DOM nodes for nullish and false', async () => {
function Component1() {
return nullORUndefined;
}
function Component2() {
return false;
}
const container1 = document.createElement('div');
const root1 = ReactDOMClient.createRoot(container1);
await act(() => {
root1.render(<Component1 />);
});
expect(container1.children.length).toBe(0);
const container2 = document.createElement('div');
const root2 = ReactDOMClient.createRoot(container2);
await act(() => {
root2.render(<Component2 />);
});
expect(container2.children.length).toBe(0);
});
it('should be able to switch between rendering nullish and a normal tag', async () => {
const instance1 = (
<TogglingComponent
firstComponent={nullORUndefined}
secondComponent={'div'}
/>
);
const instance2 = (
<TogglingComponent
firstComponent={'div'}
secondComponent={nullORUndefined}
/>
);
const container2 = document.createElement('div');
const root1 = ReactDOMClient.createRoot(container);
await act(() => {
root1.render(instance1);
});
assertLog(['mount undefined', 'update DIV']);
const root2 = ReactDOMClient.createRoot(container2);
await act(() => {
root2.render(instance2);
});
assertLog(['mount DIV', 'update undefined']);
});
it('should be able to switch in a list of children', async () => {
const instance1 = (
<TogglingComponent
firstComponent={nullORUndefined}
secondComponent={'div'}
/>
);
const root = ReactDOMClient.createRoot(container);
await act(() => {
root.render(
<div>
{instance1}
{instance1}
{instance1}
</div>,
);
});
assertLog([
'mount undefined',
'mount undefined',
'mount undefined',
'update DIV',
'update DIV',
'update DIV',
]);
});
it('should distinguish between a script placeholder and an actual script tag', () => {
const instance1 = (
<TogglingComponent
firstComponent={nullORUndefined}
secondComponent={'script'}
/>
);
const instance2 = (
<TogglingComponent
firstComponent={'script'}
secondComponent={nullORUndefined}
/>
);
const root1 = ReactDOMClient.createRoot(container);
expect(() => {
ReactDOM.flushSync(() => {
root1.render(instance1);
});
}).not.toThrow();
const container2 = document.createElement('div');
const root2 = ReactDOMClient.createRoot(container2);
expect(() => {
ReactDOM.flushSync(() => {
root2.render(instance2);
});
}).not.toThrow();
assertLog([
'mount undefined',
'update SCRIPT',
'mount SCRIPT',
'update undefined',
]);
});
it(
'should have findDOMNode return null when multiple layers of composite ' +
'components render to the same nullish placeholder',
() => {
function GrandChild() {
return nullORUndefined;
}
function Child() {
return <GrandChild />;
}
const instance1 = (
<TogglingComponent firstComponent={'div'} secondComponent={Child} />
);
const instance2 = (
<TogglingComponent firstComponent={Child} secondComponent={'div'} />
);
const root1 = ReactDOMClient.createRoot(container);
expect(() => {
ReactDOM.flushSync(() => {
root1.render(instance1);
});
}).not.toThrow();
const container2 = document.createElement('div');
const root2 = ReactDOMClient.createRoot(container2);
expect(() => {
ReactDOM.flushSync(() => {
root2.render(instance2);
});
}).not.toThrow();
assertLog([
'mount DIV',
'update undefined',
'mount undefined',
'update DIV',
]);
},
);
it('works when switching components', async () => {
let innerRef;
class Inner extends React.Component {
render() {
return <span />;
}
componentDidMount() {
expect(findDOMNode(this)).not.toBe(null);
}
componentWillUnmount() {
expect(findDOMNode(this)).not.toBe(null);
}
}
function Wrapper({showInner}) {
innerRef = React.createRef(null);
return showInner ? <Inner ref={innerRef} /> : nullORUndefined;
}
const el = document.createElement('div');
const root = ReactDOMClient.createRoot(el);
await act(() => {
root.render(<Wrapper showInner={true} />);
});
expect(innerRef.current).not.toBe(null);
await act(() => {
root.render(<Wrapper showInner={false} />);
});
expect(innerRef.current).toBe(null);
await act(() => {
root.render(<Wrapper showInner={true} />);
});
expect(innerRef.current).not.toBe(null);
expect.assertions(6);
});
it('can render nullish at the top level', async () => {
const div = document.createElement('div');
const root = ReactDOMClient.createRoot(div);
await act(() => {
root.render(nullORUndefined);
});
expect(div.innerHTML).toBe('');
});
it('does not break when updating during mount', () => {
class Child extends React.Component {
componentDidMount() {
if (this.props.onMount) {
this.props.onMount();
}
}
render() {
if (!this.props.visible) {
return nullORUndefined;
}
return <div>hello world</div>;
}
}
class Parent extends React.Component {
update = () => {
this.forceUpdate();
};
render() {
return (
<div>
<Child key="1" visible={false} />
<Child key="0" visible={true} onMount={this.update} />
<Child key="2" visible={false} />
</div>
);
}
}
const root = ReactDOMClient.createRoot(container);
expect(() => {
ReactDOM.flushSync(() => {
root.render(<Parent />);
});
}).not.toThrow();
});
it('preserves the dom node during updates', async () => {
function Empty() {
return nullORUndefined;
}
const root = ReactDOMClient.createRoot(container);
await act(() => {
root.render(<Empty />);
});
const noscript1 = container.firstChild;
expect(noscript1).toBe(null);
await act(() => {
root.render(<Empty />);
});
const noscript2 = container.firstChild;
expect(noscript2).toBe(null);
});
it('should not warn about React.forwardRef that returns nullish', () => {
const Empty = () => {
return nullORUndefined;
};
const EmptyForwardRef = React.forwardRef(Empty);
const root = ReactDOMClient.createRoot(container);
expect(() => {
ReactDOM.flushSync(() => {
root.render(<EmptyForwardRef />);
});
}).not.toThrowError();
});
it('should not warn about React.memo that returns nullish', () => {
const Empty = () => {
return nullORUndefined;
};
const EmptyMemo = React.memo(Empty);
const root = ReactDOMClient.createRoot(container);
expect(() => {
ReactDOM.flushSync(() => {
root.render(<EmptyMemo />);
});
}).not.toThrowError();
});
});
});