'use strict';
let PropTypes;
let React;
let ReactDOM;
let ReactDOMClient;
let assertConsoleErrorDev;
let assertConsoleWarnDev;
describe('ReactES6Class', () => {
let container;
let root;
const freeze = function (expectation) {
Object.freeze(expectation);
return expectation;
};
let Inner;
let attachedListener = null;
let renderedName = null;
beforeEach(() => {
PropTypes = require('prop-types');
React = require('react');
ReactDOM = require('react-dom');
ReactDOMClient = require('react-dom/client');
({
assertConsoleErrorDev,
assertConsoleWarnDev,
} = require('internal-test-utils'));
container = document.createElement('div');
root = ReactDOMClient.createRoot(container);
attachedListener = null;
renderedName = null;
Inner = class extends React.Component {
getName() {
return this.props.name;
}
render() {
attachedListener = this.props.onClick;
renderedName = this.props.name;
return <div className={this.props.name} />;
}
};
});
function runTest(element, expectedTag, expectedClassName) {
ReactDOM.flushSync(() => root.render(element));
expect(container.firstChild).not.toBeNull();
expect(container.firstChild.tagName).toBe(expectedTag);
expect(container.firstChild.className).toBe(expectedClassName);
}
it('preserves the name of the class for use in error messages', () => {
class Foo extends React.Component {}
expect(Foo.name).toBe('Foo');
});
it('throws if no render function is defined', () => {
class Foo extends React.Component {}
const caughtErrors = [];
function errorHandler(event) {
event.preventDefault();
caughtErrors.push(event.error);
}
window.addEventListener('error', errorHandler);
try {
ReactDOM.flushSync(() => root.render(<Foo />));
assertConsoleErrorDev([
'No `render` method found on the Foo instance: ' +
'you may have forgotten to define `render`.\n' +
' in Foo (at **)',
'No `render` method found on the Foo instance: ' +
'you may have forgotten to define `render`.\n' +
' in Foo (at **)',
]);
} finally {
window.removeEventListener('error', errorHandler);
}
expect(caughtErrors).toEqual([
expect.objectContaining({
message: expect.stringContaining('is not a function'),
}),
]);
});
it('renders a simple stateless component with prop', () => {
class Foo extends React.Component {
render() {
return <Inner name={this.props.bar} />;
}
}
runTest(<Foo bar="foo" />, 'DIV', 'foo');
runTest(<Foo bar="bar" />, 'DIV', 'bar');
});
it('renders based on state using initial values in this.props', () => {
class Foo extends React.Component {
constructor(props) {
super(props);
this.state = {bar: this.props.initialValue};
}
render() {
return <span className={this.state.bar} />;
}
}
runTest(<Foo initialValue="foo" />, 'SPAN', 'foo');
});
it('renders based on state using props in the constructor', () => {
class Foo extends React.Component {
constructor(props) {
super(props);
this.state = {bar: props.initialValue};
}
changeState() {
this.setState({bar: 'bar'});
}
render() {
if (this.state.bar === 'foo') {
return <div className="foo" />;
}
return <span className={this.state.bar} />;
}
}
const ref = React.createRef();
runTest(<Foo initialValue="foo" ref={ref} />, 'DIV', 'foo');
ReactDOM.flushSync(() => ref.current.changeState());
runTest(<Foo />, 'SPAN', 'bar');
});
it('sets initial state with value returned by static getDerivedStateFromProps', () => {
class Foo extends React.Component {
state = {};
static getDerivedStateFromProps(nextProps, prevState) {
return {
foo: nextProps.foo,
bar: 'bar',
};
}
render() {
return <div className={`${this.state.foo} ${this.state.bar}`} />;
}
}
runTest(<Foo foo="foo" />, 'DIV', 'foo bar');
});
it('warns if getDerivedStateFromProps is not static', () => {
class Foo extends React.Component {
getDerivedStateFromProps() {
return {};
}
render() {
return <div />;
}
}
ReactDOM.flushSync(() => root.render(<Foo foo="foo" />));
assertConsoleErrorDev([
'Foo: getDerivedStateFromProps() is defined as an instance method ' +
'and will be ignored. Instead, declare it as a static method.\n' +
' in Foo (at **)',
]);
});
it('warns if getDerivedStateFromError is not static', () => {
class Foo extends React.Component {
getDerivedStateFromError() {
return {};
}
render() {
return <div />;
}
}
ReactDOM.flushSync(() => root.render(<Foo foo="foo" />));
assertConsoleErrorDev([
'Foo: getDerivedStateFromError() is defined as an instance method ' +
'and will be ignored. Instead, declare it as a static method.\n' +
' in Foo (at **)',
]);
});
it('warns if getSnapshotBeforeUpdate is static', () => {
class Foo extends React.Component {
static getSnapshotBeforeUpdate() {}
render() {
return <div />;
}
}
ReactDOM.flushSync(() => root.render(<Foo foo="foo" />));
assertConsoleErrorDev([
'Foo: getSnapshotBeforeUpdate() is defined as a static method ' +
'and will be ignored. Instead, declare it as an instance method.\n' +
' in Foo (at **)',
]);
});
it('warns if state not initialized before static getDerivedStateFromProps', () => {
class Foo extends React.Component {
static getDerivedStateFromProps(nextProps, prevState) {
return {
foo: nextProps.foo,
bar: 'bar',
};
}
render() {
return <div className={`${this.state.foo} ${this.state.bar}`} />;
}
}
ReactDOM.flushSync(() => root.render(<Foo foo="foo" />));
assertConsoleErrorDev([
'`Foo` uses `getDerivedStateFromProps` but its initial state is ' +
'undefined. This is not recommended. Instead, define the initial state by ' +
'assigning an object to `this.state` in the constructor of `Foo`. ' +
'This ensures that `getDerivedStateFromProps` arguments have a consistent shape.\n' +
' in Foo (at **)',
]);
});
it('updates initial state with values returned by static getDerivedStateFromProps', () => {
class Foo extends React.Component {
state = {
foo: 'foo',
bar: 'bar',
};
static getDerivedStateFromProps(nextProps, prevState) {
return {
foo: `not-${prevState.foo}`,
};
}
render() {
return <div className={`${this.state.foo} ${this.state.bar}`} />;
}
}
runTest(<Foo />, 'DIV', 'not-foo bar');
});
it('renders updated state with values returned by static getDerivedStateFromProps', () => {
class Foo extends React.Component {
state = {
value: 'initial',
};
static getDerivedStateFromProps(nextProps, prevState) {
if (nextProps.update) {
return {
value: 'updated',
};
}
return null;
}
render() {
return <div className={this.state.value} />;
}
}
runTest(<Foo update={false} />, 'DIV', 'initial');
runTest(<Foo update={true} />, 'DIV', 'updated');
});
if (!require('shared/ReactFeatureFlags').disableLegacyContext) {
it('renders based on context in the constructor', () => {
class Foo extends React.Component {
constructor(props, context) {
super(props, context);
this.state = {tag: context.tag, className: this.context.className};
}
render() {
const Tag = this.state.tag;
return <Tag className={this.state.className} />;
}
}
Foo.contextTypes = {
tag: PropTypes.string,
className: PropTypes.string,
};
class Outer extends React.Component {
getChildContext() {
return {tag: 'span', className: 'foo'};
}
render() {
return <Foo />;
}
}
Outer.childContextTypes = {
tag: PropTypes.string,
className: PropTypes.string,
};
runTest(<Outer />, 'SPAN', 'foo');
assertConsoleErrorDev([
'Outer uses the legacy childContextTypes API which will soon be removed. ' +
'Use React.createContext() instead. (https://react.dev/link/legacy-context)\n' +
' in Outer (at **)',
'Foo uses the legacy contextTypes API which will soon be removed. ' +
'Use React.createContext() with static contextType instead. (https://react.dev/link/legacy-context)\n' +
' in Outer (at **)',
]);
});
}
it('renders only once when setting state in componentWillMount', () => {
let renderCount = 0;
class Foo extends React.Component {
constructor(props) {
super(props);
this.state = {bar: props.initialValue};
}
UNSAFE_componentWillMount() {
this.setState({bar: 'bar'});
}
render() {
renderCount++;
return <span className={this.state.bar} />;
}
}
runTest(<Foo initialValue="foo" />, 'SPAN', 'bar');
expect(renderCount).toBe(1);
});
it('should warn with non-object in the initial state property', () => {
[['an array'], 'a string', 1234].forEach(function (state) {
class Foo extends React.Component {
constructor() {
super();
this.state = state;
}
render() {
return <span />;
}
}
runTest(<Foo />, 'SPAN', '');
assertConsoleErrorDev([
'Foo.state: must be set to an object or null\n in Foo (at **)',
]);
});
});
it('should render with null in the initial state property', () => {
class Foo extends React.Component {
constructor() {
super();
this.state = null;
}
render() {
return <span />;
}
}
runTest(<Foo />, 'SPAN', '');
});
it('setState through an event handler', () => {
class Foo extends React.Component {
constructor(props) {
super(props);
this.state = {bar: props.initialValue};
}
handleClick() {
this.setState({bar: 'bar'});
}
render() {
return (
<Inner name={this.state.bar} onClick={this.handleClick.bind(this)} />
);
}
}
runTest(<Foo initialValue="foo" />, 'DIV', 'foo');
ReactDOM.flushSync(() => attachedListener());
expect(renderedName).toBe('bar');
});
it('should not implicitly bind event handlers', () => {
class Foo extends React.Component {
constructor(props) {
super(props);
this.state = {bar: props.initialValue};
}
handleClick() {
this.setState({bar: 'bar'});
}
render() {
return <Inner name={this.state.bar} onClick={this.handleClick} />;
}
}
runTest(<Foo initialValue="foo" />, 'DIV', 'foo');
expect(attachedListener).toThrow();
});
it('renders using forceUpdate even when there is no state', () => {
class Foo extends React.Component {
constructor(props) {
super(props);
this.mutativeValue = props.initialValue;
}
handleClick() {
this.mutativeValue = 'bar';
this.forceUpdate();
}
render() {
return (
<Inner
name={this.mutativeValue}
onClick={this.handleClick.bind(this)}
/>
);
}
}
runTest(<Foo initialValue="foo" />, 'DIV', 'foo');
ReactDOM.flushSync(() => attachedListener());
expect(renderedName).toBe('bar');
});
it('will call all the normal life cycle methods', () => {
let lifeCycles = [];
class Foo extends React.Component {
constructor() {
super();
this.state = {};
}
UNSAFE_componentWillMount() {
lifeCycles.push('will-mount');
}
componentDidMount() {
lifeCycles.push('did-mount');
}
UNSAFE_componentWillReceiveProps(nextProps) {
lifeCycles.push('receive-props', nextProps);
}
shouldComponentUpdate(nextProps, nextState) {
lifeCycles.push('should-update', nextProps, nextState);
return true;
}
UNSAFE_componentWillUpdate(nextProps, nextState) {
lifeCycles.push('will-update', nextProps, nextState);
}
componentDidUpdate(prevProps, prevState) {
lifeCycles.push('did-update', prevProps, prevState);
}
componentWillUnmount() {
lifeCycles.push('will-unmount');
}
render() {
return <span className={this.props.value} />;
}
}
runTest(<Foo value="foo" />, 'SPAN', 'foo');
expect(lifeCycles).toEqual(['will-mount', 'did-mount']);
lifeCycles = [];
runTest(<Foo value="bar" />, 'SPAN', 'bar');
expect(lifeCycles).toEqual([
'receive-props', freeze({value: 'bar'}),
'should-update', freeze({value: 'bar'}), {},
'will-update', freeze({value: 'bar'}), {},
'did-update', freeze({value: 'foo'}), {},
]);
lifeCycles = [];
ReactDOM.flushSync(() => root.unmount());
expect(lifeCycles).toEqual(['will-unmount']);
});
if (!require('shared/ReactFeatureFlags').disableLegacyContext) {
it('warns when classic properties are defined on the instance, but does not invoke them.', () => {
let getDefaultPropsWasCalled = false;
let getInitialStateWasCalled = false;
class Foo extends React.Component {
constructor() {
super();
this.contextTypes = {};
this.contextType = {};
}
getInitialState() {
getInitialStateWasCalled = true;
return {};
}
getDefaultProps() {
getDefaultPropsWasCalled = true;
return {};
}
render() {
return <span className="foo" />;
}
}
runTest(<Foo />, 'SPAN', 'foo');
assertConsoleErrorDev([
'getInitialState was defined on Foo, a plain JavaScript class. ' +
'This is only supported for classes created using React.createClass. ' +
'Did you mean to define a state property instead?\n' +
' in Foo (at **)',
'getDefaultProps was defined on Foo, a plain JavaScript class. ' +
'This is only supported for classes created using React.createClass. ' +
'Use a static property to define defaultProps instead.\n' +
' in Foo (at **)',
'contextType was defined as an instance property on Foo. ' +
'Use a static property to define contextType instead.\n' +
' in Foo (at **)',
'contextTypes was defined as an instance property on Foo. ' +
'Use a static property to define contextTypes instead.\n' +
' in Foo (at **)',
]);
expect(getInitialStateWasCalled).toBe(false);
expect(getDefaultPropsWasCalled).toBe(false);
});
}
it('does not warn about getInitialState() on class components if state is also defined.', () => {
class Foo extends React.Component {
state = this.getInitialState();
getInitialState() {
return {};
}
render() {
return <span className="foo" />;
}
}
runTest(<Foo />, 'SPAN', 'foo');
});
it('should warn when misspelling shouldComponentUpdate', () => {
class NamedComponent extends React.Component {
componentShouldUpdate() {
return false;
}
render() {
return <span className="foo" />;
}
}
runTest(<NamedComponent />, 'SPAN', 'foo');
assertConsoleErrorDev([
'NamedComponent has a method called componentShouldUpdate(). Did you ' +
'mean shouldComponentUpdate()? The name is phrased as a question ' +
'because the function is expected to return a value.\n' +
' in NamedComponent (at **)',
]);
});
it('should warn when misspelling componentWillReceiveProps', () => {
class NamedComponent extends React.Component {
componentWillRecieveProps() {
return false;
}
render() {
return <span className="foo" />;
}
}
runTest(<NamedComponent />, 'SPAN', 'foo');
assertConsoleErrorDev([
'NamedComponent has a method called componentWillRecieveProps(). Did ' +
'you mean componentWillReceiveProps()?\n' +
' in NamedComponent (at **)',
]);
});
it('should warn when misspelling UNSAFE_componentWillReceiveProps', () => {
class NamedComponent extends React.Component {
UNSAFE_componentWillRecieveProps() {
return false;
}
render() {
return <span className="foo" />;
}
}
runTest(<NamedComponent />, 'SPAN', 'foo');
assertConsoleErrorDev([
'NamedComponent has a method called UNSAFE_componentWillRecieveProps(). ' +
'Did you mean UNSAFE_componentWillReceiveProps()?\n' +
' in NamedComponent (at **)',
]);
});
it('should throw AND warn when trying to access classic APIs', () => {
const ref = React.createRef();
runTest(<Inner name="foo" ref={ref} />, 'DIV', 'foo');
expect(() => ref.current.replaceState({})).toThrow();
assertConsoleWarnDev(
[
'replaceState(...) is deprecated in plain JavaScript React classes. ' +
'Refactor your code to use setState instead (see https://github.com/facebook/react/issues/3236).',
],
{withoutStack: true},
);
expect(() => ref.current.isMounted()).toThrow();
assertConsoleWarnDev(
[
'isMounted(...) is deprecated in plain JavaScript React classes. ' +
'Instead, make sure to clean up subscriptions and pending requests in ' +
'componentWillUnmount to prevent memory leaks.',
],
{withoutStack: true},
);
});
if (!require('shared/ReactFeatureFlags').disableLegacyContext) {
it('supports this.context passed via getChildContext', () => {
class Bar extends React.Component {
render() {
return <div className={this.context.bar} />;
}
}
Bar.contextTypes = {bar: PropTypes.string};
class Foo extends React.Component {
getChildContext() {
return {bar: 'bar-through-context'};
}
render() {
return <Bar />;
}
}
Foo.childContextTypes = {bar: PropTypes.string};
runTest(<Foo />, 'DIV', 'bar-through-context');
assertConsoleErrorDev([
'Foo uses the legacy childContextTypes API which will soon be removed. ' +
'Use React.createContext() instead. (https://react.dev/link/legacy-context)\n' +
' in Foo (at **)',
'Bar uses the legacy contextTypes API which will soon be removed. ' +
'Use React.createContext() with static contextType instead. (https://react.dev/link/legacy-context)\n' +
' in Foo (at **)',
]);
});
}
});