From 838ac070f9a59bfcecc843fcd8189e0b398573c0 Mon Sep 17 00:00:00 2001 From: Siarhei Startsau Date: Thu, 9 Mar 2023 23:39:30 +0100 Subject: [PATCH] [New] `no-deprecated`: add React 18 deprecations --- CHANGELOG.md | 2 + docs/rules/no-deprecated.md | 25 +++++- lib/rules/no-deprecated.js | 25 ++++++ tests/lib/rules/no-deprecated.js | 126 ++++++++++++++++++++++++++++++- 4 files changed, 175 insertions(+), 3 deletions(-) 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 } + ), + ], + }, ]), });