'use strict';
let act;
let assertConsoleErrorDev;
let assertConsoleWarnDev;
let PropTypes;
let React;
let ReactDOMClient;
let createReactClass;
describe('create-react-class-integration', () => {
beforeEach(() => {
jest.resetModules();
({
act,
assertConsoleErrorDev,
assertConsoleWarnDev,
} = require('internal-test-utils'));
PropTypes = require('prop-types');
React = require('react');
ReactDOMClient = require('react-dom/client');
createReactClass = require('create-react-class/factory')(
React.Component,
React.isValidElement,
new React.Component().updater,
);
});
it('should throw when `render` is not specified', () => {
expect(function () {
createReactClass({});
}).toThrowError('Class specification must implement a `render` method.');
});
it('should copy prop types onto the Constructor', () => {
const propValidator = jest.fn();
const TestComponent = createReactClass({
propTypes: {
value: propValidator,
},
render: function () {
return <div />;
},
});
expect(TestComponent.propTypes).toBeDefined();
expect(TestComponent.propTypes.value).toBe(propValidator);
});
it('should warn on invalid prop types', () => {
createReactClass({
displayName: 'Component',
propTypes: {
prop: null,
},
render: function () {
return <span>{this.props.prop}</span>;
},
});
assertConsoleErrorDev(
[
'Warning: Component: prop type `prop` is invalid; ' +
'it must be a function, usually from React.PropTypes.',
],
{withoutStack: true},
);
});
it('should warn on invalid context types', () => {
createReactClass({
displayName: 'Component',
contextTypes: {
prop: null,
},
render: function () {
return <span>{this.props.prop}</span>;
},
});
assertConsoleErrorDev(
[
'Warning: Component: context type `prop` is invalid; ' +
'it must be a function, usually from React.PropTypes.',
],
{withoutStack: true},
);
});
it('should throw on invalid child context types', () => {
createReactClass({
displayName: 'Component',
childContextTypes: {
prop: null,
},
render: function () {
return <span>{this.props.prop}</span>;
},
});
assertConsoleErrorDev(
[
'Warning: Component: child context type `prop` is invalid; it must be a function, usually from React.PropTypes.',
],
{withoutStack: true},
);
});
it('should warn when misspelling shouldComponentUpdate', () => {
createReactClass({
componentShouldUpdate: function () {
return false;
},
render: function () {
return <div />;
},
});
assertConsoleErrorDev(
[
'Warning: A component 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.',
],
{withoutStack: true},
);
createReactClass({
displayName: 'NamedComponent',
componentShouldUpdate: function () {
return false;
},
render: function () {
return <div />;
},
});
assertConsoleErrorDev(
[
'Warning: 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.',
],
{withoutStack: true},
);
});
it('should warn when misspelling componentWillReceiveProps', () => {
createReactClass({
componentWillRecieveProps: function () {
return false;
},
render: function () {
return <div />;
},
});
assertConsoleErrorDev(
[
'Warning: A component has a method called componentWillRecieveProps(). Did you ' +
'mean componentWillReceiveProps()?',
],
{withoutStack: true},
);
});
it('should warn when misspelling UNSAFE_componentWillReceiveProps', () => {
createReactClass({
UNSAFE_componentWillRecieveProps: function () {
return false;
},
render: function () {
return <div />;
},
});
assertConsoleErrorDev(
[
'Warning: A component has a method called UNSAFE_componentWillRecieveProps(). ' +
'Did you mean UNSAFE_componentWillReceiveProps()?',
],
{withoutStack: true},
);
});
it('should throw if a reserved property is in statics', () => {
expect(function () {
createReactClass({
statics: {
getDefaultProps: function () {
return {
foo: 0,
};
},
},
render: function () {
return <span />;
},
});
}).toThrowError(
'ReactClass: You are attempting to define a reserved property, ' +
'`getDefaultProps`, that shouldn\'t be on the "statics" key. Define ' +
'it as an instance property instead; it will still be accessible on ' +
'the constructor.',
);
});
it.skip('should warn when using deprecated non-static spec keys', () => {
createReactClass({
mixins: [{}],
propTypes: {
foo: PropTypes.string,
},
contextTypes: {
foo: PropTypes.string,
},
childContextTypes: {
foo: PropTypes.string,
},
render: function () {
return <div />;
},
});
assertConsoleErrorDev([
'`mixins` is now a static property and should ' +
'be defined inside "statics".',
'`propTypes` is now a static property and should ' +
'be defined inside "statics".',
'`contextTypes` is now a static property and ' +
'should be defined inside "statics".',
'`childContextTypes` is now a static property and ' +
'should be defined inside "statics".',
]);
});
it('should support statics', async () => {
const Component = createReactClass({
statics: {
abc: 'def',
def: 0,
ghi: null,
jkl: 'mno',
pqr: function () {
return this;
},
},
render: function () {
return <span />;
},
});
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
let instance;
await act(() => {
root.render(<Component ref={current => (instance = current)} />);
});
expect(instance.constructor.abc).toBe('def');
expect(Component.abc).toBe('def');
expect(instance.constructor.def).toBe(0);
expect(Component.def).toBe(0);
expect(instance.constructor.ghi).toBe(null);
expect(Component.ghi).toBe(null);
expect(instance.constructor.jkl).toBe('mno');
expect(Component.jkl).toBe('mno');
expect(instance.constructor.pqr()).toBe(Component);
expect(Component.pqr()).toBe(Component);
});
it('should work with object getInitialState() return values', async () => {
const Component = createReactClass({
getInitialState: function () {
return {
occupation: 'clown',
};
},
render: function () {
return <span />;
},
});
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
let instance;
await act(() => {
root.render(<Component ref={current => (instance = current)} />);
});
expect(instance.state.occupation).toEqual('clown');
});
it('should work with getDerivedStateFromProps() return values', async () => {
const Component = createReactClass({
getInitialState() {
return {};
},
render: function () {
return <span />;
},
});
Component.getDerivedStateFromProps = () => {
return {occupation: 'clown'};
};
let instance;
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
await act(() => {
root.render(<Component ref={current => (instance = current)} />);
});
expect(instance.state.occupation).toEqual('clown');
});
it('renders based on context getInitialState', async () => {
const Foo = createReactClass({
contextTypes: {
className: PropTypes.string,
},
getInitialState() {
return {className: this.context.className};
},
render() {
return <span className={this.state.className} />;
},
});
const Outer = createReactClass({
childContextTypes: {
className: PropTypes.string,
},
getChildContext() {
return {className: 'foo'};
},
render() {
return <Foo />;
},
});
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
await act(() => {
root.render(<Outer />);
});
assertConsoleErrorDev([
[
'Component uses the legacy childContextTypes API which will soon be removed. Use React.createContext() instead.',
{withoutStack: true},
],
'Component uses the legacy contextTypes API which will soon be removed. Use React.createContext() with static contextType instead.',
]);
expect(container.firstChild.className).toBe('foo');
});
it('should throw with non-object getInitialState() return values', async () => {
for (const state of [['an array'], 'a string', 1234]) {
const Component = createReactClass({
getInitialState: function () {
return state;
},
render: function () {
return <span />;
},
});
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
await expect(
act(() => {
root.render(<Component />);
}),
).rejects.toThrowError(
'Component.getInitialState(): must return an object or null',
);
}
});
it('should work with a null getInitialState() return value', async () => {
const Component = createReactClass({
getInitialState: function () {
return null;
},
render: function () {
return <span />;
},
});
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
await expect(
act(() => {
root.render(<Component />);
}),
).resolves.not.toThrow();
});
it('should throw when using legacy factories', () => {
const Component = createReactClass({
render() {
return <div />;
},
});
expect(() => Component()).toThrow();
assertConsoleErrorDev(
[
'Warning: Something is calling a React component directly. Use a ' +
'factory or JSX instead. See: https://fb.me/react-legacyfactory',
],
{withoutStack: true},
);
});
it('replaceState and callback works', async () => {
const ops = [];
const Component = createReactClass({
getInitialState() {
return {step: 0};
},
render() {
ops.push('Render: ' + this.state.step);
return <div />;
},
});
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
let instance;
await act(() => {
root.render(<Component ref={current => (instance = current)} />);
});
await act(() => {
instance.replaceState({step: 1}, () => {
ops.push('Callback: ' + instance.state.step);
});
});
expect(ops).toEqual(['Render: 0', 'Render: 1', 'Callback: 1']);
});
it('getDerivedStateFromProps updates state when props change', async () => {
const Component = createReactClass({
getInitialState() {
return {
count: 1,
};
},
render() {
return <div>count:{this.state.count}</div>;
},
});
Component.getDerivedStateFromProps = (nextProps, prevState) => ({
count: prevState.count + nextProps.incrementBy,
});
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
await act(() => {
root.render(
<div>
<Component incrementBy={0} />
</div>,
);
});
expect(container.firstChild.textContent).toEqual('count:1');
await act(() => {
root.render(
<div>
<Component incrementBy={2} />
</div>,
);
});
expect(container.firstChild.textContent).toEqual('count:3');
});
it('should support the new static getDerivedStateFromProps method', async () => {
let instance;
const Component = createReactClass({
statics: {
getDerivedStateFromProps: function () {
return {foo: 'bar'};
},
},
getInitialState() {
return {};
},
render: function () {
instance = this;
return null;
},
});
const root = ReactDOMClient.createRoot(document.createElement('div'));
await act(() => {
root.render(<Component />);
});
expect(instance.state.foo).toBe('bar');
});
it('warns if getDerivedStateFromProps is not static', async () => {
const Foo = createReactClass({
displayName: 'Foo',
getDerivedStateFromProps() {
return {};
},
render() {
return <div />;
},
});
const root = ReactDOMClient.createRoot(document.createElement('div'));
await act(() => {
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', async () => {
const Foo = createReactClass({
displayName: 'Foo',
getDerivedStateFromError() {
return {};
},
render() {
return <div />;
},
});
const root = ReactDOMClient.createRoot(document.createElement('div'));
await act(() => {
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', async () => {
const Foo = createReactClass({
displayName: 'Foo',
statics: {
getSnapshotBeforeUpdate: function () {
return null;
},
},
render() {
return <div />;
},
});
const root = ReactDOMClient.createRoot(document.createElement('div'));
await act(() => {
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('should warn if state is not properly initialized before getDerivedStateFromProps', async () => {
const Component = createReactClass({
displayName: 'Component',
statics: {
getDerivedStateFromProps: function () {
return null;
},
},
render: function () {
return null;
},
});
const root = ReactDOMClient.createRoot(document.createElement('div'));
await act(() => {
root.render(<Component />);
});
assertConsoleErrorDev([
'`Component` uses `getDerivedStateFromProps` but its initial state is ' +
'null. This is not recommended. Instead, define the initial state by ' +
'assigning an object to `this.state` in the constructor of `Component`. ' +
'This ensures that `getDerivedStateFromProps` arguments have a consistent shape.\n' +
' in Component (at **)',
]);
});
it('should not invoke deprecated lifecycles (cWM/cWRP/cWU) if new static gDSFP is present', async () => {
const Component = createReactClass({
statics: {
getDerivedStateFromProps: function () {
return null;
},
},
componentWillMount: function () {
throw Error('unexpected');
},
componentWillReceiveProps: function () {
throw Error('unexpected');
},
componentWillUpdate: function () {
throw Error('unexpected');
},
getInitialState: function () {
return {};
},
render: function () {
return null;
},
});
Component.displayName = 'Component';
const root = ReactDOMClient.createRoot(document.createElement('div'));
await act(() => {
root.render(<Component />);
});
assertConsoleErrorDev([
'Unsafe legacy lifecycles will not be called for components using new component APIs.\n\n' +
'Component uses getDerivedStateFromProps() but also contains the following legacy lifecycles:\n' +
' componentWillMount\n' +
' componentWillReceiveProps\n' +
' componentWillUpdate\n\n' +
'The above lifecycles should be removed. Learn more about this warning here:\n' +
'https://react.dev/link/unsafe-component-lifecycles\n' +
' in Component (at **)',
]);
assertConsoleWarnDev(
[
'componentWillMount has been renamed, and is not recommended for use. ' +
'See https://react.dev/link/unsafe-component-lifecycles for details.\n\n' +
'* Move code with side effects to componentDidMount, and set initial state in the constructor.\n' +
'* Rename componentWillMount to UNSAFE_componentWillMount to suppress ' +
'this warning in non-strict mode. In React 18.x, only the UNSAFE_ name will work. ' +
'To rename all deprecated lifecycles to their new names, you can run ' +
'`npx react-codemod rename-unsafe-lifecycles` in your project source folder.\n' +
'\nPlease update the following components: Component',
'componentWillReceiveProps has been renamed, and is not recommended for use. ' +
'See https://react.dev/link/unsafe-component-lifecycles for details.\n\n' +
'* Move data fetching code or side effects to componentDidUpdate.\n' +
"* If you're updating state whenever props change, refactor your " +
'code to use memoization techniques or move it to ' +
'static getDerivedStateFromProps. Learn more at: https://react.dev/link/derived-state\n' +
'* Rename componentWillReceiveProps to UNSAFE_componentWillReceiveProps to suppress ' +
'this warning in non-strict mode. In React 18.x, only the UNSAFE_ name will work. ' +
'To rename all deprecated lifecycles to their new names, you can run ' +
'`npx react-codemod rename-unsafe-lifecycles` in your project source folder.\n' +
'\nPlease update the following components: Component',
'componentWillUpdate has been renamed, and is not recommended for use. ' +
'See https://react.dev/link/unsafe-component-lifecycles for details.\n\n' +
'* Move data fetching code or side effects to componentDidUpdate.\n' +
'* Rename componentWillUpdate to UNSAFE_componentWillUpdate to suppress ' +
'this warning in non-strict mode. In React 18.x, only the UNSAFE_ name will work. ' +
'To rename all deprecated lifecycles to their new names, you can run ' +
'`npx react-codemod rename-unsafe-lifecycles` in your project source folder.\n' +
'\nPlease update the following components: Component',
],
{withoutStack: true},
);
await act(() => {
root.render(<Component foo={1} />);
});
});
it('should not invoke deprecated lifecycles (cWM/cWRP/cWU) if new getSnapshotBeforeUpdate is present', async () => {
const Component = createReactClass({
getSnapshotBeforeUpdate: function () {
return null;
},
componentWillMount: function () {
throw Error('unexpected');
},
componentWillReceiveProps: function () {
throw Error('unexpected');
},
componentWillUpdate: function () {
throw Error('unexpected');
},
componentDidUpdate: function () {},
render: function () {
return null;
},
});
Component.displayName = 'Component';
const root = ReactDOMClient.createRoot(document.createElement('div'));
await act(() => {
root.render(<Component />);
});
assertConsoleErrorDev([
'Unsafe legacy lifecycles will not be called for components using new component APIs.\n\n' +
'Component uses getSnapshotBeforeUpdate() but also contains the following legacy lifecycles:\n' +
' componentWillMount\n' +
' componentWillReceiveProps\n' +
' componentWillUpdate\n\n' +
'The above lifecycles should be removed. Learn more about this warning here:\n' +
'https://react.dev/link/unsafe-component-lifecycles\n' +
' in Component (at **)',
]);
assertConsoleWarnDev(
[
'componentWillMount has been renamed, and is not recommended for use. ' +
'See https://react.dev/link/unsafe-component-lifecycles for details.\n\n' +
'* Move code with side effects to componentDidMount, and set initial state in the constructor.\n' +
'* Rename componentWillMount to UNSAFE_componentWillMount to suppress ' +
'this warning in non-strict mode. In React 18.x, only the UNSAFE_ name will work. ' +
'To rename all deprecated lifecycles to their new names, you can run ' +
'`npx react-codemod rename-unsafe-lifecycles` in your project source folder.\n' +
'\nPlease update the following components: Component',
'componentWillReceiveProps has been renamed, and is not recommended for use. ' +
'See https://react.dev/link/unsafe-component-lifecycles for details.\n\n' +
'* Move data fetching code or side effects to componentDidUpdate.\n' +
"* If you're updating state whenever props change, refactor your " +
'code to use memoization techniques or move it to ' +
'static getDerivedStateFromProps. Learn more at: https://react.dev/link/derived-state\n' +
'* Rename componentWillReceiveProps to UNSAFE_componentWillReceiveProps to suppress ' +
'this warning in non-strict mode. In React 18.x, only the UNSAFE_ name will work. ' +
'To rename all deprecated lifecycles to their new names, you can run ' +
'`npx react-codemod rename-unsafe-lifecycles` in your project source folder.\n' +
'\nPlease update the following components: Component',
'componentWillUpdate has been renamed, and is not recommended for use. ' +
'See https://react.dev/link/unsafe-component-lifecycles for details.\n\n' +
'* Move data fetching code or side effects to componentDidUpdate.\n' +
'* Rename componentWillUpdate to UNSAFE_componentWillUpdate to suppress ' +
'this warning in non-strict mode. In React 18.x, only the UNSAFE_ name will work. ' +
'To rename all deprecated lifecycles to their new names, you can run ' +
'`npx react-codemod rename-unsafe-lifecycles` in your project source folder.\n' +
'\nPlease update the following components: Component',
],
{withoutStack: true},
);
await act(() => {
const root2 = ReactDOMClient.createRoot(document.createElement('div'));
root2.render(<Component foo={1} />);
});
});
it('should invoke both deprecated and new lifecycles if both are present', async () => {
const log = [];
const Component = createReactClass({
mixins: [
{
componentWillMount: function () {
log.push('componentWillMount');
},
componentWillReceiveProps: function () {
log.push('componentWillReceiveProps');
},
componentWillUpdate: function () {
log.push('componentWillUpdate');
},
},
],
UNSAFE_componentWillMount: function () {
log.push('UNSAFE_componentWillMount');
},
UNSAFE_componentWillReceiveProps: function () {
log.push('UNSAFE_componentWillReceiveProps');
},
UNSAFE_componentWillUpdate: function () {
log.push('UNSAFE_componentWillUpdate');
},
render: function () {
return null;
},
});
const root = ReactDOMClient.createRoot(document.createElement('div'));
await act(() => {
root.render(<Component foo="bar" />);
});
assertConsoleWarnDev(
[
'componentWillMount has been renamed, and is not recommended for use. ' +
'See https://react.dev/link/unsafe-component-lifecycles for details.\n\n' +
'* Move code with side effects to componentDidMount, and set initial state in the constructor.\n' +
'* Rename componentWillMount to UNSAFE_componentWillMount to suppress ' +
'this warning in non-strict mode. In React 18.x, only the UNSAFE_ name will work. ' +
'To rename all deprecated lifecycles to their new names, you can run ' +
'`npx react-codemod rename-unsafe-lifecycles` in your project source folder.\n' +
'\nPlease update the following components: Component',
'componentWillReceiveProps has been renamed, and is not recommended for use. ' +
'See https://react.dev/link/unsafe-component-lifecycles for details.\n\n' +
'* Move data fetching code or side effects to componentDidUpdate.\n' +
"* If you're updating state whenever props change, refactor your " +
'code to use memoization techniques or move it to ' +
'static getDerivedStateFromProps. Learn more at: https://react.dev/link/derived-state\n' +
'* Rename componentWillReceiveProps to UNSAFE_componentWillReceiveProps to suppress ' +
'this warning in non-strict mode. In React 18.x, only the UNSAFE_ name will work. ' +
'To rename all deprecated lifecycles to their new names, you can run ' +
'`npx react-codemod rename-unsafe-lifecycles` in your project source folder.\n' +
'\nPlease update the following components: Component',
'componentWillUpdate has been renamed, and is not recommended for use. ' +
'See https://react.dev/link/unsafe-component-lifecycles for details.\n\n' +
'* Move data fetching code or side effects to componentDidUpdate.\n' +
'* Rename componentWillUpdate to UNSAFE_componentWillUpdate to suppress ' +
'this warning in non-strict mode. In React 18.x, only the UNSAFE_ name will work. ' +
'To rename all deprecated lifecycles to their new names, you can run ' +
'`npx react-codemod rename-unsafe-lifecycles` in your project source folder.\n' +
'\nPlease update the following components: Component',
],
{withoutStack: true},
);
expect(log).toEqual(['componentWillMount', 'UNSAFE_componentWillMount']);
log.length = 0;
await act(() => {
root.render(<Component foo="baz" />);
});
expect(log).toEqual([
'componentWillReceiveProps',
'UNSAFE_componentWillReceiveProps',
'componentWillUpdate',
'UNSAFE_componentWillUpdate',
]);
});
it('isMounted works', async () => {
const ops = [];
let instance;
const Component = createReactClass({
displayName: 'MyComponent',
mixins: [
{
UNSAFE_componentWillMount() {
this.log('mixin.componentWillMount');
},
componentDidMount() {
this.log('mixin.componentDidMount');
},
UNSAFE_componentWillUpdate() {
this.log('mixin.componentWillUpdate');
},
componentDidUpdate() {
this.log('mixin.componentDidUpdate');
},
componentWillUnmount() {
this.log('mixin.componentWillUnmount');
},
},
],
log(name) {
ops.push(`${name}: ${this.isMounted()}`);
},
getInitialState() {
this.log('getInitialState');
return {};
},
UNSAFE_componentWillMount() {
this.log('componentWillMount');
},
componentDidMount() {
this.log('componentDidMount');
},
UNSAFE_componentWillUpdate() {
this.log('componentWillUpdate');
},
componentDidUpdate() {
this.log('componentDidUpdate');
},
componentWillUnmount() {
this.log('componentWillUnmount');
},
render() {
instance = this;
this.log('render');
return <div />;
},
});
const root = ReactDOMClient.createRoot(document.createElement('div'));
await act(() => {
root.render(<Component />);
});
assertConsoleErrorDev(
[
'Warning: MyComponent: isMounted is deprecated. Instead, make sure to ' +
'clean up subscriptions and pending requests in componentWillUnmount ' +
'to prevent memory leaks.\n' +
' in MyComponent (at **)',
],
);
await act(() => {
root.render(<Component />);
});
await act(() => {
root.unmount();
});
instance.log('after unmount');
expect(ops).toEqual([
'getInitialState: false',
'mixin.componentWillMount: false',
'componentWillMount: false',
'render: false',
'mixin.componentDidMount: true',
'componentDidMount: true',
'mixin.componentWillUpdate: true',
'componentWillUpdate: true',
'render: true',
'mixin.componentDidUpdate: true',
'componentDidUpdate: true',
'mixin.componentWillUnmount: true',
'componentWillUnmount: true',
'after unmount: false',
]);
});
});