diff --git a/CHANGELOG.md b/CHANGELOG.md
index a2eed5f339..ebc0fec0ce 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -8,12 +8,14 @@ This change log adheres to standards from [Keep a CHANGELOG](https://keepachange
### Added
* [`display-name`]: add `checkContextObjects` option ([#3529][] @JulesBlm)
* [`jsx-first-prop-new-line`]: add `multiprop` option ([#3533][] @haydncomley)
+* [`no-deprecated`]: add React 18 deprecations ([#3548][] @sergei-startsev)
### Fixed
* [`no-array-index-key`]: consider flatMap ([#3530][] @k-yle)
* [`jsx-curly-brace-presence`]: handle single and only expression template literals ([#3538][] @taozhou-glean)
* [`no-unknown-property`]: allow `onLoad` on `source` (@ljharb)
+[#3548]: https://github.com/jsx-eslint/eslint-plugin-react/pull/3548
[#3538]: https://github.com/jsx-eslint/eslint-plugin-react/pull/3538
[#3533]: https://github.com/jsx-eslint/eslint-plugin-react/pull/3533
[#3530]: https://github.com/jsx-eslint/eslint-plugin-react/pull/3530
diff --git a/docs/rules/no-deprecated.md b/docs/rules/no-deprecated.md
index 030efb870e..b7a3326456 100644
--- a/docs/rules/no-deprecated.md
+++ b/docs/rules/no-deprecated.md
@@ -36,14 +36,28 @@ import React, { PropTypes } from 'react';
componentWillMount() { }
componentWillReceiveProps() { }
componentWillUpdate() { }
+
+// React 18 deprecations
+import { render } from 'react-dom';
+ReactDOM.render(
, container);
+
+import { hydrate } from 'react-dom';
+ReactDOM.hydrate(, container);
+
+import {unmountComponentAtNode} from 'react-dom';
+ReactDOM.unmountComponentAtNode(container);
+
+import { renderToNodeStream } from 'react-dom/server';
+ReactDOMServer.renderToNodeStream(element);
```
Examples of **correct** code for this rule:
```jsx
+// when React < 18
ReactDOM.render(, root);
-// When [1, {"react": "0.13.0"}]
+// when React is < 0.14
ReactDOM.findDOMNode(this.refs.foo);
import { PropTypes } from 'prop-types';
@@ -51,4 +65,13 @@ import { PropTypes } from 'prop-types';
UNSAFE_componentWillMount() { }
UNSAFE_componentWillReceiveProps() { }
UNSAFE_componentWillUpdate() { }
+
+ReactDOM.createPortal(child, container);
+
+import { createRoot } from 'react-dom/client';
+const root = createRoot(container);
+root.unmount();
+
+import { hydrateRoot } from 'react-dom/client';
+const root = hydrateRoot(container, );
```
diff --git a/lib/rules/no-deprecated.js b/lib/rules/no-deprecated.js
index 8c99314d0e..877680658a 100644
--- a/lib/rules/no-deprecated.js
+++ b/lib/rules/no-deprecated.js
@@ -22,6 +22,8 @@ const report = require('../util/report');
const MODULES = {
react: ['React'],
'react-addons-perf': ['ReactPerf', 'Perf'],
+ 'react-dom': ['ReactDOM'],
+ 'react-dom/server': ['ReactDOMServer'],
};
// ------------------------------------------------------------------------------
@@ -82,6 +84,29 @@ function getDeprecated(pragma) {
'https://reactjs.org/docs/react-component.html#unsafe_componentwillupdate. '
+ 'Use https://github.com/reactjs/react-codemod#rename-unsafe-lifecycles to automatically update your components.',
];
+ // 18.0.0
+ // https://reactjs.org/blog/2022/03/08/react-18-upgrade-guide.html#deprecations
+ deprecated['ReactDOM.render'] = [
+ '18.0.0',
+ 'createRoot',
+ 'https://reactjs.org/link/switch-to-createroot',
+ ];
+ deprecated['ReactDOM.hydrate'] = [
+ '18.0.0',
+ 'hydrateRoot',
+ 'https://reactjs.org/link/switch-to-createroot',
+ ];
+ deprecated['ReactDOM.unmountComponentAtNode'] = [
+ '18.0.0',
+ 'root.unmount',
+ 'https://reactjs.org/link/switch-to-createroot',
+ ];
+ deprecated['ReactDOMServer.renderToNodeStream'] = [
+ '18.0.0',
+ 'renderToPipeableStream',
+ 'https://reactjs.org/docs/react-dom-server.html#rendertonodestream',
+ ];
+
return deprecated;
}
diff --git a/tests/lib/rules/no-deprecated.js b/tests/lib/rules/no-deprecated.js
index bf0e5bd7c3..dfea11f833 100644
--- a/tests/lib/rules/no-deprecated.js
+++ b/tests/lib/rules/no-deprecated.js
@@ -48,9 +48,9 @@ ruleTester.run('no-deprecated', rule, {
// Not deprecated
'var element = React.createElement(\'p\', {}, null);',
'var clone = React.cloneElement(element);',
- 'ReactDOM.render(element, container);',
- 'ReactDOM.unmountComponentAtNode(container);',
+ 'ReactDOM.cloneElement(child, container);',
'ReactDOM.findDOMNode(instance);',
+ 'ReactDOM.createPortal(child, container);',
'ReactDOMServer.renderToString(element);',
'ReactDOMServer.renderToStaticMarkup(element);',
{
@@ -119,6 +119,40 @@ ruleTester.run('no-deprecated', rule, {
let { default: defaultReactExport, ...allReactExports } = React;
`,
},
+ // React < 18
+ {
+ code: `
+ import { render, hydrate } from 'react-dom';
+ import { renderToNodeStream } from 'react-dom/server';
+ ReactDOM.render(element, container);
+ ReactDOM.unmountComponentAtNode(container);
+ ReactDOMServer.renderToNodeStream(element);
+ `,
+ settings: { react: { version: '17.999.999' } },
+ },
+ // React 18 API
+ {
+ code: `
+ import ReactDOM, { createRoot } from 'react-dom/client';
+ ReactDOM.createRoot(container);
+ const root = createRoot(container);
+ root.unmount();
+ `,
+ },
+ {
+ code: `
+ import ReactDOM, { hydrateRoot } from 'react-dom/client';
+ ReactDOM.hydrateRoot(container, );
+ hydrateRoot(container, );
+ `,
+ },
+ {
+ code: `
+ import ReactDOMServer, { renderToPipeableStream } from 'react-dom/server';
+ ReactDOMServer.renderToPipeableStream(, {});
+ renderToPipeableStream(, {});
+ `,
+ },
]),
invalid: parsers.all([
@@ -454,5 +488,93 @@ ruleTester.run('no-deprecated', rule, {
),
],
},
+ {
+ code: `
+ import { render } from 'react-dom';
+ ReactDOM.render(, container);
+ `,
+ errors: [
+ errorMessage(
+ 'ReactDOM.render',
+ '18.0.0',
+ 'createRoot',
+ 'https://reactjs.org/link/switch-to-createroot',
+ { type: 'ImportDeclaration', line: 2, column: 9 }
+ ),
+ errorMessage(
+ 'ReactDOM.render',
+ '18.0.0',
+ 'createRoot',
+ 'https://reactjs.org/link/switch-to-createroot',
+ { type: 'MemberExpression', line: 3, column: 9 }
+ ),
+ ],
+ },
+ {
+ code: `
+ import { hydrate } from 'react-dom';
+ ReactDOM.hydrate(, container);
+ `,
+ errors: [
+ errorMessage(
+ 'ReactDOM.hydrate',
+ '18.0.0',
+ 'hydrateRoot',
+ 'https://reactjs.org/link/switch-to-createroot',
+ { type: 'ImportDeclaration', line: 2, column: 9 }
+ ),
+ errorMessage(
+ 'ReactDOM.hydrate',
+ '18.0.0',
+ 'hydrateRoot',
+ 'https://reactjs.org/link/switch-to-createroot',
+ { type: 'MemberExpression', line: 3, column: 9 }
+ ),
+ ],
+ },
+ {
+ code: `
+ import { unmountComponentAtNode } from 'react-dom';
+ ReactDOM.unmountComponentAtNode(container);
+ `,
+ errors: [
+ errorMessage(
+ 'ReactDOM.unmountComponentAtNode',
+ '18.0.0',
+ 'root.unmount',
+ 'https://reactjs.org/link/switch-to-createroot',
+ { type: 'ImportDeclaration', line: 2, column: 9 }
+ ),
+ errorMessage(
+ 'ReactDOM.unmountComponentAtNode',
+ '18.0.0',
+ 'root.unmount',
+ 'https://reactjs.org/link/switch-to-createroot',
+ { type: 'MemberExpression', line: 3, column: 9 }
+ ),
+ ],
+ },
+ {
+ code: `
+ import { renderToNodeStream } from 'react-dom/server';
+ ReactDOMServer.renderToNodeStream(element);
+ `,
+ errors: [
+ errorMessage(
+ 'ReactDOMServer.renderToNodeStream',
+ '18.0.0',
+ 'renderToPipeableStream',
+ 'https://reactjs.org/docs/react-dom-server.html#rendertonodestream',
+ { type: 'ImportDeclaration', line: 2, column: 9 }
+ ),
+ errorMessage(
+ 'ReactDOMServer.renderToNodeStream',
+ '18.0.0',
+ 'renderToPipeableStream',
+ 'https://reactjs.org/docs/react-dom-server.html#rendertonodestream',
+ { type: 'MemberExpression', line: 3, column: 9 }
+ ),
+ ],
+ },
]),
});