diff --git a/.circleci/config.yml b/.circleci/config.yml index 584f8c111327e..ea8f6458f96bb 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -11,9 +11,9 @@ aliases: restore_cache: name: Restore node_modules cache keys: - - v1-node-{{ arch }}-{{ .Branch }}-{{ checksum "yarn.lock" }} - - v1-node-{{ arch }}-{{ .Branch }}- - - v1-node-{{ arch }}- + - v2-node-{{ arch }}-{{ .Branch }}-{{ checksum "yarn.lock" }} + - v2-node-{{ arch }}-{{ .Branch }}- + - v2-node-{{ arch }}- - &run_yarn run: name: Install Packages @@ -62,7 +62,7 @@ jobs: - *run_yarn - save_cache: name: Save node_modules cache - key: v1-node-{{ arch }}-{{ .Branch }}-{{ checksum "yarn.lock" }} + key: v2-node-{{ arch }}-{{ .Branch }}-{{ checksum "yarn.lock" }} paths: - ~/.cache/yarn @@ -141,6 +141,18 @@ jobs: RELEASE_CHANNEL: stable command: yarn test-prod --maxWorkers=2 + test_source_prod_experimental: + docker: *docker + environment: *environment + steps: + - checkout + - *restore_yarn_cache + - *run_yarn + - run: + environment: + RELEASE_CHANNEL: experimental + command: yarn test-prod --maxWorkers=2 + build: docker: *docker environment: *environment @@ -400,6 +412,9 @@ workflows: - test_source_experimental: requires: - setup + - test_source_prod_experimental: + requires: + - setup - build_experimental: requires: - setup diff --git a/.eslintrc.js b/.eslintrc.js index 74b87ab13a679..b10d30525c198 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -5,6 +5,8 @@ const { esNextPaths, } = require('./scripts/shared/pathsByLanguageVersion'); +const restrictedGlobals = require('confusing-browser-globals'); + const OFF = 0; const ERROR = 2; @@ -45,6 +47,7 @@ module.exports = { 'no-bitwise': OFF, 'no-inner-declarations': [ERROR, 'functions'], 'no-multi-spaces': ERROR, + 'no-restricted-globals': [ERROR].concat(restrictedGlobals), 'no-restricted-syntax': [ERROR, 'WithStatement'], 'no-shadow': ERROR, 'no-unused-expressions': ERROR, diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000000000..e5bb31b2b3787 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,7 @@ +contact_links: + - name: 📃 Documentation Issue + url: https://github.com/reactjs/reactjs.org/issues/new + about: This issue tracker is not for documentation issues. Please file documentation issues here. + - name: 🤔 Questions and Help + url: https://reactjs.org/community/support.html + about: This issue tracker is not for support questions. Please refer to the React community's help and discussion forums. diff --git a/.github/ISSUE_TEMPLATE/documentation.md b/.github/ISSUE_TEMPLATE/documentation.md deleted file mode 100644 index 57c380e2ac97a..0000000000000 --- a/.github/ISSUE_TEMPLATE/documentation.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -name: "📃 Documentation Issue" -about: This issue tracker is not for documentation issues. Please file documentation issues at https://github.com/reactjs/reactjs.org. -title: 'Docs: ' -labels: 'Resolution: Invalid' - ---- - -🚨 This issue tracker is not for documentation issues. 🚨 - -The React website is hosted on a separate repository. You may let the -team know about any issues with the documentation by opening an issue there: -- https://github.com/reactjs/reactjs.org/issues/new diff --git a/.github/ISSUE_TEMPLATE/question.md b/.github/ISSUE_TEMPLATE/question.md deleted file mode 100644 index 0131925d2c7a3..0000000000000 --- a/.github/ISSUE_TEMPLATE/question.md +++ /dev/null @@ -1,29 +0,0 @@ ---- -name: "🤔 Questions and Help" -about: This issue tracker is not for questions. Please ask questions at https://stackoverflow.com/questions/tagged/react. -title: 'Question: ' -labels: 'Resolution: Invalid, Type: Question' - ---- - -🚨 This issue tracker is not for questions. 🚨 - -As it happens, support requests that are created as issues are likely to be closed. We want to make sure you are able to find the help you seek. Please take a look at the following resources. - -## Coding Questions - -If you have a coding question related to React and React DOM, it might be better suited for Stack Overflow. It's a great place to browse through frequent questions about using React, as well as ask for help with specific questions. - -https://stackoverflow.com/questions/tagged/react - -## Talk to other React developers - -There are many online forums which are a great place for discussion about best practices and application architecture as well as the future of React. - -https://reactjs.org/community/support.html#popular-discussion-forums - -## Proposals - -If you'd like to discuss topics related to the future of React, or would like to propose a new feature or change before sending a pull request, please check out the discussions and proposals repository. - -https://github.com/reactjs/rfcs diff --git a/CHANGELOG.md b/CHANGELOG.md index e8eb19c9d8927..9dc7136ed8481 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,10 +1,32 @@ -## [Unreleased] -
- - Changes that have landed in master but are not yet released. - Click to see more. - -
+## 16.13.0 (February 26, 2020) + +### React + +* Warn when a string ref is used in a manner that's not amenable to a future codemod ([@lunaruan](https://github.com/lunaruan) in [#17864](https://github.com/facebook/react/pull/17864)) +* Deprecate `React.createFactory()` ([@trueadm](https://github.com/trueadm) in [#17878](https://github.com/facebook/react/pull/17878)) + +### React DOM + +* Warn when changes in `style` may cause an unexpected collision ([@sophiebits](https://github.com/sophiebits) in [#14181](https://github.com/facebook/react/pull/14181), [#18002](https://github.com/facebook/react/pull/18002)) +* Warn when a function component is updated during another component's render phase ([@acdlite](https://github.com/acdlite) in [#17099](https://github.com/facebook/react/pull/17099)) +* Deprecate `unstable_createPortal` ([@trueadm](https://github.com/trueadm) in [#17880](https://github.com/facebook/react/pull/17880)) +* Fix `onMouseEnter` being fired on disabled buttons ([@AlfredoGJ](https://github.com/AlfredoGJ) in [#17675](https://github.com/facebook/react/pull/17675)) +* Call `shouldComponentUpdate` twice when developing in `StrictMode` ([@bvaughn](https://github.com/bvaughn) in [#17942](https://github.com/facebook/react/pull/17942)) +* Add `version` property to ReactDOM ([@ealush](https://github.com/ealush) in [#15780](https://github.com/facebook/react/pull/15780)) +* Don't call `toString()` of `dangerouslySetInnerHTML` ([@sebmarkbage](https://github.com/sebmarkbage) in [#17773](https://github.com/facebook/react/pull/17773)) +* Show component stacks in more warnings ([@gaearon](https://github.com/gaearon) in [#17922](https://github.com/facebook/react/pull/17922), [#17586](https://github.com/facebook/react/pull/17586)) + +### Concurrent Mode (Experimental) + +* Warn for problematic usages of `ReactDOM.createRoot()` ([@trueadm](https://github.com/trueadm) in [#17937](https://github.com/facebook/react/pull/17937)) +* Remove `ReactDOM.createRoot()` callback params and added warnings on usage ([@bvaughn](https://github.com/bvaughn) in [#17916](https://github.com/facebook/react/pull/17916)) +* Don't group Idle/Offscreen work with other work ([@sebmarkbage](https://github.com/sebmarkbage) in [#17456](https://github.com/facebook/react/pull/17456)) +* Adjust `SuspenseList` CPU bound heuristic ([@sebmarkbage](https://github.com/sebmarkbage) in [#17455](https://github.com/facebook/react/pull/17455)) +* Add missing event plugin priorities ([@trueadm](https://github.com/trueadm) in [#17914](https://github.com/facebook/react/pull/17914)) +* Fix `isPending` only being true when transitioning from inside an input event ([@acdlite](https://github.com/acdlite) in [#17382](https://github.com/facebook/react/pull/17382)) +* Fix `React.memo` components dropping updates when interrupted by a higher priority update ([@acdlite]((https://github.com/acdlite)) in [#18091](https://github.com/facebook/react/pull/18091)) +* Don't warn when suspending at the wrong priority ([@gaearon](https://github.com/gaearon) in [#17971](https://github.com/facebook/react/pull/17971)) +* Fix a bug with rebasing updates ([@acdlite](https://github.com/acdlite) and [@sebmarkbage](https://github.com/sebmarkbage) in [#17560](https://github.com/facebook/react/pull/17560), [#17510](https://github.com/facebook/react/pull/17510), [#17483](https://github.com/facebook/react/pull/17483), [#17480](https://github.com/facebook/react/pull/17480)) ## 16.12.0 (November 14, 2019) diff --git a/fixtures/dom/src/__tests__/nested-act-test.js b/fixtures/dom/src/__tests__/nested-act-test.js index c7a191943abdf..4a39a0ea98f7f 100644 --- a/fixtures/dom/src/__tests__/nested-act-test.js +++ b/fixtures/dom/src/__tests__/nested-act-test.js @@ -8,24 +8,21 @@ */ let React; -let ReactDOM; +let DOMAct; let TestRenderer; +let TestAct; global.__DEV__ = process.env.NODE_ENV !== 'production'; -jest.mock('react-dom', () => - require.requireActual('react-dom/cjs/react-dom-testing.development.js') -); -// we'll replace the above with react/testing and react-dom/testing right before the next minor - expect.extend(require('../toWarnDev')); describe('unmocked scheduler', () => { beforeEach(() => { jest.resetModules(); React = require('react'); - ReactDOM = require('react-dom'); + DOMAct = require('react-dom/test-utils').act; TestRenderer = require('react-test-renderer'); + TestAct = TestRenderer.act; }); it('flushes work only outside the outermost act() corresponding to its own renderer', () => { @@ -37,8 +34,8 @@ describe('unmocked scheduler', () => { return null; } // in legacy mode, this tests whether an act only flushes its own effects - TestRenderer.act(() => { - ReactDOM.act(() => { + TestAct(() => { + DOMAct(() => { TestRenderer.create(); }); expect(log).toEqual([]); @@ -47,8 +44,8 @@ describe('unmocked scheduler', () => { log = []; // for doublechecking, we flip it inside out, and assert on the outermost - ReactDOM.act(() => { - TestRenderer.act(() => { + DOMAct(() => { + TestAct(() => { TestRenderer.create(); }); expect(log).toEqual(['called']); @@ -64,8 +61,9 @@ describe('mocked scheduler', () => { require.requireActual('scheduler/unstable_mock') ); React = require('react'); - ReactDOM = require('react-dom'); + DOMAct = require('react-dom/test-utils').act; TestRenderer = require('react-test-renderer'); + TestAct = TestRenderer.act; }); afterEach(() => { @@ -81,8 +79,8 @@ describe('mocked scheduler', () => { return null; } // with a mocked scheduler, this tests whether it flushes all work only on the outermost act - TestRenderer.act(() => { - ReactDOM.act(() => { + TestAct(() => { + DOMAct(() => { TestRenderer.create(); }); expect(log).toEqual([]); @@ -91,8 +89,8 @@ describe('mocked scheduler', () => { log = []; // for doublechecking, we flip it inside out, and assert on the outermost - ReactDOM.act(() => { - TestRenderer.act(() => { + DOMAct(() => { + TestAct(() => { TestRenderer.create(); }); expect(log).toEqual([]); diff --git a/fixtures/dom/src/__tests__/wrong-act-test.js b/fixtures/dom/src/__tests__/wrong-act-test.js index 10df65ce6df60..38029be9b9cae 100644 --- a/fixtures/dom/src/__tests__/wrong-act-test.js +++ b/fixtures/dom/src/__tests__/wrong-act-test.js @@ -10,6 +10,7 @@ let React; let ReactDOM; let ReactART; +let TestUtils; let ARTSVGMode; let ARTCurrentMode; let TestRenderer; @@ -18,11 +19,6 @@ let ARTTest; global.__DEV__ = process.env.NODE_ENV !== 'production'; global.__EXPERIMENTAL__ = process.env.RELEASE_CHANNEL === 'experimental'; -jest.mock('react-dom', () => - require.requireActual('react-dom/cjs/react-dom-testing.development.js') -); -// we'll replace the above with react/testing and react-dom/testing right before the next minor - expect.extend(require('../toWarnDev')); function App(props) { @@ -33,6 +29,7 @@ beforeEach(() => { jest.resetModules(); React = require('react'); ReactDOM = require('react-dom'); + TestUtils = require('react-dom/test-utils'); ReactART = require('react-art'); ARTSVGMode = require('art/modes/svg'); ARTCurrentMode = require('art/modes/current'); @@ -73,7 +70,7 @@ beforeEach(() => { }); it("doesn't warn when you use the right act + renderer: dom", () => { - ReactDOM.act(() => { + TestUtils.act(() => { ReactDOM.render(, document.createElement('div')); }); }); @@ -89,7 +86,7 @@ it('resets correctly across renderers', () => { React.useEffect(() => {}, []); return null; } - ReactDOM.act(() => { + TestUtils.act(() => { TestRenderer.act(() => {}); expect(() => { TestRenderer.create(); @@ -126,7 +123,7 @@ it('warns when using the wrong act version - test + dom: updates', () => { it('warns when using the wrong act version - dom + test: .create()', () => { expect(() => { - ReactDOM.act(() => { + TestUtils.act(() => { TestRenderer.create(); }); }).toWarnDev(["It looks like you're using the wrong act()"], { @@ -137,7 +134,7 @@ it('warns when using the wrong act version - dom + test: .create()', () => { it('warns when using the wrong act version - dom + test: .update()', () => { const root = TestRenderer.create(); expect(() => { - ReactDOM.act(() => { + TestUtils.act(() => { root.update(); }); }).toWarnDev(["It looks like you're using the wrong act()"], { @@ -154,14 +151,14 @@ it('warns when using the wrong act version - dom + test: updates', () => { } TestRenderer.create(); expect(() => { - ReactDOM.act(() => { + TestUtils.act(() => { setCtr(1); }); }).toWarnDev(["It looks like you're using the wrong act()"]); }); it('does not warn when nesting react-act inside react-dom', () => { - ReactDOM.act(() => { + TestUtils.act(() => { ReactDOM.render(, document.createElement('div')); }); }); @@ -174,7 +171,7 @@ it('does not warn when nesting react-act inside react-test-renderer', () => { it("doesn't warn if you use nested acts from different renderers", () => { TestRenderer.act(() => { - ReactDOM.act(() => { + TestUtils.act(() => { TestRenderer.create(); }); }); diff --git a/package.json b/package.json index 89d8426a8ef1f..42fa148eb0714 100644 --- a/package.json +++ b/package.json @@ -40,6 +40,7 @@ "chalk": "^3.0.0", "cli-table": "^0.3.1", "coffee-script": "^1.12.7", + "confusing-browser-globals": "^1.0.9", "core-js": "^3.6.4", "coveralls": "^3.0.9", "create-react-class": "^15.6.3", @@ -77,12 +78,12 @@ "random-seed": "^0.3.0", "react-lifecycles-compat": "^3.0.4", "rimraf": "^3.0.0", - "rollup": "^0.52.1", + "rollup": "^1.19.4", "rollup-plugin-babel": "^4.0.1", - "rollup-plugin-commonjs": "^8.2.6", + "rollup-plugin-commonjs": "^9.3.4", "rollup-plugin-node-resolve": "^2.1.1", - "rollup-plugin-prettier": "^0.3.0", - "rollup-plugin-replace": "^2.0.0", + "rollup-plugin-prettier": "^0.6.0", + "rollup-plugin-replace": "^2.2.0", "rollup-plugin-strip-banner": "^0.2.0", "semver": "^7.1.1", "targz": "^1.0.1", diff --git a/packages/babel-plugin-react-jsx/__tests__/TransformJSXToReactCreateElement-test.js b/packages/babel-plugin-react-jsx/__tests__/TransformJSXToReactCreateElement-test.js deleted file mode 100644 index d8340607faa91..0000000000000 --- a/packages/babel-plugin-react-jsx/__tests__/TransformJSXToReactCreateElement-test.js +++ /dev/null @@ -1,392 +0,0 @@ -/** - * Copyright (c) Facebook, Inc. and its affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */ -/* eslint-disable quotes */ -'use strict'; - -const babel = require('@babel/core'); -const codeFrame = require('@babel/code-frame'); -const {wrap} = require('jest-snapshot-serializer-raw'); - -function transform(input, options) { - return wrap( - babel.transform(input, { - configFile: false, - plugins: [ - '@babel/plugin-syntax-jsx', - '@babel/plugin-transform-arrow-functions', - ...(options && options.development - ? [ - '@babel/plugin-transform-react-jsx-source', - '@babel/plugin-transform-react-jsx-self', - ] - : []), - [ - './packages/babel-plugin-react-jsx', - { - development: __DEV__, - useBuiltIns: true, - useCreateElement: true, - ...options, - }, - ], - ], - }).code - ); -} - -describe('transform react to jsx', () => { - it('fragment with no children', () => { - expect(transform(`var x = <>`)).toMatchSnapshot(); - }); - - it('React.Fragment to set keys and source', () => { - expect( - transform(`var x =
`, { - development: true, - }) - ).toMatchSnapshot(); - }); - - it('normal fragments not to set key and source', () => { - expect( - transform(`var x = <>
`, { - development: true, - }) - ).toMatchSnapshot(); - }); - - it('should properly handle comments adjacent to children', () => { - expect( - transform(` - var x = ( -
- {/* A comment at the beginning */} - {/* A second comment at the beginning */} - - {/* A nested comment */} - - {/* A sandwiched comment */} -
- {/* A comment at the end */} - {/* A second comment at the end */} -
- ); - `) - ).toMatchSnapshot(); - }); - - it('adds appropriate new lines when using spread attribute', () => { - expect(transform(``)).toMatchSnapshot(); - }); - - it('arrow functions', () => { - expect( - transform(` - var foo = function () { - return () => ; - }; - - var bar = function () { - return () => ; - }; - - `) - ).toMatchSnapshot(); - }); - - it('assignment', () => { - expect( - transform(`var div = `) - ).toMatchSnapshot(); - }); - - it('concatenates adjacent string literals', () => { - expect( - transform(` - var x = -
- foo - {"bar"} - baz -
- buz - bang -
- qux - {null} - quack -
- `) - ).toMatchSnapshot(); - }); - - it('should allow constructor as prop', () => { - expect(transform(`;`)).toMatchSnapshot(); - }); - - it('should allow deeper js namespacing', () => { - expect( - transform(`;`) - ).toMatchSnapshot(); - }); - - it('should allow elements as attributes', () => { - expect(transform(`
/>`)).toMatchSnapshot(); - }); - - it('should allow js namespacing', () => { - expect(transform(`;`)).toMatchSnapshot(); - }); - - it('should allow nested fragments', () => { - expect( - transform(` -
- < > - <> - Hello - world - - <> - Goodbye - world - - -
- `) - ).toMatchSnapshot(); - }); - - it('should avoid wrapping in extra parens if not needed', () => { - expect( - transform(` - var x =
- -
; - - var x =
- {props.children} -
; - - var x = - {props.children} - ; - - var x = - - ; - `) - ).toMatchSnapshot(); - }); - - it('should convert simple tags', () => { - expect(transform(`var x =
;`)).toMatchSnapshot(); - }); - - it('should convert simple text', () => { - expect(transform(`var x =
text
;`)).toMatchSnapshot(); - }); - - it('should disallow spread children', () => { - let _error; - const code = `
{...children}
;`; - try { - transform(code); - } catch (error) { - _error = error; - } - expect(_error).toEqual( - new SyntaxError( - 'unknown: Spread children are not supported in React.' + - '\n' + - codeFrame.codeFrameColumns( - code, - {start: {line: 1, column: 6}, end: {line: 1, column: 19}}, - {highlightCode: true} - ) - ) - ); - }); - - it('should escape xhtml jsxattribute', () => { - expect( - transform(` -
; -
; -
; - `) - ).toMatchSnapshot(); - }); - - it('should escape xhtml jsxtext', () => { - /* eslint-disable no-irregular-whitespace */ - expect( - transform(` -
wow
; -
wôw
; - -
w & w
; -
w & w
; - -
w   w
; -
this should not parse as unicode: \u00a0
; -
this should parse as nbsp:  
; -
this should parse as unicode: {'\u00a0 '}
; - -
w < w
; - `) - ).toMatchSnapshot(); - /*eslint-enable */ - }); - - it('should handle attributed elements', () => { - expect( - transform(` - var HelloMessage = React.createClass({ - render: function() { - return
Hello {this.props.name}
; - } - }); - - React.render( - Sebastian - - } />, mountNode); - `) - ).toMatchSnapshot(); - }); - - it('should handle has own property correctly', () => { - expect( - transform(`testing;`) - ).toMatchSnapshot(); - }); - - it('should have correct comma in nested children', () => { - expect( - transform(` - var x =
-

- {foo}
{bar}
-
-
; - `) - ).toMatchSnapshot(); - }); - - it('should insert commas after expressions before whitespace', () => { - expect( - transform(` - var x = -
-
- `) - ).toMatchSnapshot(); - }); - - it('should not add quotes to identifier names', () => { - expect( - transform(`var e = ;`) - ).toMatchSnapshot(); - }); - - it('should not strip nbsp even couple with other whitespace', () => { - expect(transform(`
 
;`)).toMatchSnapshot(); - }); - - it('should not strip tags with a single child of nbsp', () => { - expect(transform(`
 
;`)).toMatchSnapshot(); - }); - - it('should properly handle comments between props', () => { - expect( - transform(` - var x = ( -
- -
- ); - `) - ).toMatchSnapshot(); - }); - - it('should quote jsx attributes', () => { - expect( - transform(``) - ).toMatchSnapshot(); - }); - - it('should support xml namespaces if flag', () => { - expect( - transform('', {throwIfNamespace: false}) - ).toMatchSnapshot(); - }); - - it('should throw error namespaces if not flag', () => { - let _error; - const code = ``; - try { - transform(code); - } catch (error) { - _error = error; - } - expect(_error).toEqual( - new SyntaxError( - "unknown: Namespace tags are not supported by default. React's " + - "JSX doesn't support namespace tags. You can turn on the " + - "'throwIfNamespace' flag to bypass this warning." + - '\n' + - codeFrame.codeFrameColumns( - code, - {start: {line: 1, column: 2}, end: {line: 1, column: 9}}, - {highlightCode: true} - ) - ) - ); - }); - - it('should transform known hyphenated tags', () => { - expect(transform(``)).toMatchSnapshot(); - }); - - it('wraps props in react spread for first spread attributes', () => { - expect(transform(``)).toMatchSnapshot(); - }); - - it('wraps props in react spread for last spread attributes', () => { - expect(transform(``)).toMatchSnapshot(); - }); - - it('wraps props in react spread for middle spread attributes', () => { - expect(transform(``)).toMatchSnapshot(); - }); - - it('useBuiltIns false uses extend instead of Object.assign', () => { - expect( - transform(``, {useBuiltIns: false}) - ).toMatchSnapshot(); - }); -}); diff --git a/packages/babel-plugin-react-jsx/__tests__/TransformJSXToReactJSX-test.js b/packages/babel-plugin-react-jsx/__tests__/TransformJSXToReactJSX-test.js index ce7179e2b0ac1..e01259c851a39 100644 --- a/packages/babel-plugin-react-jsx/__tests__/TransformJSXToReactJSX-test.js +++ b/packages/babel-plugin-react-jsx/__tests__/TransformJSXToReactJSX-test.js @@ -11,14 +11,15 @@ const babel = require('@babel/core'); const codeFrame = require('@babel/code-frame'); const {wrap} = require('jest-snapshot-serializer-raw'); -function transform(input, options) { +function transform(input, pluginOpts, babelOpts) { return wrap( babel.transform(input, { configFile: false, + sourceType: 'module', plugins: [ '@babel/plugin-syntax-jsx', '@babel/plugin-transform-arrow-functions', - ...(options && options.development + ...(pluginOpts && pluginOpts.development ? [ '@babel/plugin-transform-react-jsx-source', '@babel/plugin-transform-react-jsx-self', @@ -29,15 +30,380 @@ function transform(input, options) { { useBuiltIns: true, useCreateElement: false, - ...options, + ...pluginOpts, }, ], ], + ...babelOpts, }).code ); } describe('transform react to jsx', () => { + it('auto import pragma overrides regular pragma', () => { + expect( + transform( + `/** @jsxAutoImport defaultExport */ + var x =
+ `, + { + autoImport: 'namespace', + importSource: 'foobar', + } + ) + ).toMatchSnapshot(); + }); + + it('import source pragma overrides regular pragma', () => { + expect( + transform( + `/** @jsxImportSource baz */ + var x =
+ `, + { + autoImport: 'namespace', + importSource: 'foobar', + } + ) + ).toMatchSnapshot(); + }); + + it('multiple pragmas work', () => { + expect( + transform( + `/** Some comment here + * @jsxImportSource baz + * @jsxAutoImport defaultExport + */ + var x =
+ `, + { + autoImport: 'namespace', + importSource: 'foobar', + } + ) + ).toMatchSnapshot(); + }); + + it('throws error when sourceType is module and autoImport is require', () => { + const code = `var x =
`; + expect(() => { + transform(code, { + autoImport: 'require', + }); + }).toThrow( + 'Babel `sourceType` must be set to `script` for autoImport ' + + 'to use `require` syntax. See Babel `sourceType` for details.\n' + + codeFrame.codeFrameColumns( + code, + {start: {line: 1, column: 1}, end: {line: 1, column: 28}}, + {highlightCode: true} + ) + ); + }); + + it('throws error when sourceType is script and autoImport is not require', () => { + const code = `var x =
`; + expect(() => { + transform( + code, + { + autoImport: 'namespace', + }, + {sourceType: 'script'} + ); + }).toThrow( + 'Babel `sourceType` must be set to `module` for autoImport ' + + 'to use `namespace` syntax. See Babel `sourceType` for details.\n' + + codeFrame.codeFrameColumns( + code, + {start: {line: 1, column: 1}, end: {line: 1, column: 28}}, + {highlightCode: true} + ) + ); + }); + + it("auto import that doesn't exist should throw error", () => { + const code = `var x =
`; + expect(() => { + transform(code, { + autoImport: 'foo', + }); + }).toThrow( + 'autoImport must be one of the following: none, require, namespace, defaultExport, namedExports\n' + + codeFrame.codeFrameColumns( + code, + {start: {line: 1, column: 1}, end: {line: 1, column: 28}}, + {highlightCode: true} + ) + ); + }); + + it('auto import can specify source', () => { + expect( + transform(`var x =
`, { + autoImport: 'namespace', + importSource: 'foobar', + }) + ).toMatchSnapshot(); + }); + + it('auto import require', () => { + expect( + transform( + `var x = ( + <> +
+
+
+
+
+
+ + );`, + { + autoImport: 'require', + }, + { + sourceType: 'script', + } + ) + ).toMatchSnapshot(); + }); + + it('auto import namespace', () => { + expect( + transform( + `var x = ( + <> +
+
+
+
+
+
+ + );`, + { + autoImport: 'namespace', + } + ) + ).toMatchSnapshot(); + }); + + it('auto import default', () => { + expect( + transform( + `var x = ( + <> +
+
+
+
+
+
+ + );`, + { + autoImport: 'defaultExport', + } + ) + ).toMatchSnapshot(); + }); + + it('auto import named exports', () => { + expect( + transform( + `var x = ( + <> +
+
+
+
+
+
+ + );`, + { + autoImport: 'namedExports', + } + ) + ).toMatchSnapshot(); + }); + + it('auto import with no JSX', () => { + expect( + transform( + `var foo = "
"`, + { + autoImport: 'require', + }, + { + sourceType: 'script', + } + ) + ).toMatchSnapshot(); + }); + + it('complicated scope require', () => { + expect( + transform( + ` + const Bar = () => { + const Foo = () => { + const Component = ({thing, ..._react}) => { + if (!thing) { + var _react2 = "something useless"; + var b = _react3(); + var c = _react5(); + var jsx = 1; + var _jsx = 2; + return
; + }; + return ; + }; + } + } + `, + { + autoImport: 'require', + }, + { + sourceType: 'script', + } + ) + ).toMatchSnapshot(); + }); + + it('complicated scope named exports', () => { + expect( + transform( + ` + const Bar = () => { + const Foo = () => { + const Component = ({thing, ..._react}) => { + if (!thing) { + var _react2 = "something useless"; + var b = _react3(); + var jsx = 1; + var _jsx = 2; + return
; + }; + return ; + }; + } + } + `, + { + autoImport: 'namedExports', + } + ) + ).toMatchSnapshot(); + }); + + it('auto import in dev', () => { + expect( + transform( + `var x = ( + <> +
+
+
+
+
+
+ + );`, + { + autoImport: 'namedExports', + development: true, + } + ) + ).toMatchSnapshot(); + }); + + it('auto import none', () => { + expect( + transform( + `var x = ( + <> +
+
+
+
+
+
+ + );`, + { + autoImport: 'none', + } + ) + ).toMatchSnapshot(); + }); + + it('auto import undefined', () => { + expect( + transform( + `var x = ( + <> +
+
+
+
+
+
+ + );` + ) + ).toMatchSnapshot(); + }); + + it('auto import with namespaces already defined', () => { + expect( + transform( + ` + import * as _react from "foo"; + const react = _react(1); + const _react1 = react; + const _react2 = react; + var x = ( +
+
+
+
+
+
+ );`, + { + autoImport: 'namespace', + } + ) + ).toMatchSnapshot(); + }); + + it('auto import with react already defined', () => { + expect( + transform( + ` + import * as react from "react"; + var y = react.createElement("div", {foo: 1}); + var x = ( +
+
+
+
+
+
+ );`, + + { + autoImport: 'namespace', + } + ) + ).toMatchSnapshot(); + }); + it('fragment with no children', () => { expect(transform(`var x = <>`)).toMatchSnapshot(); }); diff --git a/packages/babel-plugin-react-jsx/__tests__/__snapshots__/TransformJSXToReactCreateElement-test.js.snap b/packages/babel-plugin-react-jsx/__tests__/__snapshots__/TransformJSXToReactCreateElement-test.js.snap deleted file mode 100644 index a74c7e1d15e82..0000000000000 --- a/packages/babel-plugin-react-jsx/__tests__/__snapshots__/TransformJSXToReactCreateElement-test.js.snap +++ /dev/null @@ -1,213 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`transform react to jsx React.Fragment to set keys and source 1`] = ` -var _jsxFileName = ""; -var x = React.createElement(React.Fragment, { - key: "foo", - __source: { - fileName: _jsxFileName, - lineNumber: 1 - }, - __self: this -}, React.createElement("div", { - __source: { - fileName: _jsxFileName, - lineNumber: 1 - }, - __self: this -})); -`; - -exports[`transform react to jsx adds appropriate new lines when using spread attribute 1`] = ` -React.createElement(Component, Object.assign({}, props, { - sound: "moo" -})); -`; - -exports[`transform react to jsx arrow functions 1`] = ` -var foo = function () { - var _this = this; - - return function () { - return React.createElement(_this, null); - }; -}; - -var bar = function () { - var _this2 = this; - - return function () { - return React.createElement(_this2.foo, null); - }; -}; -`; - -exports[`transform react to jsx assignment 1`] = ` -var div = React.createElement(Component, Object.assign({}, props, { - foo: "bar" -})); -`; - -exports[`transform react to jsx concatenates adjacent string literals 1`] = `var x = React.createElement("div", null, "foo", "bar", "baz", React.createElement("div", null, "buz bang"), "qux", null, "quack");`; - -exports[`transform react to jsx fragment with no children 1`] = `var x = React.createElement(React.Fragment, null);`; - -exports[`transform react to jsx normal fragments not to set key and source 1`] = ` -var _jsxFileName = ""; -var x = React.createElement(React.Fragment, null, React.createElement("div", { - __source: { - fileName: _jsxFileName, - lineNumber: 1 - }, - __self: this -})); -`; - -exports[`transform react to jsx should allow constructor as prop 1`] = ` -React.createElement(Component, { - constructor: "foo" -}); -`; - -exports[`transform react to jsx should allow deeper js namespacing 1`] = `React.createElement(Namespace.DeepNamespace.Component, null);`; - -exports[`transform react to jsx should allow elements as attributes 1`] = ` -React.createElement("div", { - attr: React.createElement("div", null) -}); -`; - -exports[`transform react to jsx should allow js namespacing 1`] = `React.createElement(Namespace.Component, null);`; - -exports[`transform react to jsx should allow nested fragments 1`] = `React.createElement("div", null, React.createElement(React.Fragment, null, React.createElement(React.Fragment, null, React.createElement("span", null, "Hello"), React.createElement("span", null, "world")), React.createElement(React.Fragment, null, React.createElement("span", null, "Goodbye"), React.createElement("span", null, "world"))));`; - -exports[`transform react to jsx should avoid wrapping in extra parens if not needed 1`] = ` -var x = React.createElement("div", null, React.createElement(Component, null)); -var x = React.createElement("div", null, props.children); -var x = React.createElement(Composite, null, props.children); -var x = React.createElement(Composite, null, React.createElement(Composite2, null)); -`; - -exports[`transform react to jsx should convert simple tags 1`] = `var x = React.createElement("div", null);`; - -exports[`transform react to jsx should convert simple text 1`] = `var x = React.createElement("div", null, "text");`; - -exports[`transform react to jsx should escape xhtml jsxattribute 1`] = ` -React.createElement("div", { - id: "w\\xF4w" -}); -React.createElement("div", { - id: "w" -}); -React.createElement("div", { - id: "w < w" -}); -`; - -exports[`transform react to jsx should escape xhtml jsxtext 1`] = ` -React.createElement("div", null, "wow"); -React.createElement("div", null, "w\\xF4w"); -React.createElement("div", null, "w & w"); -React.createElement("div", null, "w & w"); -React.createElement("div", null, "w \\xA0 w"); -React.createElement("div", null, "this should not parse as unicode: \\xA0"); -React.createElement("div", null, "this should parse as nbsp: \\xA0 "); -React.createElement("div", null, "this should parse as unicode: ", '  '); -React.createElement("div", null, "w < w"); -`; - -exports[`transform react to jsx should handle attributed elements 1`] = ` -var HelloMessage = React.createClass({ - render: function () { - return React.createElement("div", null, "Hello ", this.props.name); - } -}); -React.render(React.createElement(HelloMessage, { - name: React.createElement("span", null, "Sebastian") -}), mountNode); -`; - -exports[`transform react to jsx should handle has own property correctly 1`] = `React.createElement("hasOwnProperty", null, "testing");`; - -exports[`transform react to jsx should have correct comma in nested children 1`] = `var x = React.createElement("div", null, React.createElement("div", null, React.createElement("br", null)), React.createElement(Component, null, foo, React.createElement("br", null), bar), React.createElement("br", null));`; - -exports[`transform react to jsx should insert commas after expressions before whitespace 1`] = ` -var x = React.createElement("div", { - attr1: "foo" + "bar", - attr2: "foo" + "bar" + "baz" + "bug", - attr3: "foo" + "bar" + "baz" + "bug", - attr4: "baz" -}); -`; - -exports[`transform react to jsx should not add quotes to identifier names 1`] = ` -var e = React.createElement(F, { - aaa: true, - new: true, - const: true, - var: true, - default: true, - "foo-bar": true -}); -`; - -exports[`transform react to jsx should not strip nbsp even couple with other whitespace 1`] = `React.createElement("div", null, "\\xA0 ");`; - -exports[`transform react to jsx should not strip tags with a single child of nbsp 1`] = `React.createElement("div", null, "\\xA0");`; - -exports[`transform react to jsx should properly handle comments adjacent to children 1`] = `var x = React.createElement("div", null, React.createElement("span", null), React.createElement("br", null));`; - -exports[`transform react to jsx should properly handle comments between props 1`] = ` -var x = React.createElement("div", { - /* a multi-line - comment */ - attr1: "foo" -}, React.createElement("span", { - // a double-slash comment - attr2: "bar" -})); -`; - -exports[`transform react to jsx should quote jsx attributes 1`] = ` -React.createElement("button", { - "data-value": "a value" -}, "Button"); -`; - -exports[`transform react to jsx should support xml namespaces if flag 1`] = ` -React.createElement("f:image", { - "n:attr": true -}); -`; - -exports[`transform react to jsx should transform known hyphenated tags 1`] = `React.createElement("font-face", null);`; - -exports[`transform react to jsx useBuiltIns false uses extend instead of Object.assign 1`] = ` -function _extends() { _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); } - -React.createElement(Component, _extends({ - y: 2 -}, x)); -`; - -exports[`transform react to jsx wraps props in react spread for first spread attributes 1`] = ` -React.createElement(Component, Object.assign({}, x, { - y: 2, - z: true -})); -`; - -exports[`transform react to jsx wraps props in react spread for last spread attributes 1`] = ` -React.createElement(Component, Object.assign({ - y: 2, - z: true -}, x)); -`; - -exports[`transform react to jsx wraps props in react spread for middle spread attributes 1`] = ` -React.createElement(Component, Object.assign({ - y: 2 -}, x, { - z: true -})); -`; diff --git a/packages/babel-plugin-react-jsx/__tests__/__snapshots__/TransformJSXToReactJSX-test.js.snap b/packages/babel-plugin-react-jsx/__tests__/__snapshots__/TransformJSXToReactJSX-test.js.snap index 22b83d9ec1513..cb8a89cc046de 100644 --- a/packages/babel-plugin-react-jsx/__tests__/__snapshots__/TransformJSXToReactJSX-test.js.snap +++ b/packages/babel-plugin-react-jsx/__tests__/__snapshots__/TransformJSXToReactJSX-test.js.snap @@ -38,6 +38,230 @@ var div = React.jsx(Component, Object.assign({}, props, { })); `; +exports[`transform react to jsx auto import can specify source 1`] = ` +import * as _foobar from "foobar"; + +var x = _foobar.jsx("div", { + children: _foobar.jsx("span", {}) +}); +`; + +exports[`transform react to jsx auto import default 1`] = ` +import _default from "react"; + +var x = _default.jsx(_default.Fragment, { + children: _default.jsxs("div", { + children: [_default.jsx("div", {}, "1"), _default.jsx("div", { + meow: "wolf" + }, "2"), _default.jsx("div", {}, "3"), _default.createElement("div", Object.assign({}, props, { + key: "4" + }))] + }) +}); +`; + +exports[`transform react to jsx auto import in dev 1`] = ` +import { createElement as _createElement } from "react"; +import { jsxDEV as _jsxDEV } from "react"; +import { Fragment as _Fragment } from "react"; +var _jsxFileName = ""; + +var x = _jsxDEV(_Fragment, { + children: _jsxDEV("div", { + children: [_jsxDEV("div", {}, "1", false, { + fileName: _jsxFileName, + lineNumber: 4 + }, this), _jsxDEV("div", { + meow: "wolf" + }, "2", false, { + fileName: _jsxFileName, + lineNumber: 5 + }, this), _jsxDEV("div", {}, "3", false, { + fileName: _jsxFileName, + lineNumber: 6 + }, this), _createElement("div", Object.assign({}, props, { + key: "4", + __source: { + fileName: _jsxFileName, + lineNumber: 7 + }, + __self: this + }))] + }, undefined, true, { + fileName: _jsxFileName, + lineNumber: 3 + }, this) +}, undefined, false); +`; + +exports[`transform react to jsx auto import named exports 1`] = ` +import { createElement as _createElement } from "react"; +import { jsx as _jsx } from "react"; +import { jsxs as _jsxs } from "react"; +import { Fragment as _Fragment } from "react"; + +var x = _jsx(_Fragment, { + children: _jsxs("div", { + children: [_jsx("div", {}, "1"), _jsx("div", { + meow: "wolf" + }, "2"), _jsx("div", {}, "3"), _createElement("div", Object.assign({}, props, { + key: "4" + }))] + }) +}); +`; + +exports[`transform react to jsx auto import namespace 1`] = ` +import * as _react from "react"; + +var x = _react.jsx(_react.Fragment, { + children: _react.jsxs("div", { + children: [_react.jsx("div", {}, "1"), _react.jsx("div", { + meow: "wolf" + }, "2"), _react.jsx("div", {}, "3"), _react.createElement("div", Object.assign({}, props, { + key: "4" + }))] + }) +}); +`; + +exports[`transform react to jsx auto import none 1`] = ` +var x = React.jsx(React.Fragment, { + children: React.jsxs("div", { + children: [React.jsx("div", {}, "1"), React.jsx("div", { + meow: "wolf" + }, "2"), React.jsx("div", {}, "3"), React.createElement("div", Object.assign({}, props, { + key: "4" + }))] + }) +}); +`; + +exports[`transform react to jsx auto import pragma overrides regular pragma 1`] = ` +import _default from "foobar"; + +/** @jsxAutoImport defaultExport */ +var x = _default.jsx("div", { + children: _default.jsx("span", {}) +}); +`; + +exports[`transform react to jsx auto import require 1`] = ` +var _react = require("react"); + +var x = _react.jsx(_react.Fragment, { + children: _react.jsxs("div", { + children: [_react.jsx("div", {}, "1"), _react.jsx("div", { + meow: "wolf" + }, "2"), _react.jsx("div", {}, "3"), _react.createElement("div", Object.assign({}, props, { + key: "4" + }))] + }) +}); +`; + +exports[`transform react to jsx auto import undefined 1`] = ` +var x = React.jsx(React.Fragment, { + children: React.jsxs("div", { + children: [React.jsx("div", {}, "1"), React.jsx("div", { + meow: "wolf" + }, "2"), React.jsx("div", {}, "3"), React.createElement("div", Object.assign({}, props, { + key: "4" + }))] + }) +}); +`; + +exports[`transform react to jsx auto import with namespaces already defined 1`] = ` +import * as _react3 from "react"; +import * as _react from "foo"; + +const react = _react(1); + +const _react1 = react; +const _react2 = react; + +var x = _react3.jsxs("div", { + children: [_react3.jsx("div", {}, "1"), _react3.jsx("div", { + meow: "wolf" + }, "2"), _react3.jsx("div", {}, "3"), _react3.createElement("div", Object.assign({}, props, { + key: "4" + }))] +}); +`; + +exports[`transform react to jsx auto import with no JSX 1`] = `var foo = "
";`; + +exports[`transform react to jsx auto import with react already defined 1`] = ` +import * as _react from "react"; +import * as react from "react"; +var y = react.createElement("div", { + foo: 1 +}); + +var x = _react.jsxs("div", { + children: [_react.jsx("div", {}, "1"), _react.jsx("div", { + meow: "wolf" + }, "2"), _react.jsx("div", {}, "3"), _react.createElement("div", Object.assign({}, props, { + key: "4" + }))] +}); +`; + +exports[`transform react to jsx complicated scope named exports 1`] = ` +import { jsx as _jsx2 } from "react"; + +const Bar = function () { + const Foo = function () { + const Component = function ({ + thing, + ..._react + }) { + if (!thing) { + var _react2 = "something useless"; + + var b = _react3(); + + var jsx = 1; + var _jsx = 2; + return _jsx2("div", {}); + } + + ; + return _jsx2("span", {}); + }; + }; +}; +`; + +exports[`transform react to jsx complicated scope require 1`] = ` +var _react4 = require("react"); + +const Bar = function () { + const Foo = function () { + const Component = function ({ + thing, + ..._react + }) { + if (!thing) { + var _react2 = "something useless"; + + var b = _react3(); + + var c = _react5(); + + var jsx = 1; + var _jsx = 2; + return _react4.jsx("div", {}); + } + + ; + return _react4.jsx("span", {}); + }; + }; +}; +`; + exports[`transform react to jsx concatenates adjacent string literals 1`] = ` var x = React.jsxs("div", { children: ["foo", "bar", "baz", React.jsx("div", { @@ -92,6 +316,27 @@ var x = React.jsxDEV(React.Fragment, { exports[`transform react to jsx fragments to set keys 1`] = `var x = React.jsx(React.Fragment, {}, "foo");`; +exports[`transform react to jsx import source pragma overrides regular pragma 1`] = ` +import * as _baz from "baz"; + +/** @jsxImportSource baz */ +var x = _baz.jsx("div", { + children: _baz.jsx("span", {}) +}); +`; + +exports[`transform react to jsx multiple pragmas work 1`] = ` +import _default from "baz"; + +/** Some comment here + * @jsxImportSource baz + * @jsxAutoImport defaultExport + */ +var x = _default.jsx("div", { + children: _default.jsx("span", {}) +}); +`; + exports[`transform react to jsx nonStatic children 1`] = ` var _jsxFileName = ""; var x = React.jsxDEV("div", { diff --git a/packages/babel-plugin-react-jsx/package.json b/packages/babel-plugin-react-jsx/package.json index 41243452d4f17..c33a20e06965b 100644 --- a/packages/babel-plugin-react-jsx/package.json +++ b/packages/babel-plugin-react-jsx/package.json @@ -5,8 +5,8 @@ "description": "@babel/plugin-transform-react-jsx", "main": "index.js", "dependencies": { + "@babel/helper-module-imports": "^7.0.0", "esutils": "^2.0.0" - }, "files": [ "README.md", diff --git a/packages/babel-plugin-react-jsx/src/TransformJSXToReactBabelPlugin.js b/packages/babel-plugin-react-jsx/src/TransformJSXToReactBabelPlugin.js index 308fc7116f8c0..2d8ebc8c6ec99 100644 --- a/packages/babel-plugin-react-jsx/src/TransformJSXToReactBabelPlugin.js +++ b/packages/babel-plugin-react-jsx/src/TransformJSXToReactBabelPlugin.js @@ -24,6 +24,50 @@ 'use strict'; const esutils = require('esutils'); +const { + isModule, + addNamespace, + addNamed, + addDefault, +} = require('@babel/helper-module-imports'); + +// These are all the valid auto import types (under the config autoImport) +// that a user can specific +const IMPORT_TYPES = { + none: 'none', // default option. Will not import anything + require: 'require', // var _react = require("react"); + namespace: 'namespace', // import * as _react from "react"; + defaultExport: 'defaultExport', // import _default from "react"; + namedExports: 'namedExports', // import { jsx } from "react"; +}; + +const JSX_AUTO_IMPORT_ANNOTATION_REGEX = /\*?\s*@jsxAutoImport\s+([^\s]+)/; +const JSX_IMPORT_SOURCE_ANNOTATION_REGEX = /\*?\s*@jsxImportSource\s+([^\s]+)/; + +// We want to use React.createElement, even in the case of +// jsx, for
to distinguish it +// from
. This is an intermediary +// step while we deprecate key spread from props. Afterwards, +// we will remove createElement entirely +function shouldUseCreateElement(path, types) { + const openingPath = path.get('openingElement'); + const attributes = openingPath.node.attributes; + + let seenPropsSpread = false; + for (let i = 0; i < attributes.length; i++) { + const attr = attributes[i]; + if ( + seenPropsSpread && + types.isJSXAttribute(attr) && + attr.name.name === 'key' + ) { + return true; + } else if (types.isJSXSpreadAttribute(attr)) { + seenPropsSpread = true; + } + } + return false; +} function helper(babel, opts) { const {types: t} = babel; @@ -52,7 +96,7 @@ You can turn on the 'throwIfNamespace' flag to bypass this warning.`, visitor.JSXElement = { exit(path, file) { let callExpr; - if (file.opts.useCreateElement || shouldUseCreateElement(path)) { + if (shouldUseCreateElement(path, t)) { callExpr = buildCreateElementCall(path, file); } else { callExpr = buildJSXElementCall(path, file); @@ -71,12 +115,7 @@ You can turn on the 'throwIfNamespace' flag to bypass this warning.`, 'Fragment tags are only supported in React 16 and up.', ); } - let callExpr; - if (file.opts.useCreateElement) { - callExpr = buildCreateElementFragmentCall(path, file); - } else { - callExpr = buildJSXFragmentCall(path, file); - } + let callExpr = buildJSXFragmentCall(path, file); if (callExpr) { path.replaceWith(t.inherits(callExpr, path.node)); @@ -147,31 +186,6 @@ You can turn on the 'throwIfNamespace' flag to bypass this warning.`, return t.inherits(t.objectProperty(node.name, value), node); } - // We want to use React.createElement, even in the case of - // jsx, for
to distinguish it - // from
. This is an intermediary - // step while we deprecate key spread from props. Afterwards, - // we will remove createElement entirely - function shouldUseCreateElement(path) { - const openingPath = path.get('openingElement'); - const attributes = openingPath.node.attributes; - - let seenPropsSpread = false; - for (let i = 0; i < attributes.length; i++) { - const attr = attributes[i]; - if ( - seenPropsSpread && - t.isJSXAttribute(attr) && - attr.name.name === 'key' - ) { - return true; - } else if (t.isJSXSpreadAttribute(attr)) { - seenPropsSpread = true; - } - } - return false; - } - // Builds JSX into: // Production: React.jsx(type, arguments, key) // Development: React.jsxDEV(type, arguments, key, isStaticChildren, source, self) @@ -262,6 +276,7 @@ You can turn on the 'throwIfNamespace' flag to bypass this warning.`, if (opts.post) { opts.post(state, file); } + return ( state.call || t.callExpression( @@ -561,38 +576,6 @@ You can turn on the 'throwIfNamespace' flag to bypass this warning.`, return attribs; } - - function buildCreateElementFragmentCall(path, file) { - if (opts.filter && !opts.filter(path.node, file)) { - return; - } - - const openingPath = path.get('openingElement'); - openingPath.parent.children = t.react.buildChildren(openingPath.parent); - - const args = []; - const tagName = null; - const tagExpr = file.get('jsxFragIdentifier')(); - - const state = { - tagExpr: tagExpr, - tagName: tagName, - args: args, - }; - - if (opts.pre) { - opts.pre(state, file); - } - - // no attributes are allowed with <> syntax - args.push(t.nullLiteral(), ...path.node.children); - - if (opts.post) { - opts.post(state, file); - } - - return state.call || t.callExpression(state.oldCallee, args); - } } module.exports = function(babel) { @@ -623,25 +606,183 @@ module.exports = function(babel) { }, }); - visitor.Program = { - enter(path, state) { - state.set( - 'oldJSXIdentifier', - createIdentifierParser('React.createElement'), + const createIdentifierName = (path, autoImport, name, importName) => { + if (autoImport === IMPORT_TYPES.none) { + return `React.${name}`; + } else if (autoImport === IMPORT_TYPES.namedExports) { + if (importName) { + const identifierName = `${importName[name]}`; + return identifierName; + } + } else { + return `${importName}.${name}`; + } + }; + + function getImportNames(parentPath, state) { + const imports = {}; + parentPath.traverse({ + JSXElement(path) { + if (shouldUseCreateElement(path, t)) { + imports.createElement = true; + } else if (path.node.children.length > 1) { + const importName = state.development ? 'jsxDEV' : 'jsxs'; + imports[importName] = true; + } else { + const importName = state.development ? 'jsxDEV' : 'jsx'; + imports[importName] = true; + } + }, + + JSXFragment(path) { + imports.Fragment = true; + }, + }); + return imports; + } + + function hasJSX(parentPath) { + let fileHasJSX = false; + parentPath.traverse({ + JSXElement(path) { + fileHasJSX = true; + path.stop(); + }, + + JSXFragment(path) { + fileHasJSX = true; + path.stop(); + }, + }); + + return fileHasJSX; + } + + function addAutoImports(path, state) { + if (state.autoImport === IMPORT_TYPES.none) { + return; + } + + if (IMPORT_TYPES[state.autoImport] === undefined) { + throw path.buildCodeFrameError( + 'autoImport must be one of the following: ' + + Object.keys(IMPORT_TYPES).join(', '), ); - state.set( - 'jsxIdentifier', - createIdentifierParser( - state.opts.development ? 'React.jsxDEV' : 'React.jsx', - ), + } + if (state.autoImport === IMPORT_TYPES.require && isModule(path)) { + throw path.buildCodeFrameError( + 'Babel `sourceType` must be set to `script` for autoImport ' + + 'to use `require` syntax. See Babel `sourceType` for details.', ); - state.set( - 'jsxStaticIdentifier', - createIdentifierParser( - state.opts.development ? 'React.jsxDEV' : 'React.jsxs', - ), + } + if (state.autoImport !== IMPORT_TYPES.require && !isModule(path)) { + throw path.buildCodeFrameError( + 'Babel `sourceType` must be set to `module` for autoImport to use `' + + state.autoImport + + '` syntax. See Babel `sourceType` for details.', ); - state.set('jsxFragIdentifier', createIdentifierParser('React.Fragment')); + } + + // import {jsx} from "react"; + // import {createElement} from "react"; + if (state.autoImport === IMPORT_TYPES.namedExports) { + const imports = getImportNames(path, state); + const importMap = {}; + + Object.keys(imports).forEach(importName => { + importMap[importName] = addNamed(path, importName, state.source).name; + }); + + return importMap; + } + + // add import to file and get the import name + let name; + if (state.autoImport === IMPORT_TYPES.require) { + // var _react = require("react"); + name = addNamespace(path, state.source, { + importedInterop: 'uncompiled', + }).name; + } else if (state.autoImport === IMPORT_TYPES.namespace) { + // import * as _react from "react"; + name = addNamespace(path, state.source).name; + } else if (state.autoImport === IMPORT_TYPES.defaultExport) { + // import _default from "react"; + name = addDefault(path, state.source).name; + } + + return name; + } + + visitor.Program = { + enter(path, state) { + if (hasJSX(path)) { + let autoImport = state.opts.autoImport || IMPORT_TYPES.none; + let source = state.opts.importSource || 'react'; + const {file} = state; + + if (file.ast.comments) { + for (let i = 0; i < file.ast.comments.length; i++) { + const comment = file.ast.comments[i]; + const jsxAutoImportMatches = JSX_AUTO_IMPORT_ANNOTATION_REGEX.exec( + comment.value, + ); + if (jsxAutoImportMatches) { + autoImport = jsxAutoImportMatches[1]; + } + const jsxImportSourceMatches = JSX_IMPORT_SOURCE_ANNOTATION_REGEX.exec( + comment.value, + ); + if (jsxImportSourceMatches) { + source = jsxImportSourceMatches[1]; + } + } + } + + const importName = addAutoImports(path, { + ...state.opts, + autoImport, + source, + }); + + state.set( + 'oldJSXIdentifier', + createIdentifierParser( + createIdentifierName(path, autoImport, 'createElement', importName), + ), + ); + + state.set( + 'jsxIdentifier', + createIdentifierParser( + createIdentifierName( + path, + autoImport, + state.opts.development ? 'jsxDEV' : 'jsx', + importName, + ), + ), + ); + + state.set( + 'jsxStaticIdentifier', + createIdentifierParser( + createIdentifierName( + path, + autoImport, + state.opts.development ? 'jsxDEV' : 'jsxs', + importName, + ), + ), + ); + + state.set( + 'jsxFragIdentifier', + createIdentifierParser( + createIdentifierName(path, autoImport, 'Fragment', importName), + ), + ); + } }, }; diff --git a/packages/create-subscription/package.json b/packages/create-subscription/package.json index 34b87144a92b8..4f7b245152fbb 100644 --- a/packages/create-subscription/package.json +++ b/packages/create-subscription/package.json @@ -1,7 +1,7 @@ { "name": "create-subscription", "description": "utility for subscribing to external data sources inside React components", - "version": "16.12.0", + "version": "16.13.0", "repository": { "type": "git", "url": "https://github.com/facebook/react.git", diff --git a/packages/create-subscription/src/createSubscription.js b/packages/create-subscription/src/createSubscription.js index b7750bb319ae4..30d7b669f65fe 100644 --- a/packages/create-subscription/src/createSubscription.js +++ b/packages/create-subscription/src/createSubscription.js @@ -7,7 +7,7 @@ * @flow */ -import React from 'react'; +import * as React from 'react'; import invariant from 'shared/invariant'; type Unsubscribe = () => void; diff --git a/packages/eslint-plugin-react-hooks/README.md b/packages/eslint-plugin-react-hooks/README.md index bda6fe8fdc83a..df8ae3440262d 100644 --- a/packages/eslint-plugin-react-hooks/README.md +++ b/packages/eslint-plugin-react-hooks/README.md @@ -34,6 +34,17 @@ Then add it to your ESLint configuration: } ``` +Or use the recommended config: + +```js +{ + "extends": [ + // ... + "plugin:react-hooks/recommended" + ] +} +``` + ## Valid and Invalid Examples Please refer to the [Rules of Hooks](https://reactjs.org/docs/hooks-rules.html) documentation and the [Hooks FAQ](https://reactjs.org/docs/hooks-faq.html#what-exactly-do-the-lint-rules-enforce) to learn more about this rule. diff --git a/packages/eslint-plugin-react-hooks/__tests__/ESLintRuleExhaustiveDeps-test.js b/packages/eslint-plugin-react-hooks/__tests__/ESLintRuleExhaustiveDeps-test.js index aa25e13ba5121..d45728aedb0fc 100644 --- a/packages/eslint-plugin-react-hooks/__tests__/ESLintRuleExhaustiveDeps-test.js +++ b/packages/eslint-plugin-react-hooks/__tests__/ESLintRuleExhaustiveDeps-test.js @@ -19,6 +19,16 @@ ESLintTester.setDefaultConfig({ }, }); +/** + * A string template tag that removes padding from the left side of multi-line strings + * @param {Array} strings array of code strings (only one expected) + */ +function normalizeIndent(strings) { + const codeLines = strings[0].split('\n'); + const leftPadding = codeLines[1].match(/\s+/)[0]; + return codeLines.map(line => line.substr(leftPadding.length)).join('\n'); +} + // *************************************************** // For easier local testing, you can add to any case: // { @@ -32,7 +42,7 @@ ESLintTester.setDefaultConfig({ const tests = { valid: [ { - code: ` + code: normalizeIndent` function MyComponent() { const local = {}; useEffect(() => { @@ -42,7 +52,7 @@ const tests = { `, }, { - code: ` + code: normalizeIndent` function MyComponent() { useEffect(() => { const local = {}; @@ -52,7 +62,7 @@ const tests = { `, }, { - code: ` + code: normalizeIndent` function MyComponent() { const local = {}; useEffect(() => { @@ -67,7 +77,7 @@ const tests = { // to be an import that hasn't been added yet, or // a component-level variable. Ignore it until it // gets defined (a different rule would flag it anyway). - code: ` + code: normalizeIndent` function MyComponent() { useEffect(() => { console.log(props.foo); @@ -76,7 +86,7 @@ const tests = { `, }, { - code: ` + code: normalizeIndent` function MyComponent() { const local1 = {}; { @@ -90,7 +100,7 @@ const tests = { `, }, { - code: ` + code: normalizeIndent` function MyComponent() { const local1 = {}; { @@ -104,7 +114,7 @@ const tests = { `, }, { - code: ` + code: normalizeIndent` function MyComponent() { const local1 = {}; function MyNestedComponent() { @@ -118,7 +128,7 @@ const tests = { `, }, { - code: ` + code: normalizeIndent` function MyComponent() { const local = {}; useEffect(() => { @@ -129,7 +139,7 @@ const tests = { `, }, { - code: ` + code: normalizeIndent` function MyComponent() { useEffect(() => { console.log(unresolved); @@ -138,7 +148,7 @@ const tests = { `, }, { - code: ` + code: normalizeIndent` function MyComponent() { const local = {}; useEffect(() => { @@ -149,7 +159,7 @@ const tests = { }, { // Regression test - code: ` + code: normalizeIndent` function MyComponent({ foo }) { useEffect(() => { console.log(foo.length); @@ -159,7 +169,7 @@ const tests = { }, { // Regression test - code: ` + code: normalizeIndent` function MyComponent({ foo }) { useEffect(() => { console.log(foo.length); @@ -170,7 +180,7 @@ const tests = { }, { // Regression test - code: ` + code: normalizeIndent` function MyComponent({ history }) { useEffect(() => { return history.listen(); @@ -180,7 +190,7 @@ const tests = { }, { // Valid because they have meaning without deps. - code: ` + code: normalizeIndent` function MyComponent(props) { useEffect(() => {}); useLayoutEffect(() => {}); @@ -189,7 +199,7 @@ const tests = { `, }, { - code: ` + code: normalizeIndent` function MyComponent(props) { useEffect(() => { console.log(props.foo); @@ -198,7 +208,7 @@ const tests = { `, }, { - code: ` + code: normalizeIndent` function MyComponent(props) { useEffect(() => { console.log(props.foo); @@ -208,7 +218,7 @@ const tests = { `, }, { - code: ` + code: normalizeIndent` function MyComponent(props) { useEffect(() => { console.log(props.foo); @@ -218,7 +228,7 @@ const tests = { `, }, { - code: ` + code: normalizeIndent` function MyComponent(props) { const local = {}; useEffect(() => { @@ -233,7 +243,7 @@ const tests = { // [props, props.foo] is technically unnecessary ('props' covers 'props.foo'). // However, it's valid for effects to over-specify their deps. // So we don't warn about this. We *would* warn about useMemo/useCallback. - code: ` + code: normalizeIndent` function MyComponent(props) { const local = {}; useEffect(() => { @@ -250,7 +260,7 @@ const tests = { `, }, { - code: ` + code: normalizeIndent` function MyComponent(props) { useCustomEffect(() => { console.log(props.foo); @@ -260,7 +270,7 @@ const tests = { options: [{additionalHooks: 'useCustomEffect'}], }, { - code: ` + code: normalizeIndent` function MyComponent(props) { useCustomEffect(() => { console.log(props.foo); @@ -270,7 +280,7 @@ const tests = { options: [{additionalHooks: 'useCustomEffect'}], }, { - code: ` + code: normalizeIndent` function MyComponent(props) { useCustomEffect(() => { console.log(props.foo); @@ -281,7 +291,7 @@ const tests = { }, { // Valid because we don't care about hooks outside of components. - code: ` + code: normalizeIndent` const local = {}; useEffect(() => { console.log(local); @@ -290,7 +300,7 @@ const tests = { }, { // Valid because we don't care about hooks outside of components. - code: ` + code: normalizeIndent` const local1 = {}; { const local2 = {}; @@ -302,7 +312,7 @@ const tests = { `, }, { - code: ` + code: normalizeIndent` function MyComponent() { const ref = useRef(); useEffect(() => { @@ -312,7 +322,7 @@ const tests = { `, }, { - code: ` + code: normalizeIndent` function MyComponent() { const ref = useRef(); useEffect(() => { @@ -322,7 +332,7 @@ const tests = { `, }, { - code: ` + code: normalizeIndent` function MyComponent({ maybeRef2, foo }) { const definitelyRef1 = useRef(); const definitelyRef2 = useRef(); @@ -376,7 +386,7 @@ const tests = { `, }, { - code: ` + code: normalizeIndent` function MyComponent({ maybeRef2 }) { const definitelyRef1 = useRef(); const definitelyRef2 = useRef(); @@ -434,7 +444,7 @@ const tests = { `, }, { - code: ` + code: normalizeIndent` const MyComponent = forwardRef((props, ref) => { useImperativeHandle(ref, () => ({ focus() { @@ -445,7 +455,7 @@ const tests = { `, }, { - code: ` + code: normalizeIndent` const MyComponent = forwardRef((props, ref) => { useImperativeHandle(ref, () => ({ focus() { @@ -459,7 +469,7 @@ const tests = { // This is not ideal but warning would likely create // too many false positives. We do, however, prevent // direct assignments. - code: ` + code: normalizeIndent` function MyComponent(props) { let obj = {}; useEffect(() => { @@ -472,7 +482,7 @@ const tests = { // Valid because we assign ref.current // ourselves. Therefore it's likely not // a ref managed by React. - code: ` + code: normalizeIndent` function MyComponent() { const myRef = useRef(); useEffect(() => { @@ -490,7 +500,7 @@ const tests = { // Valid because we assign ref.current // ourselves. Therefore it's likely not // a ref managed by React. - code: ` + code: normalizeIndent` function useMyThing(myRef) { useEffect(() => { const handleMove = () => {}; @@ -504,7 +514,7 @@ const tests = { }, { // Valid because the ref is captured. - code: ` + code: normalizeIndent` function MyComponent() { const myRef = useRef(); useEffect(() => { @@ -519,7 +529,7 @@ const tests = { }, { // Valid because the ref is captured. - code: ` + code: normalizeIndent` function useMyThing(myRef) { useEffect(() => { const handleMove = () => {}; @@ -533,7 +543,7 @@ const tests = { }, { // Valid because it's not an effect. - code: ` + code: normalizeIndent` function useMyThing(myRef) { useCallback(() => { const handleMouse = () => {}; @@ -551,7 +561,7 @@ const tests = { }, { // Valid because we read ref.current in a function that isn't cleanup. - code: ` + code: normalizeIndent` function useMyThing() { const myRef = useRef(); useEffect(() => { @@ -567,7 +577,7 @@ const tests = { }, { // Valid because we read ref.current in a function that isn't cleanup. - code: ` + code: normalizeIndent` function useMyThing() { const myRef = useRef(); useEffect(() => { @@ -583,7 +593,7 @@ const tests = { }, { // Valid because it's a primitive constant. - code: ` + code: normalizeIndent` function MyComponent() { const local1 = 42; const local2 = '42'; @@ -598,7 +608,7 @@ const tests = { }, { // It's not a mistake to specify constant values though. - code: ` + code: normalizeIndent` function MyComponent() { const local1 = 42; const local2 = '42'; @@ -613,7 +623,7 @@ const tests = { }, { // It is valid for effects to over-specify their deps. - code: ` + code: normalizeIndent` function MyComponent(props) { const local = props.local; useEffect(() => {}, [local]); @@ -623,7 +633,7 @@ const tests = { { // Valid even though activeTab is "unused". // We allow over-specifying deps for effects, but not callbacks or memo. - code: ` + code: normalizeIndent` function Foo({ activeTab }) { useEffect(() => { window.scrollTo(0, 0); @@ -634,7 +644,7 @@ const tests = { { // It is valid to specify broader effect deps than strictly necessary. // Don't warn for this. - code: ` + code: normalizeIndent` function MyComponent(props) { useEffect(() => { console.log(props.foo.bar.baz); @@ -654,7 +664,7 @@ const tests = { { // It is *also* valid to specify broader memo/callback deps than strictly necessary. // Don't warn for this either. - code: ` + code: normalizeIndent` function MyComponent(props) { const fn = useCallback(() => { console.log(props.foo.bar.baz); @@ -674,7 +684,7 @@ const tests = { { // Declaring handleNext is optional because // it doesn't use anything in the function scope. - code: ` + code: normalizeIndent` function MyComponent(props) { function handleNext1() { console.log('hello'); @@ -700,7 +710,7 @@ const tests = { { // Declaring handleNext is optional because // it doesn't use anything in the function scope. - code: ` + code: normalizeIndent` function MyComponent(props) { function handleNext() { console.log('hello'); @@ -720,7 +730,7 @@ const tests = { { // Declaring handleNext is optional because // everything they use is fully static. - code: ` + code: normalizeIndent` function MyComponent(props) { let [, setState] = useState(); let [, dispatch] = React.useReducer(); @@ -751,7 +761,7 @@ const tests = { `, }, { - code: ` + code: normalizeIndent` function useInterval(callback, delay) { const savedCallback = useRef(); useEffect(() => { @@ -770,7 +780,7 @@ const tests = { `, }, { - code: ` + code: normalizeIndent` function Counter() { const [count, setCount] = useState(0); @@ -786,7 +796,7 @@ const tests = { `, }, { - code: ` + code: normalizeIndent` function Counter() { const [count, setCount] = useState(0); @@ -806,7 +816,7 @@ const tests = { `, }, { - code: ` + code: normalizeIndent` function Counter() { const [count, dispatch] = useReducer((state, action) => { if (action === 'inc') { @@ -826,7 +836,7 @@ const tests = { `, }, { - code: ` + code: normalizeIndent` function Counter() { const [count, dispatch] = useReducer((state, action) => { if (action === 'inc') { @@ -849,7 +859,7 @@ const tests = { }, { // Regression test for a crash - code: ` + code: normalizeIndent` function Podcasts() { useEffect(() => { setPodcasts([]); @@ -859,7 +869,7 @@ const tests = { `, }, { - code: ` + code: normalizeIndent` function withFetch(fetchPodcasts) { return function Podcasts({ id }) { let [podcasts, setPodcasts] = useState(null); @@ -871,7 +881,7 @@ const tests = { `, }, { - code: ` + code: normalizeIndent` function Podcasts({ id }) { let [podcasts, setPodcasts] = useState(null); useEffect(() => { @@ -884,7 +894,7 @@ const tests = { `, }, { - code: ` + code: normalizeIndent` function Counter() { let [count, setCount] = useState(0); @@ -904,7 +914,7 @@ const tests = { `, }, { - code: ` + code: normalizeIndent` function Counter() { let [count, setCount] = useState(0); @@ -924,7 +934,7 @@ const tests = { `, }, { - code: ` + code: normalizeIndent` import increment from './increment'; function Counter() { let [count, setCount] = useState(0); @@ -941,7 +951,7 @@ const tests = { `, }, { - code: ` + code: normalizeIndent` function withStuff(increment) { return function Counter() { let [count, setCount] = useState(0); @@ -959,7 +969,7 @@ const tests = { `, }, { - code: ` + code: normalizeIndent` function App() { const [query, setQuery] = useState('react'); const [state, setState] = useState(null); @@ -982,7 +992,7 @@ const tests = { `, }, { - code: ` + code: normalizeIndent` function Example() { const foo = useCallback(() => { foo(); @@ -991,7 +1001,7 @@ const tests = { `, }, { - code: ` + code: normalizeIndent` function Example({ prop }) { const foo = useCallback(() => { if (prop) { @@ -1002,7 +1012,7 @@ const tests = { `, }, { - code: ` + code: normalizeIndent` function Hello() { const [state, setState] = useState(0); useEffect(() => { @@ -1015,7 +1025,7 @@ const tests = { }, // Ignore Generic Type Variables for arrow functions { - code: ` + code: normalizeIndent` function Example({ prop }) { const bar = useEffect((a: T): Hello => { prop(); @@ -1025,7 +1035,7 @@ const tests = { }, // Ignore arguments keyword for arrow functions. { - code: ` + code: normalizeIndent` function Example() { useEffect(() => { arguments @@ -1034,7 +1044,7 @@ const tests = { `, }, { - code: ` + code: normalizeIndent` function Example() { useEffect(() => { const bar = () => { @@ -1048,7 +1058,7 @@ const tests = { ], invalid: [ { - code: ` + code: normalizeIndent` function MyComponent() { const local = {}; useEffect(() => { @@ -1056,24 +1066,32 @@ const tests = { }, []); } `, - output: ` - function MyComponent() { - const local = {}; - useEffect(() => { - console.log(local); - }, [local]); - } - `, errors: [ - "React Hook useEffect has a missing dependency: 'local'. " + - 'Either include it or remove the dependency array.', + { + message: + "React Hook useEffect has a missing dependency: 'local'. " + + 'Either include it or remove the dependency array.', + suggestions: [ + { + desc: 'Update the dependencies array to be: [local]', + output: normalizeIndent` + function MyComponent() { + const local = {}; + useEffect(() => { + console.log(local); + }, [local]); + } + `, + }, + ], + }, ], }, { // Note: we *could* detect it's a primitive and never assigned // even though it's not a constant -- but we currently don't. // So this is an error. - code: ` + code: normalizeIndent` function MyComponent() { let local = 42; useEffect(() => { @@ -1081,22 +1099,30 @@ const tests = { }, []); } `, - output: ` - function MyComponent() { - let local = 42; - useEffect(() => { - console.log(local); - }, [local]); - } - `, errors: [ - "React Hook useEffect has a missing dependency: 'local'. " + - 'Either include it or remove the dependency array.', + { + message: + "React Hook useEffect has a missing dependency: 'local'. " + + 'Either include it or remove the dependency array.', + suggestions: [ + { + desc: 'Update the dependencies array to be: [local]', + output: normalizeIndent` + function MyComponent() { + let local = 42; + useEffect(() => { + console.log(local); + }, [local]); + } + `, + }, + ], + }, ], }, { // Regexes are literals but potentially stateful. - code: ` + code: normalizeIndent` function MyComponent() { const local = /foo/; useEffect(() => { @@ -1104,44 +1130,54 @@ const tests = { }, []); } `, - output: ` - function MyComponent() { - const local = /foo/; - useEffect(() => { - console.log(local); - }, [local]); - } - `, errors: [ - "React Hook useEffect has a missing dependency: 'local'. " + - 'Either include it or remove the dependency array.', + { + message: + "React Hook useEffect has a missing dependency: 'local'. " + + 'Either include it or remove the dependency array.', + suggestions: [ + { + desc: 'Update the dependencies array to be: [local]', + output: normalizeIndent` + function MyComponent() { + const local = /foo/; + useEffect(() => { + console.log(local); + }, [local]); + } + `, + }, + ], + }, ], }, { // Invalid because they don't have a meaning without deps. - code: ` + code: normalizeIndent` function MyComponent(props) { const value = useMemo(() => { return 2*2; }); const fn = useCallback(() => { alert('foo'); }); } `, // We don't know what you meant. - output: ` - function MyComponent(props) { - const value = useMemo(() => { return 2*2; }); - const fn = useCallback(() => { alert('foo'); }); - } - `, errors: [ - 'React Hook useMemo does nothing when called with only one argument. ' + - 'Did you forget to pass an array of dependencies?', - 'React Hook useCallback does nothing when called with only one argument. ' + - 'Did you forget to pass an array of dependencies?', + { + message: + 'React Hook useMemo does nothing when called with only one argument. ' + + 'Did you forget to pass an array of dependencies?', + suggestions: undefined, + }, + { + message: + 'React Hook useCallback does nothing when called with only one argument. ' + + 'Did you forget to pass an array of dependencies?', + suggestions: undefined, + }, ], }, { // Regression test - code: ` + code: normalizeIndent` function MyComponent() { const local = {}; useEffect(() => { @@ -1151,24 +1187,32 @@ const tests = { }, []); } `, - output: ` - function MyComponent() { - const local = {}; - useEffect(() => { - if (true) { - console.log(local); - } - }, [local]); - } - `, errors: [ - "React Hook useEffect has a missing dependency: 'local'. " + - 'Either include it or remove the dependency array.', + { + message: + "React Hook useEffect has a missing dependency: 'local'. " + + 'Either include it or remove the dependency array.', + suggestions: [ + { + desc: 'Update the dependencies array to be: [local]', + output: normalizeIndent` + function MyComponent() { + const local = {}; + useEffect(() => { + if (true) { + console.log(local); + } + }, [local]); + } + `, + }, + ], + }, ], }, { // Regression test - code: ` + code: normalizeIndent` function MyComponent() { const local = {}; useEffect(() => { @@ -1178,24 +1222,32 @@ const tests = { }, []); } `, - output: ` - function MyComponent() { - const local = {}; - useEffect(() => { - try { - console.log(local); - } finally {} - }, [local]); - } - `, errors: [ - "React Hook useEffect has a missing dependency: 'local'. " + - 'Either include it or remove the dependency array.', + { + message: + "React Hook useEffect has a missing dependency: 'local'. " + + 'Either include it or remove the dependency array.', + suggestions: [ + { + desc: 'Update the dependencies array to be: [local]', + output: normalizeIndent` + function MyComponent() { + const local = {}; + useEffect(() => { + try { + console.log(local); + } finally {} + }, [local]); + } + `, + }, + ], + }, ], }, { // Regression test - code: ` + code: normalizeIndent` function MyComponent() { const local = {}; useEffect(() => { @@ -1206,24 +1258,32 @@ const tests = { }, []); } `, - output: ` - function MyComponent() { - const local = {}; - useEffect(() => { - function inner() { - console.log(local); - } - inner(); - }, [local]); - } - `, errors: [ - "React Hook useEffect has a missing dependency: 'local'. " + - 'Either include it or remove the dependency array.', + { + message: + "React Hook useEffect has a missing dependency: 'local'. " + + 'Either include it or remove the dependency array.', + suggestions: [ + { + desc: 'Update the dependencies array to be: [local]', + output: normalizeIndent` + function MyComponent() { + const local = {}; + useEffect(() => { + function inner() { + console.log(local); + } + inner(); + }, [local]); + } + `, + }, + ], + }, ], }, { - code: ` + code: normalizeIndent` function MyComponent() { const local1 = {}; { @@ -1235,25 +1295,33 @@ const tests = { } } `, - output: ` - function MyComponent() { - const local1 = {}; - { - const local2 = {}; - useEffect(() => { - console.log(local1); - console.log(local2); - }, [local1, local2]); - } - } - `, errors: [ - "React Hook useEffect has missing dependencies: 'local1' and 'local2'. " + - 'Either include them or remove the dependency array.', + { + message: + "React Hook useEffect has missing dependencies: 'local1' and 'local2'. " + + 'Either include them or remove the dependency array.', + suggestions: [ + { + desc: 'Update the dependencies array to be: [local1, local2]', + output: normalizeIndent` + function MyComponent() { + const local1 = {}; + { + const local2 = {}; + useEffect(() => { + console.log(local1); + console.log(local2); + }, [local1, local2]); + } + } + `, + }, + ], + }, ], }, { - code: ` + code: normalizeIndent` function MyComponent() { const local1 = {}; const local2 = {}; @@ -1263,23 +1331,31 @@ const tests = { }, [local1]); } `, - output: ` - function MyComponent() { - const local1 = {}; - const local2 = {}; - useEffect(() => { - console.log(local1); - console.log(local2); - }, [local1, local2]); - } - `, errors: [ - "React Hook useEffect has a missing dependency: 'local2'. " + - 'Either include it or remove the dependency array.', + { + message: + "React Hook useEffect has a missing dependency: 'local2'. " + + 'Either include it or remove the dependency array.', + suggestions: [ + { + desc: 'Update the dependencies array to be: [local1, local2]', + output: normalizeIndent` + function MyComponent() { + const local1 = {}; + const local2 = {}; + useEffect(() => { + console.log(local1); + console.log(local2); + }, [local1, local2]); + } + `, + }, + ], + }, ], }, { - code: ` + code: normalizeIndent` function MyComponent() { const local1 = {}; const local2 = {}; @@ -1288,22 +1364,30 @@ const tests = { }, [local1, local2]); } `, - output: ` - function MyComponent() { - const local1 = {}; - const local2 = {}; - useMemo(() => { - console.log(local1); - }, [local1]); - } - `, errors: [ - "React Hook useMemo has an unnecessary dependency: 'local2'. " + - 'Either exclude it or remove the dependency array.', + { + message: + "React Hook useMemo has an unnecessary dependency: 'local2'. " + + 'Either exclude it or remove the dependency array.', + suggestions: [ + { + desc: 'Update the dependencies array to be: [local1]', + output: normalizeIndent` + function MyComponent() { + const local1 = {}; + const local2 = {}; + useMemo(() => { + console.log(local1); + }, [local1]); + } + `, + }, + ], + }, ], }, { - code: ` + code: normalizeIndent` function MyComponent() { const local1 = {}; function MyNestedComponent() { @@ -1315,27 +1399,35 @@ const tests = { } } `, - output: ` - function MyComponent() { - const local1 = {}; - function MyNestedComponent() { - const local2 = {}; - useCallback(() => { - console.log(local1); - console.log(local2); - }, [local2]); - } - } - `, errors: [ - "React Hook useCallback has a missing dependency: 'local2'. " + - 'Either include it or remove the dependency array. ' + - "Outer scope values like 'local1' aren't valid dependencies " + - "because mutating them doesn't re-render the component.", + { + message: + "React Hook useCallback has a missing dependency: 'local2'. " + + 'Either include it or remove the dependency array. ' + + "Outer scope values like 'local1' aren't valid dependencies " + + "because mutating them doesn't re-render the component.", + suggestions: [ + { + desc: 'Update the dependencies array to be: [local2]', + output: normalizeIndent` + function MyComponent() { + const local1 = {}; + function MyNestedComponent() { + const local2 = {}; + useCallback(() => { + console.log(local1); + console.log(local2); + }, [local2]); + } + } + `, + }, + ], + }, ], }, { - code: ` + code: normalizeIndent` function MyComponent() { const local = {}; useEffect(() => { @@ -1344,22 +1436,30 @@ const tests = { }, []); } `, - output: ` - function MyComponent() { - const local = {}; - useEffect(() => { - console.log(local); - console.log(local); - }, [local]); - } - `, errors: [ - "React Hook useEffect has a missing dependency: 'local'. " + - 'Either include it or remove the dependency array.', + { + message: + "React Hook useEffect has a missing dependency: 'local'. " + + 'Either include it or remove the dependency array.', + suggestions: [ + { + desc: 'Update the dependencies array to be: [local]', + output: normalizeIndent` + function MyComponent() { + const local = {}; + useEffect(() => { + console.log(local); + console.log(local); + }, [local]); + } + `, + }, + ], + }, ], }, { - code: ` + code: normalizeIndent` function MyComponent() { const local = {}; useEffect(() => { @@ -1368,80 +1468,112 @@ const tests = { }, [local, local]); } `, - output: ` - function MyComponent() { - const local = {}; - useEffect(() => { - console.log(local); - console.log(local); - }, [local]); - } - `, errors: [ - "React Hook useEffect has a duplicate dependency: 'local'. " + - 'Either omit it or remove the dependency array.', + { + message: + "React Hook useEffect has a duplicate dependency: 'local'. " + + 'Either omit it or remove the dependency array.', + suggestions: [ + { + desc: 'Update the dependencies array to be: [local]', + output: normalizeIndent` + function MyComponent() { + const local = {}; + useEffect(() => { + console.log(local); + console.log(local); + }, [local]); + } + `, + }, + ], + }, ], }, { - code: ` + code: normalizeIndent` function MyComponent() { useCallback(() => {}, [window]); } `, - output: ` - function MyComponent() { - useCallback(() => {}, []); - } - `, errors: [ - "React Hook useCallback has an unnecessary dependency: 'window'. " + - 'Either exclude it or remove the dependency array. ' + - "Outer scope values like 'window' aren't valid dependencies " + - "because mutating them doesn't re-render the component.", + { + message: + "React Hook useCallback has an unnecessary dependency: 'window'. " + + 'Either exclude it or remove the dependency array. ' + + "Outer scope values like 'window' aren't valid dependencies " + + "because mutating them doesn't re-render the component.", + suggestions: [ + { + desc: 'Update the dependencies array to be: []', + output: normalizeIndent` + function MyComponent() { + useCallback(() => {}, []); + } + `, + }, + ], + }, ], }, { // It is not valid for useCallback to specify extraneous deps // because it doesn't serve as a side effect trigger unlike useEffect. - code: ` + code: normalizeIndent` function MyComponent(props) { let local = props.foo; useCallback(() => {}, [local]); } `, - output: ` - function MyComponent(props) { - let local = props.foo; - useCallback(() => {}, []); - } - `, errors: [ - "React Hook useCallback has an unnecessary dependency: 'local'. " + - 'Either exclude it or remove the dependency array.', + { + message: + "React Hook useCallback has an unnecessary dependency: 'local'. " + + 'Either exclude it or remove the dependency array.', + suggestions: [ + { + desc: 'Update the dependencies array to be: []', + output: normalizeIndent` + function MyComponent(props) { + let local = props.foo; + useCallback(() => {}, []); + } + `, + }, + ], + }, ], }, { - code: ` + code: normalizeIndent` function MyComponent({ history }) { useEffect(() => { return history.listen(); }, []); } `, - output: ` - function MyComponent({ history }) { - useEffect(() => { - return history.listen(); - }, [history]); - } - `, errors: [ - "React Hook useEffect has a missing dependency: 'history'. " + - 'Either include it or remove the dependency array.', + { + message: + "React Hook useEffect has a missing dependency: 'history'. " + + 'Either include it or remove the dependency array.', + suggestions: [ + { + desc: 'Update the dependencies array to be: [history]', + output: normalizeIndent` + function MyComponent({ history }) { + useEffect(() => { + return history.listen(); + }, [history]); + } + `, + }, + ], + }, ], }, { - code: ` + code: normalizeIndent` function MyComponent({ history }) { useEffect(() => { return [ @@ -1451,107 +1583,148 @@ const tests = { }, []); } `, - output: ` - function MyComponent({ history }) { - useEffect(() => { - return [ - history.foo.bar[2].dobedo.listen(), - history.foo.bar().dobedo.listen[2] - ]; - }, [history.foo]); - } - `, errors: [ - "React Hook useEffect has a missing dependency: 'history.foo'. " + - 'Either include it or remove the dependency array.', + { + message: + "React Hook useEffect has a missing dependency: 'history.foo'. " + + 'Either include it or remove the dependency array.', + suggestions: [ + { + desc: 'Update the dependencies array to be: [history.foo]', + output: normalizeIndent` + function MyComponent({ history }) { + useEffect(() => { + return [ + history.foo.bar[2].dobedo.listen(), + history.foo.bar().dobedo.listen[2] + ]; + }, [history.foo]); + } + `, + }, + ], + }, ], }, { - code: ` - function MyComponent() { - useEffect(() => {}, ['foo']); - } - `, - // TODO: we could autofix this. - output: ` + code: normalizeIndent` function MyComponent() { useEffect(() => {}, ['foo']); } `, errors: [ - // Don't assume user meant `foo` because it's not used in the effect. - "The 'foo' literal is not a valid dependency because it never changes. " + - 'You can safely remove it.', + { + message: + // Don't assume user meant `foo` because it's not used in the effect. + "The 'foo' literal is not a valid dependency because it never changes. " + + 'You can safely remove it.', + // TODO: provide suggestion. + suggestions: undefined, + }, ], }, { - code: ` + code: normalizeIndent` function MyComponent({ foo, bar, baz }) { useEffect(() => { console.log(foo, bar, baz); }, ['foo', 'bar']); } `, - output: ` - function MyComponent({ foo, bar, baz }) { - useEffect(() => { - console.log(foo, bar, baz); - }, [bar, baz, foo]); - } - `, errors: [ - "React Hook useEffect has missing dependencies: 'bar', 'baz', and 'foo'. " + - 'Either include them or remove the dependency array.', - "The 'foo' literal is not a valid dependency because it never changes. " + - 'Did you mean to include foo in the array instead?', - "The 'bar' literal is not a valid dependency because it never changes. " + - 'Did you mean to include bar in the array instead?', + { + message: + "React Hook useEffect has missing dependencies: 'bar', 'baz', and 'foo'. " + + 'Either include them or remove the dependency array.', + suggestions: [ + { + desc: 'Update the dependencies array to be: [bar, baz, foo]', + output: normalizeIndent` + function MyComponent({ foo, bar, baz }) { + useEffect(() => { + console.log(foo, bar, baz); + }, [bar, baz, foo]); + } + `, + }, + ], + }, + { + message: + "The 'foo' literal is not a valid dependency because it never changes. " + + 'Did you mean to include foo in the array instead?', + suggestions: undefined, + }, + { + message: + "The 'bar' literal is not a valid dependency because it never changes. " + + 'Did you mean to include bar in the array instead?', + suggestions: undefined, + }, ], }, { - code: ` + code: normalizeIndent` function MyComponent({ foo, bar, baz }) { useEffect(() => { console.log(foo, bar, baz); }, [42, false, null]); } `, - output: ` - function MyComponent({ foo, bar, baz }) { - useEffect(() => { - console.log(foo, bar, baz); - }, [bar, baz, foo]); - } - `, errors: [ - "React Hook useEffect has missing dependencies: 'bar', 'baz', and 'foo'. " + - 'Either include them or remove the dependency array.', - 'The 42 literal is not a valid dependency because it never changes. You can safely remove it.', - 'The false literal is not a valid dependency because it never changes. You can safely remove it.', - 'The null literal is not a valid dependency because it never changes. You can safely remove it.', + { + message: + "React Hook useEffect has missing dependencies: 'bar', 'baz', and 'foo'. " + + 'Either include them or remove the dependency array.', + suggestions: [ + { + desc: 'Update the dependencies array to be: [bar, baz, foo]', + output: normalizeIndent` + function MyComponent({ foo, bar, baz }) { + useEffect(() => { + console.log(foo, bar, baz); + }, [bar, baz, foo]); + } + `, + }, + ], + }, + { + message: + 'The 42 literal is not a valid dependency because it never changes. You can safely remove it.', + suggestions: undefined, + }, + { + message: + 'The false literal is not a valid dependency because it never changes. You can safely remove it.', + suggestions: undefined, + }, + { + message: + 'The null literal is not a valid dependency because it never changes. You can safely remove it.', + suggestions: undefined, + }, ], }, { - code: ` - function MyComponent() { - const dependencies = []; - useEffect(() => {}, dependencies); - } - `, - output: ` + code: normalizeIndent` function MyComponent() { const dependencies = []; useEffect(() => {}, dependencies); } `, errors: [ - 'React Hook useEffect was passed a dependency list that is not an ' + - "array literal. This means we can't statically verify whether you've " + - 'passed the correct dependencies.', + { + message: + 'React Hook useEffect was passed a dependency list that is not an ' + + "array literal. This means we can't statically verify whether you've " + + 'passed the correct dependencies.', + suggestions: undefined, + }, ], }, { - code: ` + code: normalizeIndent` function MyComponent() { const local = {}; const dependencies = [local]; @@ -1560,26 +1733,38 @@ const tests = { }, dependencies); } `, - // TODO: should this autofix or bail out? - output: ` - function MyComponent() { - const local = {}; - const dependencies = [local]; - useEffect(() => { - console.log(local); - }, [local]); - } - `, errors: [ - 'React Hook useEffect was passed a dependency list that is not an ' + - "array literal. This means we can't statically verify whether you've " + - 'passed the correct dependencies.', - "React Hook useEffect has a missing dependency: 'local'. " + - 'Either include it or remove the dependency array.', + { + message: + 'React Hook useEffect was passed a dependency list that is not an ' + + "array literal. This means we can't statically verify whether you've " + + 'passed the correct dependencies.', + // TODO: should this autofix or bail out? + suggestions: undefined, + }, + { + message: + "React Hook useEffect has a missing dependency: 'local'. " + + 'Either include it or remove the dependency array.', + suggestions: [ + { + desc: 'Update the dependencies array to be: [local]', + output: normalizeIndent` + function MyComponent() { + const local = {}; + const dependencies = [local]; + useEffect(() => { + console.log(local); + }, [local]); + } + `, + }, + ], + }, ], }, { - code: ` + code: normalizeIndent` function MyComponent() { const local = {}; const dependencies = [local]; @@ -1588,34 +1773,38 @@ const tests = { }, [...dependencies]); } `, - // TODO: should this autofix or bail out? - output: ` - function MyComponent() { - const local = {}; - const dependencies = [local]; - useEffect(() => { - console.log(local); - }, [local]); - } - `, errors: [ - "React Hook useEffect has a missing dependency: 'local'. " + - 'Either include it or remove the dependency array.', - 'React Hook useEffect has a spread element in its dependency array. ' + - "This means we can't statically verify whether you've passed the " + - 'correct dependencies.', + { + message: + "React Hook useEffect has a missing dependency: 'local'. " + + 'Either include it or remove the dependency array.', + suggestions: [ + { + desc: 'Update the dependencies array to be: [local]', + output: normalizeIndent` + function MyComponent() { + const local = {}; + const dependencies = [local]; + useEffect(() => { + console.log(local); + }, [local]); + } + `, + }, + ], + }, + { + message: + 'React Hook useEffect has a spread element in its dependency array. ' + + "This means we can't statically verify whether you've passed the " + + 'correct dependencies.', + // TODO: should this autofix or bail out? + suggestions: undefined, + }, ], }, { - code: ` - function MyComponent() { - const local = {}; - useEffect(() => { - console.log(local); - }, [local, ...dependencies]); - } - `, - output: ` + code: normalizeIndent` function MyComponent() { const local = {}; useEffect(() => { @@ -1624,13 +1813,17 @@ const tests = { } `, errors: [ - 'React Hook useEffect has a spread element in its dependency array. ' + - "This means we can't statically verify whether you've passed the " + - 'correct dependencies.', + { + message: + 'React Hook useEffect has a spread element in its dependency array. ' + + "This means we can't statically verify whether you've passed the " + + 'correct dependencies.', + suggestions: undefined, + }, ], }, { - code: ` + code: normalizeIndent` function MyComponent() { const local = {}; useEffect(() => { @@ -1638,55 +1831,71 @@ const tests = { }, [computeCacheKey(local)]); } `, - // TODO: I'm not sure this is a good idea. - // Maybe bail out? - output: ` - function MyComponent() { - const local = {}; - useEffect(() => { - console.log(local); - }, [local]); - } - `, errors: [ - "React Hook useEffect has a missing dependency: 'local'. " + - 'Either include it or remove the dependency array.', - 'React Hook useEffect has a complex expression in the dependency array. ' + - 'Extract it to a separate variable so it can be statically checked.', + { + message: + "React Hook useEffect has a missing dependency: 'local'. " + + 'Either include it or remove the dependency array.', + // TODO: I'm not sure this is a good idea. + // Maybe bail out? + suggestions: [ + { + desc: 'Update the dependencies array to be: [local]', + output: normalizeIndent` + function MyComponent() { + const local = {}; + useEffect(() => { + console.log(local); + }, [local]); + } + `, + }, + ], + }, + { + message: + 'React Hook useEffect has a complex expression in the dependency array. ' + + 'Extract it to a separate variable so it can be statically checked.', + suggestions: undefined, + }, ], }, { - code: ` + code: normalizeIndent` function MyComponent(props) { useEffect(() => { console.log(props.items[0]); }, [props.items[0]]); } `, - output: ` - function MyComponent(props) { - useEffect(() => { - console.log(props.items[0]); - }, [props.items]); - } - `, errors: [ - "React Hook useEffect has a missing dependency: 'props.items'. " + - 'Either include it or remove the dependency array.', - 'React Hook useEffect has a complex expression in the dependency array. ' + - 'Extract it to a separate variable so it can be statically checked.', + { + message: + "React Hook useEffect has a missing dependency: 'props.items'. " + + 'Either include it or remove the dependency array.', + suggestions: [ + { + desc: 'Update the dependencies array to be: [props.items]', + output: normalizeIndent` + function MyComponent(props) { + useEffect(() => { + console.log(props.items[0]); + }, [props.items]); + } + `, + }, + ], + }, + { + message: + 'React Hook useEffect has a complex expression in the dependency array. ' + + 'Extract it to a separate variable so it can be statically checked.', + suggestions: undefined, + }, ], }, { - code: ` - function MyComponent(props) { - useEffect(() => { - console.log(props.items[0]); - }, [props.items, props.items[0]]); - } - `, - // TODO: ideally autofix would remove the bad expression? - output: ` + code: normalizeIndent` function MyComponent(props) { useEffect(() => { console.log(props.items[0]); @@ -1694,42 +1903,51 @@ const tests = { } `, errors: [ - 'React Hook useEffect has a complex expression in the dependency array. ' + - 'Extract it to a separate variable so it can be statically checked.', + { + message: + 'React Hook useEffect has a complex expression in the dependency array. ' + + 'Extract it to a separate variable so it can be statically checked.', + // TODO: ideally suggestion would remove the bad expression? + suggestions: undefined, + }, ], }, { - code: ` + code: normalizeIndent` function MyComponent({ items }) { useEffect(() => { console.log(items[0]); }, [items[0]]); } `, - output: ` - function MyComponent({ items }) { - useEffect(() => { - console.log(items[0]); - }, [items]); - } - `, errors: [ - "React Hook useEffect has a missing dependency: 'items'. " + - 'Either include it or remove the dependency array.', - 'React Hook useEffect has a complex expression in the dependency array. ' + - 'Extract it to a separate variable so it can be statically checked.', + { + message: + "React Hook useEffect has a missing dependency: 'items'. " + + 'Either include it or remove the dependency array.', + suggestions: [ + { + desc: 'Update the dependencies array to be: [items]', + output: normalizeIndent` + function MyComponent({ items }) { + useEffect(() => { + console.log(items[0]); + }, [items]); + } + `, + }, + ], + }, + { + message: + 'React Hook useEffect has a complex expression in the dependency array. ' + + 'Extract it to a separate variable so it can be statically checked.', + suggestions: undefined, + }, ], }, { - code: ` - function MyComponent({ items }) { - useEffect(() => { - console.log(items[0]); - }, [items, items[0]]); - } - `, - // TODO: ideally autofix would remove the bad expression? - output: ` + code: normalizeIndent` function MyComponent({ items }) { useEffect(() => { console.log(items[0]); @@ -1737,8 +1955,13 @@ const tests = { } `, errors: [ - 'React Hook useEffect has a complex expression in the dependency array. ' + - 'Extract it to a separate variable so it can be statically checked.', + { + message: + 'React Hook useEffect has a complex expression in the dependency array. ' + + 'Extract it to a separate variable so it can be statically checked.', + // TODO: ideally suggeston would remove the bad expression? + suggestions: undefined, + }, ], }, { @@ -1747,7 +1970,7 @@ const tests = { // However, we generally allow specifying *broader* deps as escape hatch. // So while [props, props.foo] is unnecessary, 'props' wins here as the // broader one, and this is why 'props.foo' is reported as unnecessary. - code: ` + code: normalizeIndent` function MyComponent(props) { const local = {}; useCallback(() => { @@ -1756,23 +1979,31 @@ const tests = { }, [props, props.foo]); } `, - output: ` - function MyComponent(props) { - const local = {}; - useCallback(() => { - console.log(props.foo); - console.log(props.bar); - }, [props]); - } - `, errors: [ - "React Hook useCallback has an unnecessary dependency: 'props.foo'. " + - 'Either exclude it or remove the dependency array.', + { + message: + "React Hook useCallback has an unnecessary dependency: 'props.foo'. " + + 'Either exclude it or remove the dependency array.', + suggestions: [ + { + desc: 'Update the dependencies array to be: [props]', + output: normalizeIndent` + function MyComponent(props) { + const local = {}; + useCallback(() => { + console.log(props.foo); + console.log(props.bar); + }, [props]); + } + `, + }, + ], + }, ], }, { // Since we don't have 'props' in the list, we'll suggest narrow dependencies. - code: ` + code: normalizeIndent` function MyComponent(props) { const local = {}; useCallback(() => { @@ -1781,24 +2012,33 @@ const tests = { }, []); } `, - output: ` - function MyComponent(props) { - const local = {}; - useCallback(() => { - console.log(props.foo); - console.log(props.bar); - }, [props.bar, props.foo]); - } - `, errors: [ - "React Hook useCallback has missing dependencies: 'props.bar' and 'props.foo'. " + - 'Either include them or remove the dependency array.', + { + message: + "React Hook useCallback has missing dependencies: 'props.bar' and 'props.foo'. " + + 'Either include them or remove the dependency array.', + suggestions: [ + { + desc: + 'Update the dependencies array to be: [props.bar, props.foo]', + output: normalizeIndent` + function MyComponent(props) { + const local = {}; + useCallback(() => { + console.log(props.foo); + console.log(props.bar); + }, [props.bar, props.foo]); + } + `, + }, + ], + }, ], }, { // Effects are allowed to over-specify deps. We'll complain about missing // 'local', but we won't remove the already-specified 'local.id' from your list. - code: ` + code: normalizeIndent` function MyComponent() { const local = {id: 42}; useEffect(() => { @@ -1806,23 +2046,31 @@ const tests = { }, [local.id]); } `, - output: ` - function MyComponent() { - const local = {id: 42}; - useEffect(() => { - console.log(local); - }, [local, local.id]); - } - `, errors: [ - "React Hook useEffect has a missing dependency: 'local'. " + - 'Either include it or remove the dependency array.', + { + message: + "React Hook useEffect has a missing dependency: 'local'. " + + 'Either include it or remove the dependency array.', + suggestions: [ + { + desc: 'Update the dependencies array to be: [local, local.id]', + output: normalizeIndent` + function MyComponent() { + const local = {id: 42}; + useEffect(() => { + console.log(local); + }, [local, local.id]); + } + `, + }, + ], + }, ], }, { // Callbacks are not allowed to over-specify deps. So we'll complain about missing // 'local' and we will also *remove* 'local.id' from your list. - code: ` + code: normalizeIndent` function MyComponent() { const local = {id: 42}; const fn = useCallback(() => { @@ -1830,23 +2078,31 @@ const tests = { }, [local.id]); } `, - output: ` - function MyComponent() { - const local = {id: 42}; - const fn = useCallback(() => { - console.log(local); - }, [local]); - } - `, errors: [ - "React Hook useCallback has a missing dependency: 'local'. " + - 'Either include it or remove the dependency array.', + { + message: + "React Hook useCallback has a missing dependency: 'local'. " + + 'Either include it or remove the dependency array.', + suggestions: [ + { + desc: 'Update the dependencies array to be: [local]', + output: normalizeIndent` + function MyComponent() { + const local = {id: 42}; + const fn = useCallback(() => { + console.log(local); + }, [local]); + } + `, + }, + ], + }, ], }, { // Callbacks are not allowed to over-specify deps. So we'll complain about // the unnecessary 'local.id'. - code: ` + code: normalizeIndent` function MyComponent() { const local = {id: 42}; const fn = useCallback(() => { @@ -1854,41 +2110,57 @@ const tests = { }, [local.id, local]); } `, - output: ` - function MyComponent() { - const local = {id: 42}; - const fn = useCallback(() => { - console.log(local); - }, [local]); - } - `, errors: [ - "React Hook useCallback has an unnecessary dependency: 'local.id'. " + - 'Either exclude it or remove the dependency array.', + { + message: + "React Hook useCallback has an unnecessary dependency: 'local.id'. " + + 'Either exclude it or remove the dependency array.', + suggestions: [ + { + desc: 'Update the dependencies array to be: [local]', + output: normalizeIndent` + function MyComponent() { + const local = {id: 42}; + const fn = useCallback(() => { + console.log(local); + }, [local]); + } + `, + }, + ], + }, ], }, { - code: ` + code: normalizeIndent` function MyComponent(props) { const fn = useCallback(() => { console.log(props.foo.bar.baz); }, []); } `, - output: ` - function MyComponent(props) { - const fn = useCallback(() => { - console.log(props.foo.bar.baz); - }, [props.foo.bar.baz]); - } - `, errors: [ - "React Hook useCallback has a missing dependency: 'props.foo.bar.baz'. " + - 'Either include it or remove the dependency array.', + { + message: + "React Hook useCallback has a missing dependency: 'props.foo.bar.baz'. " + + 'Either include it or remove the dependency array.', + suggestions: [ + { + desc: 'Update the dependencies array to be: [props.foo.bar.baz]', + output: normalizeIndent` + function MyComponent(props) { + const fn = useCallback(() => { + console.log(props.foo.bar.baz); + }, [props.foo.bar.baz]); + } + `, + }, + ], + }, ], }, { - code: ` + code: normalizeIndent` function MyComponent(props) { let color = {} const fn = useCallback(() => { @@ -1897,18 +2169,27 @@ const tests = { }, [props.foo, props.foo.bar.baz]); } `, - output: ` - function MyComponent(props) { - let color = {} - const fn = useCallback(() => { - console.log(props.foo.bar.baz); - console.log(color); - }, [color, props.foo.bar.baz]); - } - `, errors: [ - "React Hook useCallback has a missing dependency: 'color'. " + - 'Either include it or remove the dependency array.', + { + message: + "React Hook useCallback has a missing dependency: 'color'. " + + 'Either include it or remove the dependency array.', + suggestions: [ + { + desc: + 'Update the dependencies array to be: [color, props.foo.bar.baz]', + output: normalizeIndent` + function MyComponent(props) { + let color = {} + const fn = useCallback(() => { + console.log(props.foo.bar.baz); + console.log(color); + }, [color, props.foo.bar.baz]); + } + `, + }, + ], + }, ], }, { @@ -1917,27 +2198,35 @@ const tests = { // So in this case we ask you to remove 'props.foo.bar.baz' because 'props.foo' // already covers it, and having both is unnecessary. // TODO: maybe consider suggesting a narrower one by default in these cases. - code: ` + code: normalizeIndent` function MyComponent(props) { const fn = useCallback(() => { console.log(props.foo.bar.baz); }, [props.foo.bar.baz, props.foo]); } `, - output: ` - function MyComponent(props) { - const fn = useCallback(() => { - console.log(props.foo.bar.baz); - }, [props.foo]); - } - `, errors: [ - "React Hook useCallback has an unnecessary dependency: 'props.foo.bar.baz'. " + - 'Either exclude it or remove the dependency array.', + { + message: + "React Hook useCallback has an unnecessary dependency: 'props.foo.bar.baz'. " + + 'Either exclude it or remove the dependency array.', + suggestions: [ + { + desc: 'Update the dependencies array to be: [props.foo]', + output: normalizeIndent` + function MyComponent(props) { + const fn = useCallback(() => { + console.log(props.foo.bar.baz); + }, [props.foo]); + } + `, + }, + ], + }, ], }, { - code: ` + code: normalizeIndent` function MyComponent(props) { const fn = useCallback(() => { console.log(props.foo.bar.baz); @@ -1945,17 +2234,26 @@ const tests = { }, []); } `, - output: ` - function MyComponent(props) { - const fn = useCallback(() => { - console.log(props.foo.bar.baz); - console.log(props.foo.fizz.bizz); - }, [props.foo.bar.baz, props.foo.fizz.bizz]); - } - `, errors: [ - "React Hook useCallback has missing dependencies: 'props.foo.bar.baz' and 'props.foo.fizz.bizz'. " + - 'Either include them or remove the dependency array.', + { + message: + "React Hook useCallback has missing dependencies: 'props.foo.bar.baz' and 'props.foo.fizz.bizz'. " + + 'Either include them or remove the dependency array.', + suggestions: [ + { + desc: + 'Update the dependencies array to be: [props.foo.bar.baz, props.foo.fizz.bizz]', + output: normalizeIndent` + function MyComponent(props) { + const fn = useCallback(() => { + console.log(props.foo.bar.baz); + console.log(props.foo.fizz.bizz); + }, [props.foo.bar.baz, props.foo.fizz.bizz]); + } + `, + }, + ], + }, ], }, { @@ -1965,27 +2263,35 @@ const tests = { // When we're sure there is a mistake, for callbacks we will rebuild the list // from scratch. This will set the user on a better path by default. // This is why we end up with just 'props.foo.bar', and not them both. - code: ` + code: normalizeIndent` function MyComponent(props) { const fn = useCallback(() => { console.log(props.foo.bar); }, [props.foo.bar.baz]); } `, - output: ` - function MyComponent(props) { - const fn = useCallback(() => { - console.log(props.foo.bar); - }, [props.foo.bar]); - } - `, errors: [ - "React Hook useCallback has a missing dependency: 'props.foo.bar'. " + - 'Either include it or remove the dependency array.', + { + message: + "React Hook useCallback has a missing dependency: 'props.foo.bar'. " + + 'Either include it or remove the dependency array.', + suggestions: [ + { + desc: 'Update the dependencies array to be: [props.foo.bar]', + output: normalizeIndent` + function MyComponent(props) { + const fn = useCallback(() => { + console.log(props.foo.bar); + }, [props.foo.bar]); + } + `, + }, + ], + }, ], }, { - code: ` + code: normalizeIndent` function MyComponent(props) { const fn = useCallback(() => { console.log(props); @@ -1993,21 +2299,29 @@ const tests = { }, [props.foo.bar.baz]); } `, - output: ` - function MyComponent(props) { - const fn = useCallback(() => { - console.log(props); - console.log(props.hello); - }, [props]); - } - `, errors: [ - "React Hook useCallback has a missing dependency: 'props'. " + - 'Either include it or remove the dependency array.', + { + message: + "React Hook useCallback has a missing dependency: 'props'. " + + 'Either include it or remove the dependency array.', + suggestions: [ + { + desc: 'Update the dependencies array to be: [props]', + output: normalizeIndent` + function MyComponent(props) { + const fn = useCallback(() => { + console.log(props); + console.log(props.hello); + }, [props]); + } + `, + }, + ], + }, ], }, { - code: ` + code: normalizeIndent` function MyComponent() { const local = {}; useEffect(() => { @@ -2015,21 +2329,29 @@ const tests = { }, [local, local]); } `, - output: ` - function MyComponent() { - const local = {}; - useEffect(() => { - console.log(local); - }, [local]); - } - `, errors: [ - "React Hook useEffect has a duplicate dependency: 'local'. " + - 'Either omit it or remove the dependency array.', + { + message: + "React Hook useEffect has a duplicate dependency: 'local'. " + + 'Either omit it or remove the dependency array.', + suggestions: [ + { + desc: 'Update the dependencies array to be: [local]', + output: normalizeIndent` + function MyComponent() { + const local = {}; + useEffect(() => { + console.log(local); + }, [local]); + } + `, + }, + ], + }, ], }, { - code: ` + code: normalizeIndent` function MyComponent() { const local1 = {}; useCallback(() => { @@ -2038,60 +2360,84 @@ const tests = { }, [local1]); } `, - output: ` - function MyComponent() { - const local1 = {}; - useCallback(() => { - const local1 = {}; - console.log(local1); - }, []); - } - `, errors: [ - "React Hook useCallback has an unnecessary dependency: 'local1'. " + - 'Either exclude it or remove the dependency array.', + { + message: + "React Hook useCallback has an unnecessary dependency: 'local1'. " + + 'Either exclude it or remove the dependency array.', + suggestions: [ + { + desc: 'Update the dependencies array to be: []', + output: normalizeIndent` + function MyComponent() { + const local1 = {}; + useCallback(() => { + const local1 = {}; + console.log(local1); + }, []); + } + `, + }, + ], + }, ], }, { - code: ` + code: normalizeIndent` function MyComponent() { const local1 = {}; useCallback(() => {}, [local1]); } `, - output: ` - function MyComponent() { - const local1 = {}; - useCallback(() => {}, []); - } - `, errors: [ - "React Hook useCallback has an unnecessary dependency: 'local1'. " + - 'Either exclude it or remove the dependency array.', + { + message: + "React Hook useCallback has an unnecessary dependency: 'local1'. " + + 'Either exclude it or remove the dependency array.', + suggestions: [ + { + desc: 'Update the dependencies array to be: []', + output: normalizeIndent` + function MyComponent() { + const local1 = {}; + useCallback(() => {}, []); + } + `, + }, + ], + }, ], }, { - code: ` + code: normalizeIndent` function MyComponent(props) { useEffect(() => { console.log(props.foo); }, []); } `, - output: ` - function MyComponent(props) { - useEffect(() => { - console.log(props.foo); - }, [props.foo]); - } - `, - errors: [ - "React Hook useEffect has a missing dependency: 'props.foo'. " + - 'Either include it or remove the dependency array.', - ], - }, - { - code: ` + errors: [ + { + message: + "React Hook useEffect has a missing dependency: 'props.foo'. " + + 'Either include it or remove the dependency array.', + suggestions: [ + { + desc: 'Update the dependencies array to be: [props.foo]', + output: normalizeIndent` + function MyComponent(props) { + useEffect(() => { + console.log(props.foo); + }, [props.foo]); + } + `, + }, + ], + }, + ], + }, + { + code: normalizeIndent` function MyComponent(props) { useEffect(() => { console.log(props.foo); @@ -2099,21 +2445,30 @@ const tests = { }, []); } `, - output: ` - function MyComponent(props) { - useEffect(() => { - console.log(props.foo); - console.log(props.bar); - }, [props.bar, props.foo]); - } - `, errors: [ - "React Hook useEffect has missing dependencies: 'props.bar' and 'props.foo'. " + - 'Either include them or remove the dependency array.', + { + message: + "React Hook useEffect has missing dependencies: 'props.bar' and 'props.foo'. " + + 'Either include them or remove the dependency array.', + suggestions: [ + { + desc: + 'Update the dependencies array to be: [props.bar, props.foo]', + output: normalizeIndent` + function MyComponent(props) { + useEffect(() => { + console.log(props.foo); + console.log(props.bar); + }, [props.bar, props.foo]); + } + `, + }, + ], + }, ], }, { - code: ` + code: normalizeIndent` function MyComponent(props) { let a, b, c, d, e, f, g; useEffect(() => { @@ -2121,22 +2476,31 @@ const tests = { }, [c, a, g]); } `, - // Don't alphabetize if it wasn't alphabetized in the first place. - output: ` - function MyComponent(props) { - let a, b, c, d, e, f, g; - useEffect(() => { - console.log(b, e, d, c, a, g, f); - }, [c, a, g, b, e, d, f]); - } - `, errors: [ - "React Hook useEffect has missing dependencies: 'b', 'd', 'e', and 'f'. " + - 'Either include them or remove the dependency array.', + { + message: + "React Hook useEffect has missing dependencies: 'b', 'd', 'e', and 'f'. " + + 'Either include them or remove the dependency array.', + // Don't alphabetize if it wasn't alphabetized in the first place. + suggestions: [ + { + desc: + 'Update the dependencies array to be: [c, a, g, b, e, d, f]', + output: normalizeIndent` + function MyComponent(props) { + let a, b, c, d, e, f, g; + useEffect(() => { + console.log(b, e, d, c, a, g, f); + }, [c, a, g, b, e, d, f]); + } + `, + }, + ], + }, ], }, { - code: ` + code: normalizeIndent` function MyComponent(props) { let a, b, c, d, e, f, g; useEffect(() => { @@ -2144,22 +2508,31 @@ const tests = { }, [a, c, g]); } `, - // Alphabetize if it was alphabetized. - output: ` - function MyComponent(props) { - let a, b, c, d, e, f, g; - useEffect(() => { - console.log(b, e, d, c, a, g, f); - }, [a, b, c, d, e, f, g]); - } - `, errors: [ - "React Hook useEffect has missing dependencies: 'b', 'd', 'e', and 'f'. " + - 'Either include them or remove the dependency array.', + { + message: + "React Hook useEffect has missing dependencies: 'b', 'd', 'e', and 'f'. " + + 'Either include them or remove the dependency array.', + // Alphabetize if it was alphabetized. + suggestions: [ + { + desc: + 'Update the dependencies array to be: [a, b, c, d, e, f, g]', + output: normalizeIndent` + function MyComponent(props) { + let a, b, c, d, e, f, g; + useEffect(() => { + console.log(b, e, d, c, a, g, f); + }, [a, b, c, d, e, f, g]); + } + `, + }, + ], + }, ], }, { - code: ` + code: normalizeIndent` function MyComponent(props) { let a, b, c, d, e, f, g; useEffect(() => { @@ -2167,22 +2540,31 @@ const tests = { }, []); } `, - // Alphabetize if it was empty. - output: ` - function MyComponent(props) { - let a, b, c, d, e, f, g; - useEffect(() => { - console.log(b, e, d, c, a, g, f); - }, [a, b, c, d, e, f, g]); - } - `, errors: [ - "React Hook useEffect has missing dependencies: 'a', 'b', 'c', 'd', 'e', 'f', and 'g'. " + - 'Either include them or remove the dependency array.', + { + message: + "React Hook useEffect has missing dependencies: 'a', 'b', 'c', 'd', 'e', 'f', and 'g'. " + + 'Either include them or remove the dependency array.', + // Alphabetize if it was empty. + suggestions: [ + { + desc: + 'Update the dependencies array to be: [a, b, c, d, e, f, g]', + output: normalizeIndent` + function MyComponent(props) { + let a, b, c, d, e, f, g; + useEffect(() => { + console.log(b, e, d, c, a, g, f); + }, [a, b, c, d, e, f, g]); + } + `, + }, + ], + }, ], }, { - code: ` + code: normalizeIndent` function MyComponent(props) { const local = {}; useEffect(() => { @@ -2192,23 +2574,32 @@ const tests = { }, []); } `, - output: ` - function MyComponent(props) { - const local = {}; - useEffect(() => { - console.log(props.foo); - console.log(props.bar); - console.log(local); - }, [local, props.bar, props.foo]); - } - `, errors: [ - "React Hook useEffect has missing dependencies: 'local', 'props.bar', and 'props.foo'. " + - 'Either include them or remove the dependency array.', + { + message: + "React Hook useEffect has missing dependencies: 'local', 'props.bar', and 'props.foo'. " + + 'Either include them or remove the dependency array.', + suggestions: [ + { + desc: + 'Update the dependencies array to be: [local, props.bar, props.foo]', + output: normalizeIndent` + function MyComponent(props) { + const local = {}; + useEffect(() => { + console.log(props.foo); + console.log(props.bar); + console.log(local); + }, [local, props.bar, props.foo]); + } + `, + }, + ], + }, ], }, { - code: ` + code: normalizeIndent` function MyComponent(props) { const local = {}; useEffect(() => { @@ -2218,23 +2609,31 @@ const tests = { }, [props]); } `, - output: ` - function MyComponent(props) { - const local = {}; - useEffect(() => { - console.log(props.foo); - console.log(props.bar); - console.log(local); - }, [local, props]); - } - `, errors: [ - "React Hook useEffect has a missing dependency: 'local'. " + - 'Either include it or remove the dependency array.', + { + message: + "React Hook useEffect has a missing dependency: 'local'. " + + 'Either include it or remove the dependency array.', + suggestions: [ + { + desc: 'Update the dependencies array to be: [local, props]', + output: normalizeIndent` + function MyComponent(props) { + const local = {}; + useEffect(() => { + console.log(props.foo); + console.log(props.bar); + console.log(local); + }, [local, props]); + } + `, + }, + ], + }, ], }, { - code: ` + code: normalizeIndent` function MyComponent(props) { useEffect(() => { console.log(props.foo); @@ -2259,48 +2658,221 @@ const tests = { }, []); } `, - output: ` - function MyComponent(props) { - useEffect(() => { - console.log(props.foo); - }, [props.foo]); - useCallback(() => { - console.log(props.foo); - }, [props.foo]); - useMemo(() => { - console.log(props.foo); - }, [props.foo]); - React.useEffect(() => { - console.log(props.foo); - }, [props.foo]); - React.useCallback(() => { - console.log(props.foo); - }, [props.foo]); - React.useMemo(() => { - console.log(props.foo); - }, [props.foo]); - React.notReactiveHook(() => { - console.log(props.foo); - }, []); - } - `, errors: [ - "React Hook useEffect has a missing dependency: 'props.foo'. " + - 'Either include it or remove the dependency array.', - "React Hook useCallback has a missing dependency: 'props.foo'. " + - 'Either include it or remove the dependency array.', - "React Hook useMemo has a missing dependency: 'props.foo'. " + - 'Either include it or remove the dependency array.', - "React Hook React.useEffect has a missing dependency: 'props.foo'. " + - 'Either include it or remove the dependency array.', - "React Hook React.useCallback has a missing dependency: 'props.foo'. " + - 'Either include it or remove the dependency array.', - "React Hook React.useMemo has a missing dependency: 'props.foo'. " + - 'Either include it or remove the dependency array.', + { + message: + "React Hook useEffect has a missing dependency: 'props.foo'. " + + 'Either include it or remove the dependency array.', + suggestions: [ + { + desc: 'Update the dependencies array to be: [props.foo]', + output: normalizeIndent` + function MyComponent(props) { + useEffect(() => { + console.log(props.foo); + }, [props.foo]); + useCallback(() => { + console.log(props.foo); + }, []); + useMemo(() => { + console.log(props.foo); + }, []); + React.useEffect(() => { + console.log(props.foo); + }, []); + React.useCallback(() => { + console.log(props.foo); + }, []); + React.useMemo(() => { + console.log(props.foo); + }, []); + React.notReactiveHook(() => { + console.log(props.foo); + }, []); + } + `, + }, + ], + }, + { + message: + "React Hook useCallback has a missing dependency: 'props.foo'. " + + 'Either include it or remove the dependency array.', + suggestions: [ + { + desc: 'Update the dependencies array to be: [props.foo]', + output: normalizeIndent` + function MyComponent(props) { + useEffect(() => { + console.log(props.foo); + }, []); + useCallback(() => { + console.log(props.foo); + }, [props.foo]); + useMemo(() => { + console.log(props.foo); + }, []); + React.useEffect(() => { + console.log(props.foo); + }, []); + React.useCallback(() => { + console.log(props.foo); + }, []); + React.useMemo(() => { + console.log(props.foo); + }, []); + React.notReactiveHook(() => { + console.log(props.foo); + }, []); + } + `, + }, + ], + }, + { + message: + "React Hook useMemo has a missing dependency: 'props.foo'. " + + 'Either include it or remove the dependency array.', + suggestions: [ + { + desc: 'Update the dependencies array to be: [props.foo]', + output: normalizeIndent` + function MyComponent(props) { + useEffect(() => { + console.log(props.foo); + }, []); + useCallback(() => { + console.log(props.foo); + }, []); + useMemo(() => { + console.log(props.foo); + }, [props.foo]); + React.useEffect(() => { + console.log(props.foo); + }, []); + React.useCallback(() => { + console.log(props.foo); + }, []); + React.useMemo(() => { + console.log(props.foo); + }, []); + React.notReactiveHook(() => { + console.log(props.foo); + }, []); + } + `, + }, + ], + }, + { + message: + "React Hook React.useEffect has a missing dependency: 'props.foo'. " + + 'Either include it or remove the dependency array.', + suggestions: [ + { + desc: 'Update the dependencies array to be: [props.foo]', + output: normalizeIndent` + function MyComponent(props) { + useEffect(() => { + console.log(props.foo); + }, []); + useCallback(() => { + console.log(props.foo); + }, []); + useMemo(() => { + console.log(props.foo); + }, []); + React.useEffect(() => { + console.log(props.foo); + }, [props.foo]); + React.useCallback(() => { + console.log(props.foo); + }, []); + React.useMemo(() => { + console.log(props.foo); + }, []); + React.notReactiveHook(() => { + console.log(props.foo); + }, []); + } + `, + }, + ], + }, + { + message: + "React Hook React.useCallback has a missing dependency: 'props.foo'. " + + 'Either include it or remove the dependency array.', + suggestions: [ + { + desc: 'Update the dependencies array to be: [props.foo]', + output: normalizeIndent` + function MyComponent(props) { + useEffect(() => { + console.log(props.foo); + }, []); + useCallback(() => { + console.log(props.foo); + }, []); + useMemo(() => { + console.log(props.foo); + }, []); + React.useEffect(() => { + console.log(props.foo); + }, []); + React.useCallback(() => { + console.log(props.foo); + }, [props.foo]); + React.useMemo(() => { + console.log(props.foo); + }, []); + React.notReactiveHook(() => { + console.log(props.foo); + }, []); + } + `, + }, + ], + }, + { + message: + "React Hook React.useMemo has a missing dependency: 'props.foo'. " + + 'Either include it or remove the dependency array.', + suggestions: [ + { + desc: 'Update the dependencies array to be: [props.foo]', + output: normalizeIndent` + function MyComponent(props) { + useEffect(() => { + console.log(props.foo); + }, []); + useCallback(() => { + console.log(props.foo); + }, []); + useMemo(() => { + console.log(props.foo); + }, []); + React.useEffect(() => { + console.log(props.foo); + }, []); + React.useCallback(() => { + console.log(props.foo); + }, []); + React.useMemo(() => { + console.log(props.foo); + }, [props.foo]); + React.notReactiveHook(() => { + console.log(props.foo); + }, []); + } + `, + }, + ], + }, ], }, { - code: ` + code: normalizeIndent` function MyComponent(props) { useCustomEffect(() => { console.log(props.foo); @@ -2316,34 +2888,90 @@ const tests = { }, []); } `, - output: ` - function MyComponent(props) { - useCustomEffect(() => { - console.log(props.foo); - }, [props.foo]); - useEffect(() => { - console.log(props.foo); - }, [props.foo]); - React.useEffect(() => { - console.log(props.foo); - }, [props.foo]); - React.useCustomEffect(() => { - console.log(props.foo); - }, []); - } - `, options: [{additionalHooks: 'useCustomEffect'}], errors: [ - "React Hook useCustomEffect has a missing dependency: 'props.foo'. " + - 'Either include it or remove the dependency array.', - "React Hook useEffect has a missing dependency: 'props.foo'. " + - 'Either include it or remove the dependency array.', - "React Hook React.useEffect has a missing dependency: 'props.foo'. " + - 'Either include it or remove the dependency array.', + { + message: + "React Hook useCustomEffect has a missing dependency: 'props.foo'. " + + 'Either include it or remove the dependency array.', + suggestions: [ + { + desc: 'Update the dependencies array to be: [props.foo]', + output: normalizeIndent` + function MyComponent(props) { + useCustomEffect(() => { + console.log(props.foo); + }, [props.foo]); + useEffect(() => { + console.log(props.foo); + }, []); + React.useEffect(() => { + console.log(props.foo); + }, []); + React.useCustomEffect(() => { + console.log(props.foo); + }, []); + } + `, + }, + ], + }, + { + message: + "React Hook useEffect has a missing dependency: 'props.foo'. " + + 'Either include it or remove the dependency array.', + suggestions: [ + { + desc: 'Update the dependencies array to be: [props.foo]', + output: normalizeIndent` + function MyComponent(props) { + useCustomEffect(() => { + console.log(props.foo); + }, []); + useEffect(() => { + console.log(props.foo); + }, [props.foo]); + React.useEffect(() => { + console.log(props.foo); + }, []); + React.useCustomEffect(() => { + console.log(props.foo); + }, []); + } + `, + }, + ], + }, + { + message: + "React Hook React.useEffect has a missing dependency: 'props.foo'. " + + 'Either include it or remove the dependency array.', + suggestions: [ + { + desc: 'Update the dependencies array to be: [props.foo]', + output: normalizeIndent` + function MyComponent(props) { + useCustomEffect(() => { + console.log(props.foo); + }, []); + useEffect(() => { + console.log(props.foo); + }, []); + React.useEffect(() => { + console.log(props.foo); + }, [props.foo]); + React.useCustomEffect(() => { + console.log(props.foo); + }, []); + } + `, + }, + ], + }, ], }, { - code: ` + code: normalizeIndent` function MyComponent() { const local = {}; useEffect(() => { @@ -2351,24 +2979,36 @@ const tests = { }, [a ? local : b]); } `, - // TODO: should we bail out instead? - output: ` - function MyComponent() { - const local = {}; - useEffect(() => { - console.log(local); - }, [local]); - } - `, errors: [ - "React Hook useEffect has a missing dependency: 'local'. " + - 'Either include it or remove the dependency array.', - 'React Hook useEffect has a complex expression in the dependency array. ' + - 'Extract it to a separate variable so it can be statically checked.', + { + message: + "React Hook useEffect has a missing dependency: 'local'. " + + 'Either include it or remove the dependency array.', + // TODO: should we bail out instead? + suggestions: [ + { + desc: 'Update the dependencies array to be: [local]', + output: normalizeIndent` + function MyComponent() { + const local = {}; + useEffect(() => { + console.log(local); + }, [local]); + } + `, + }, + ], + }, + { + message: + 'React Hook useEffect has a complex expression in the dependency array. ' + + 'Extract it to a separate variable so it can be statically checked.', + suggestions: undefined, + }, ], }, { - code: ` + code: normalizeIndent` function MyComponent() { const local = {}; useEffect(() => { @@ -2376,24 +3016,36 @@ const tests = { }, [a && local]); } `, - // TODO: should we bail out instead? - output: ` - function MyComponent() { - const local = {}; - useEffect(() => { - console.log(local); - }, [local]); - } - `, errors: [ - "React Hook useEffect has a missing dependency: 'local'. " + - 'Either include it or remove the dependency array.', - 'React Hook useEffect has a complex expression in the dependency array. ' + - 'Extract it to a separate variable so it can be statically checked.', + { + message: + "React Hook useEffect has a missing dependency: 'local'. " + + 'Either include it or remove the dependency array.', + // TODO: should we bail out instead? + suggestions: [ + { + desc: 'Update the dependencies array to be: [local]', + output: normalizeIndent` + function MyComponent() { + const local = {}; + useEffect(() => { + console.log(local); + }, [local]); + } + `, + }, + ], + }, + { + message: + 'React Hook useEffect has a complex expression in the dependency array. ' + + 'Extract it to a separate variable so it can be statically checked.', + suggestions: undefined, + }, ], }, { - code: ` + code: normalizeIndent` function MyComponent() { const ref = useRef(); const [state, setState] = useState(); @@ -2403,25 +3055,33 @@ const tests = { }, []); } `, - output: ` - function MyComponent() { - const ref = useRef(); - const [state, setState] = useState(); - useEffect(() => { - ref.current = {}; - setState(state + 1); - }, [state]); - } - `, errors: [ - "React Hook useEffect has a missing dependency: 'state'. " + - 'Either include it or remove the dependency array. ' + - `You can also do a functional update 'setState(s => ...)' ` + - `if you only need 'state' in the 'setState' call.`, + { + message: + "React Hook useEffect has a missing dependency: 'state'. " + + 'Either include it or remove the dependency array. ' + + `You can also do a functional update 'setState(s => ...)' ` + + `if you only need 'state' in the 'setState' call.`, + suggestions: [ + { + desc: 'Update the dependencies array to be: [state]', + output: normalizeIndent` + function MyComponent() { + const ref = useRef(); + const [state, setState] = useState(); + useEffect(() => { + ref.current = {}; + setState(state + 1); + }, [state]); + } + `, + }, + ], + }, ], }, { - code: ` + code: normalizeIndent` function MyComponent() { const ref = useRef(); const [state, setState] = useState(); @@ -2431,28 +3091,36 @@ const tests = { }, [ref]); } `, - // We don't ask to remove static deps but don't add them either. - // Don't suggest removing "ref" (it's fine either way) - // but *do* add "state". *Don't* add "setState" ourselves. - output: ` - function MyComponent() { - const ref = useRef(); - const [state, setState] = useState(); - useEffect(() => { - ref.current = {}; - setState(state + 1); - }, [ref, state]); - } - `, errors: [ - "React Hook useEffect has a missing dependency: 'state'. " + - 'Either include it or remove the dependency array. ' + - `You can also do a functional update 'setState(s => ...)' ` + - `if you only need 'state' in the 'setState' call.`, + { + message: + "React Hook useEffect has a missing dependency: 'state'. " + + 'Either include it or remove the dependency array. ' + + `You can also do a functional update 'setState(s => ...)' ` + + `if you only need 'state' in the 'setState' call.`, + // We don't ask to remove static deps but don't add them either. + // Don't suggest removing "ref" (it's fine either way) + // but *do* add "state". *Don't* add "setState" ourselves. + suggestions: [ + { + desc: 'Update the dependencies array to be: [ref, state]', + output: normalizeIndent` + function MyComponent() { + const ref = useRef(); + const [state, setState] = useState(); + useEffect(() => { + ref.current = {}; + setState(state + 1); + }, [ref, state]); + } + `, + }, + ], + }, ], }, { - code: ` + code: normalizeIndent` function MyComponent(props) { const ref1 = useRef(); const ref2 = useRef(); @@ -2464,25 +3132,34 @@ const tests = { }, []); } `, - output: ` - function MyComponent(props) { - const ref1 = useRef(); - const ref2 = useRef(); - useEffect(() => { - ref1.current.focus(); - console.log(ref2.current.textContent); - alert(props.someOtherRefs.current.innerHTML); - fetch(props.color); - }, [props.color, props.someOtherRefs]); - } - `, errors: [ - "React Hook useEffect has missing dependencies: 'props.color' and 'props.someOtherRefs'. " + - 'Either include them or remove the dependency array.', + { + message: + "React Hook useEffect has missing dependencies: 'props.color' and 'props.someOtherRefs'. " + + 'Either include them or remove the dependency array.', + suggestions: [ + { + desc: + 'Update the dependencies array to be: [props.color, props.someOtherRefs]', + output: normalizeIndent` + function MyComponent(props) { + const ref1 = useRef(); + const ref2 = useRef(); + useEffect(() => { + ref1.current.focus(); + console.log(ref2.current.textContent); + alert(props.someOtherRefs.current.innerHTML); + fetch(props.color); + }, [props.color, props.someOtherRefs]); + } + `, + }, + ], + }, ], }, { - code: ` + code: normalizeIndent` function MyComponent(props) { const ref1 = useRef(); const ref2 = useRef(); @@ -2494,27 +3171,36 @@ const tests = { }, [ref1.current, ref2.current, props.someOtherRefs, props.color]); } `, - output: ` - function MyComponent(props) { - const ref1 = useRef(); - const ref2 = useRef(); - useEffect(() => { - ref1.current.focus(); - console.log(ref2.current.textContent); - alert(props.someOtherRefs.current.innerHTML); - fetch(props.color); - }, [props.someOtherRefs, props.color]); - } - `, errors: [ - "React Hook useEffect has unnecessary dependencies: 'ref1.current' and 'ref2.current'. " + - 'Either exclude them or remove the dependency array. ' + - "Mutable values like 'ref1.current' aren't valid dependencies " + - "because mutating them doesn't re-render the component.", + { + message: + "React Hook useEffect has unnecessary dependencies: 'ref1.current' and 'ref2.current'. " + + 'Either exclude them or remove the dependency array. ' + + "Mutable values like 'ref1.current' aren't valid dependencies " + + "because mutating them doesn't re-render the component.", + suggestions: [ + { + desc: + 'Update the dependencies array to be: [props.someOtherRefs, props.color]', + output: normalizeIndent` + function MyComponent(props) { + const ref1 = useRef(); + const ref2 = useRef(); + useEffect(() => { + ref1.current.focus(); + console.log(ref2.current.textContent); + alert(props.someOtherRefs.current.innerHTML); + fetch(props.color); + }, [props.someOtherRefs, props.color]); + } + `, + }, + ], + }, ], }, { - code: ` + code: normalizeIndent` function MyComponent() { const ref = useRef(); useEffect(() => { @@ -2522,23 +3208,31 @@ const tests = { }, [ref.current]); } `, - output: ` - function MyComponent() { - const ref = useRef(); - useEffect(() => { - console.log(ref.current); - }, []); - } - `, errors: [ - "React Hook useEffect has an unnecessary dependency: 'ref.current'. " + - 'Either exclude it or remove the dependency array. ' + - "Mutable values like 'ref.current' aren't valid dependencies " + - "because mutating them doesn't re-render the component.", + { + message: + "React Hook useEffect has an unnecessary dependency: 'ref.current'. " + + 'Either exclude it or remove the dependency array. ' + + "Mutable values like 'ref.current' aren't valid dependencies " + + "because mutating them doesn't re-render the component.", + suggestions: [ + { + desc: 'Update the dependencies array to be: []', + output: normalizeIndent` + function MyComponent() { + const ref = useRef(); + useEffect(() => { + console.log(ref.current); + }, []); + } + `, + }, + ], + }, ], }, { - code: ` + code: normalizeIndent` function MyComponent({ activeTab }) { const ref1 = useRef(); const ref2 = useRef(); @@ -2548,25 +3242,33 @@ const tests = { }, [ref1.current, ref2.current, activeTab]); } `, - output: ` - function MyComponent({ activeTab }) { - const ref1 = useRef(); - const ref2 = useRef(); - useEffect(() => { - ref1.current.scrollTop = 0; - ref2.current.scrollTop = 0; - }, [activeTab]); - } - `, errors: [ - "React Hook useEffect has unnecessary dependencies: 'ref1.current' and 'ref2.current'. " + - 'Either exclude them or remove the dependency array. ' + - "Mutable values like 'ref1.current' aren't valid dependencies " + - "because mutating them doesn't re-render the component.", + { + message: + "React Hook useEffect has unnecessary dependencies: 'ref1.current' and 'ref2.current'. " + + 'Either exclude them or remove the dependency array. ' + + "Mutable values like 'ref1.current' aren't valid dependencies " + + "because mutating them doesn't re-render the component.", + suggestions: [ + { + desc: 'Update the dependencies array to be: [activeTab]', + output: normalizeIndent` + function MyComponent({ activeTab }) { + const ref1 = useRef(); + const ref2 = useRef(); + useEffect(() => { + ref1.current.scrollTop = 0; + ref2.current.scrollTop = 0; + }, [activeTab]); + } + `, + }, + ], + }, ], }, { - code: ` + code: normalizeIndent` function MyComponent({ activeTab, initY }) { const ref1 = useRef(); const ref2 = useRef(); @@ -2576,25 +3278,33 @@ const tests = { }, [ref1.current, ref2.current, activeTab, initY]); } `, - output: ` - function MyComponent({ activeTab, initY }) { - const ref1 = useRef(); - const ref2 = useRef(); - const fn = useCallback(() => { - ref1.current.scrollTop = initY; - ref2.current.scrollTop = initY; - }, [initY]); - } - `, errors: [ - "React Hook useCallback has unnecessary dependencies: 'activeTab', 'ref1.current', and 'ref2.current'. " + - 'Either exclude them or remove the dependency array. ' + - "Mutable values like 'ref1.current' aren't valid dependencies " + - "because mutating them doesn't re-render the component.", + { + message: + "React Hook useCallback has unnecessary dependencies: 'activeTab', 'ref1.current', and 'ref2.current'. " + + 'Either exclude them or remove the dependency array. ' + + "Mutable values like 'ref1.current' aren't valid dependencies " + + "because mutating them doesn't re-render the component.", + suggestions: [ + { + desc: 'Update the dependencies array to be: [initY]', + output: normalizeIndent` + function MyComponent({ activeTab, initY }) { + const ref1 = useRef(); + const ref2 = useRef(); + const fn = useCallback(() => { + ref1.current.scrollTop = initY; + ref2.current.scrollTop = initY; + }, [initY]); + } + `, + }, + ], + }, ], }, { - code: ` + code: normalizeIndent` function MyComponent() { const ref = useRef(); useEffect(() => { @@ -2602,23 +3312,31 @@ const tests = { }, [ref.current, ref]); } `, - output: ` - function MyComponent() { - const ref = useRef(); - useEffect(() => { - console.log(ref.current); - }, [ref]); - } - `, errors: [ - "React Hook useEffect has an unnecessary dependency: 'ref.current'. " + - 'Either exclude it or remove the dependency array. ' + - "Mutable values like 'ref.current' aren't valid dependencies " + - "because mutating them doesn't re-render the component.", + { + message: + "React Hook useEffect has an unnecessary dependency: 'ref.current'. " + + 'Either exclude it or remove the dependency array. ' + + "Mutable values like 'ref.current' aren't valid dependencies " + + "because mutating them doesn't re-render the component.", + suggestions: [ + { + desc: 'Update the dependencies array to be: [ref]', + output: normalizeIndent` + function MyComponent() { + const ref = useRef(); + useEffect(() => { + console.log(ref.current); + }, [ref]); + } + `, + }, + ], + }, ], }, { - code: ` + code: normalizeIndent` const MyComponent = forwardRef((props, ref) => { useImperativeHandle(ref, () => ({ focus() { @@ -2627,22 +3345,30 @@ const tests = { }), []) }); `, - output: ` - const MyComponent = forwardRef((props, ref) => { - useImperativeHandle(ref, () => ({ - focus() { - alert(props.hello); - } - }), [props.hello]) - }); - `, errors: [ - "React Hook useImperativeHandle has a missing dependency: 'props.hello'. " + - 'Either include it or remove the dependency array.', + { + message: + "React Hook useImperativeHandle has a missing dependency: 'props.hello'. " + + 'Either include it or remove the dependency array.', + suggestions: [ + { + desc: 'Update the dependencies array to be: [props.hello]', + output: normalizeIndent` + const MyComponent = forwardRef((props, ref) => { + useImperativeHandle(ref, () => ({ + focus() { + alert(props.hello); + } + }), [props.hello]) + }); + `, + }, + ], + }, ], }, { - code: ` + code: normalizeIndent` function MyComponent(props) { useEffect(() => { if (props.onChange) { @@ -2651,29 +3377,37 @@ const tests = { }, []); } `, - output: ` - function MyComponent(props) { - useEffect(() => { - if (props.onChange) { - props.onChange(); - } - }, [props]); - } - `, errors: [ - "React Hook useEffect has a missing dependency: 'props'. " + - 'Either include it or remove the dependency array. ' + - `However, 'props' will change when *any* prop changes, so the ` + - `preferred fix is to destructure the 'props' object outside ` + - `of the useEffect call and refer to those specific ` + - `props inside useEffect.`, + { + message: + "React Hook useEffect has a missing dependency: 'props'. " + + 'Either include it or remove the dependency array. ' + + `However, 'props' will change when *any* prop changes, so the ` + + `preferred fix is to destructure the 'props' object outside ` + + `of the useEffect call and refer to those specific ` + + `props inside useEffect.`, + suggestions: [ + { + desc: 'Update the dependencies array to be: [props]', + output: normalizeIndent` + function MyComponent(props) { + useEffect(() => { + if (props.onChange) { + props.onChange(); + } + }, [props]); + } + `, + }, + ], + }, ], }, { - code: ` + code: normalizeIndent` function MyComponent(props) { useEffect(() => { - function play() { + function play() { props.onPlay(); } function pause() { @@ -2682,29 +3416,37 @@ const tests = { }, []); } `, - output: ` - function MyComponent(props) { - useEffect(() => { - function play() { - props.onPlay(); - } - function pause() { - props.onPause(); - } - }, [props]); - } - `, errors: [ - "React Hook useEffect has a missing dependency: 'props'. " + - 'Either include it or remove the dependency array. ' + - `However, 'props' will change when *any* prop changes, so the ` + - `preferred fix is to destructure the 'props' object outside ` + - `of the useEffect call and refer to those specific ` + - `props inside useEffect.`, + { + message: + "React Hook useEffect has a missing dependency: 'props'. " + + 'Either include it or remove the dependency array. ' + + `However, 'props' will change when *any* prop changes, so the ` + + `preferred fix is to destructure the 'props' object outside ` + + `of the useEffect call and refer to those specific ` + + `props inside useEffect.`, + suggestions: [ + { + desc: 'Update the dependencies array to be: [props]', + output: normalizeIndent` + function MyComponent(props) { + useEffect(() => { + function play() { + props.onPlay(); + } + function pause() { + props.onPause(); + } + }, [props]); + } + `, + }, + ], + }, ], }, { - code: ` + code: normalizeIndent` function MyComponent(props) { useEffect(() => { if (props.foo.onChange) { @@ -2713,22 +3455,30 @@ const tests = { }, []); } `, - output: ` - function MyComponent(props) { - useEffect(() => { - if (props.foo.onChange) { - props.foo.onChange(); - } - }, [props.foo]); - } - `, errors: [ - "React Hook useEffect has a missing dependency: 'props.foo'. " + - 'Either include it or remove the dependency array.', + { + message: + "React Hook useEffect has a missing dependency: 'props.foo'. " + + 'Either include it or remove the dependency array.', + suggestions: [ + { + desc: 'Update the dependencies array to be: [props.foo]', + output: normalizeIndent` + function MyComponent(props) { + useEffect(() => { + if (props.foo.onChange) { + props.foo.onChange(); + } + }, [props.foo]); + } + `, + }, + ], + }, ], }, { - code: ` + code: normalizeIndent` function MyComponent(props) { useEffect(() => { props.onChange(); @@ -2738,27 +3488,35 @@ const tests = { }, []); } `, - output: ` - function MyComponent(props) { - useEffect(() => { - props.onChange(); - if (props.foo.onChange) { - props.foo.onChange(); - } - }, [props]); - } - `, errors: [ - "React Hook useEffect has a missing dependency: 'props'. " + - 'Either include it or remove the dependency array. ' + - `However, 'props' will change when *any* prop changes, so the ` + - `preferred fix is to destructure the 'props' object outside ` + - `of the useEffect call and refer to those specific ` + - `props inside useEffect.`, + { + message: + "React Hook useEffect has a missing dependency: 'props'. " + + 'Either include it or remove the dependency array. ' + + `However, 'props' will change when *any* prop changes, so the ` + + `preferred fix is to destructure the 'props' object outside ` + + `of the useEffect call and refer to those specific ` + + `props inside useEffect.`, + suggestions: [ + { + desc: 'Update the dependencies array to be: [props]', + output: normalizeIndent` + function MyComponent(props) { + useEffect(() => { + props.onChange(); + if (props.foo.onChange) { + props.foo.onChange(); + } + }, [props]); + } + `, + }, + ], + }, ], }, { - code: ` + code: normalizeIndent` function MyComponent(props) { const [skillsCount] = useState(); useEffect(() => { @@ -2768,27 +3526,36 @@ const tests = { }, [skillsCount, props.isEditMode, props.toggleEditMode]); } `, - output: ` - function MyComponent(props) { - const [skillsCount] = useState(); - useEffect(() => { - if (skillsCount === 0 && !props.isEditMode) { - props.toggleEditMode(); - } - }, [skillsCount, props.isEditMode, props.toggleEditMode, props]); - } - `, errors: [ - "React Hook useEffect has a missing dependency: 'props'. " + - 'Either include it or remove the dependency array. ' + - `However, 'props' will change when *any* prop changes, so the ` + - `preferred fix is to destructure the 'props' object outside ` + - `of the useEffect call and refer to those specific ` + - `props inside useEffect.`, + { + message: + "React Hook useEffect has a missing dependency: 'props'. " + + 'Either include it or remove the dependency array. ' + + `However, 'props' will change when *any* prop changes, so the ` + + `preferred fix is to destructure the 'props' object outside ` + + `of the useEffect call and refer to those specific ` + + `props inside useEffect.`, + suggestions: [ + { + desc: + 'Update the dependencies array to be: [skillsCount, props.isEditMode, props.toggleEditMode, props]', + output: normalizeIndent` + function MyComponent(props) { + const [skillsCount] = useState(); + useEffect(() => { + if (skillsCount === 0 && !props.isEditMode) { + props.toggleEditMode(); + } + }, [skillsCount, props.isEditMode, props.toggleEditMode, props]); + } + `, + }, + ], + }, ], }, { - code: ` + code: normalizeIndent` function MyComponent(props) { const [skillsCount] = useState(); useEffect(() => { @@ -2798,27 +3565,35 @@ const tests = { }, []); } `, - output: ` - function MyComponent(props) { - const [skillsCount] = useState(); - useEffect(() => { - if (skillsCount === 0 && !props.isEditMode) { - props.toggleEditMode(); - } - }, [props, skillsCount]); - } - `, errors: [ - "React Hook useEffect has missing dependencies: 'props' and 'skillsCount'. " + - 'Either include them or remove the dependency array. ' + - `However, 'props' will change when *any* prop changes, so the ` + - `preferred fix is to destructure the 'props' object outside ` + - `of the useEffect call and refer to those specific ` + - `props inside useEffect.`, + { + message: + "React Hook useEffect has missing dependencies: 'props' and 'skillsCount'. " + + 'Either include them or remove the dependency array. ' + + `However, 'props' will change when *any* prop changes, so the ` + + `preferred fix is to destructure the 'props' object outside ` + + `of the useEffect call and refer to those specific ` + + `props inside useEffect.`, + suggestions: [ + { + desc: 'Update the dependencies array to be: [props, skillsCount]', + output: normalizeIndent` + function MyComponent(props) { + const [skillsCount] = useState(); + useEffect(() => { + if (skillsCount === 0 && !props.isEditMode) { + props.toggleEditMode(); + } + }, [props, skillsCount]); + } + `, + }, + ], + }, ], }, { - code: ` + code: normalizeIndent` function MyComponent(props) { useEffect(() => { externalCall(props); @@ -2826,22 +3601,30 @@ const tests = { }, []); } `, - output: ` - function MyComponent(props) { - useEffect(() => { - externalCall(props); - props.onChange(); - }, [props]); - } - `, // Don't suggest to destructure props here since you can't. errors: [ - "React Hook useEffect has a missing dependency: 'props'. " + - 'Either include it or remove the dependency array.', + { + message: + "React Hook useEffect has a missing dependency: 'props'. " + + 'Either include it or remove the dependency array.', + suggestions: [ + { + desc: 'Update the dependencies array to be: [props]', + output: normalizeIndent` + function MyComponent(props) { + useEffect(() => { + externalCall(props); + props.onChange(); + }, [props]); + } + `, + }, + ], + }, ], }, { - code: ` + code: normalizeIndent` function MyComponent(props) { useEffect(() => { props.onChange(); @@ -2849,22 +3632,30 @@ const tests = { }, []); } `, - output: ` - function MyComponent(props) { - useEffect(() => { - props.onChange(); - externalCall(props); - }, [props]); - } - `, // Don't suggest to destructure props here since you can't. errors: [ - "React Hook useEffect has a missing dependency: 'props'. " + - 'Either include it or remove the dependency array.', + { + message: + "React Hook useEffect has a missing dependency: 'props'. " + + 'Either include it or remove the dependency array.', + suggestions: [ + { + desc: 'Update the dependencies array to be: [props]', + output: normalizeIndent` + function MyComponent(props) { + useEffect(() => { + props.onChange(); + externalCall(props); + }, [props]); + } + `, + }, + ], + }, ], }, { - code: ` + code: normalizeIndent` function MyComponent(props) { let value; let value2; @@ -2888,54 +3679,48 @@ const tests = { `, // This is a separate warning unrelated to others. // We could've made a separate rule for it but it's rare enough to name it. - // No autofix suggestion because the intent isn't clear. - output: ` - function MyComponent(props) { - let value; - let value2; - let value3; - let value4; - let asyncValue; - useEffect(() => { - if (value4) { - value = {}; - } - value2 = 100; - value = 43; - value4 = true; - console.log(value2); - console.log(value3); - setTimeout(() => { - asyncValue = 100; - }); - }, []); - } - `, + // No suggestions because the intent isn't clear. errors: [ - // value2 - `Assignments to the 'value2' variable from inside React Hook useEffect ` + - `will be lost after each render. To preserve the value over time, ` + - `store it in a useRef Hook and keep the mutable value in the '.current' property. ` + - `Otherwise, you can move this variable directly inside useEffect.`, - // value - `Assignments to the 'value' variable from inside React Hook useEffect ` + - `will be lost after each render. To preserve the value over time, ` + - `store it in a useRef Hook and keep the mutable value in the '.current' property. ` + - `Otherwise, you can move this variable directly inside useEffect.`, - // value4 - `Assignments to the 'value4' variable from inside React Hook useEffect ` + - `will be lost after each render. To preserve the value over time, ` + - `store it in a useRef Hook and keep the mutable value in the '.current' property. ` + - `Otherwise, you can move this variable directly inside useEffect.`, - // asyncValue - `Assignments to the 'asyncValue' variable from inside React Hook useEffect ` + - `will be lost after each render. To preserve the value over time, ` + - `store it in a useRef Hook and keep the mutable value in the '.current' property. ` + - `Otherwise, you can move this variable directly inside useEffect.`, + { + message: + // value2 + `Assignments to the 'value2' variable from inside React Hook useEffect ` + + `will be lost after each render. To preserve the value over time, ` + + `store it in a useRef Hook and keep the mutable value in the '.current' property. ` + + `Otherwise, you can move this variable directly inside useEffect.`, + suggestions: undefined, + }, + { + message: + // value + `Assignments to the 'value' variable from inside React Hook useEffect ` + + `will be lost after each render. To preserve the value over time, ` + + `store it in a useRef Hook and keep the mutable value in the '.current' property. ` + + `Otherwise, you can move this variable directly inside useEffect.`, + suggestions: undefined, + }, + { + message: + // value4 + `Assignments to the 'value4' variable from inside React Hook useEffect ` + + `will be lost after each render. To preserve the value over time, ` + + `store it in a useRef Hook and keep the mutable value in the '.current' property. ` + + `Otherwise, you can move this variable directly inside useEffect.`, + suggestions: undefined, + }, + { + message: + // asyncValue + `Assignments to the 'asyncValue' variable from inside React Hook useEffect ` + + `will be lost after each render. To preserve the value over time, ` + + `store it in a useRef Hook and keep the mutable value in the '.current' property. ` + + `Otherwise, you can move this variable directly inside useEffect.`, + suggestions: undefined, + }, ], }, { - code: ` + code: normalizeIndent` function MyComponent(props) { let value; let value2; @@ -2955,56 +3740,39 @@ const tests = { `, // This is a separate warning unrelated to others. // We could've made a separate rule for it but it's rare enough to name it. - // No autofix suggestion because the intent isn't clear. - output: ` - function MyComponent(props) { - let value; - let value2; - let value3; - let asyncValue; - useEffect(() => { - value = {}; - value2 = 100; - value = 43; - console.log(value2); - console.log(value3); - setTimeout(() => { - asyncValue = 100; - }); - }, [value, value2, value3]); - } - `, + // No suggestions because the intent isn't clear. errors: [ - // value - `Assignments to the 'value' variable from inside React Hook useEffect ` + - `will be lost after each render. To preserve the value over time, ` + - `store it in a useRef Hook and keep the mutable value in the '.current' property. ` + - `Otherwise, you can move this variable directly inside useEffect.`, - // value2 - `Assignments to the 'value2' variable from inside React Hook useEffect ` + - `will be lost after each render. To preserve the value over time, ` + - `store it in a useRef Hook and keep the mutable value in the '.current' property. ` + - `Otherwise, you can move this variable directly inside useEffect.`, - // asyncValue - `Assignments to the 'asyncValue' variable from inside React Hook useEffect ` + - `will be lost after each render. To preserve the value over time, ` + - `store it in a useRef Hook and keep the mutable value in the '.current' property. ` + - `Otherwise, you can move this variable directly inside useEffect.`, + { + message: + // value + `Assignments to the 'value' variable from inside React Hook useEffect ` + + `will be lost after each render. To preserve the value over time, ` + + `store it in a useRef Hook and keep the mutable value in the '.current' property. ` + + `Otherwise, you can move this variable directly inside useEffect.`, + suggestions: undefined, + }, + { + message: + // value2 + `Assignments to the 'value2' variable from inside React Hook useEffect ` + + `will be lost after each render. To preserve the value over time, ` + + `store it in a useRef Hook and keep the mutable value in the '.current' property. ` + + `Otherwise, you can move this variable directly inside useEffect.`, + suggestions: undefined, + }, + { + message: + // asyncValue + `Assignments to the 'asyncValue' variable from inside React Hook useEffect ` + + `will be lost after each render. To preserve the value over time, ` + + `store it in a useRef Hook and keep the mutable value in the '.current' property. ` + + `Otherwise, you can move this variable directly inside useEffect.`, + suggestions: undefined, + }, ], }, { - code: ` - function MyComponent() { - const myRef = useRef(); - useEffect(() => { - const handleMove = () => {}; - myRef.current.addEventListener('mousemove', handleMove); - return () => myRef.current.removeEventListener('mousemove', handleMove); - }, []); - return
; - } - `, - output: ` + code: normalizeIndent` function MyComponent() { const myRef = useRef(); useEffect(() => { @@ -3016,25 +3784,18 @@ const tests = { } `, errors: [ - `The ref value 'myRef.current' will likely have changed by the time ` + - `this effect cleanup function runs. If this ref points to a node ` + - `rendered by React, copy 'myRef.current' to a variable inside the effect, ` + - `and use that variable in the cleanup function.`, + { + message: + `The ref value 'myRef.current' will likely have changed by the time ` + + `this effect cleanup function runs. If this ref points to a node ` + + `rendered by React, copy 'myRef.current' to a variable inside the effect, ` + + `and use that variable in the cleanup function.`, + suggestions: undefined, + }, ], }, { - code: ` - function MyComponent() { - const myRef = useRef(); - useEffect(() => { - const handleMove = () => {}; - myRef.current.addEventListener('mousemove', handleMove); - return () => myRef.current.removeEventListener('mousemove', handleMove); - }); - return
; - } - `, - output: ` + code: normalizeIndent` function MyComponent() { const myRef = useRef(); useEffect(() => { @@ -3046,23 +3807,18 @@ const tests = { } `, errors: [ - `The ref value 'myRef.current' will likely have changed by the time ` + - `this effect cleanup function runs. If this ref points to a node ` + - `rendered by React, copy 'myRef.current' to a variable inside the effect, ` + - `and use that variable in the cleanup function.`, + { + message: + `The ref value 'myRef.current' will likely have changed by the time ` + + `this effect cleanup function runs. If this ref points to a node ` + + `rendered by React, copy 'myRef.current' to a variable inside the effect, ` + + `and use that variable in the cleanup function.`, + suggestions: undefined, + }, ], }, { - code: ` - function useMyThing(myRef) { - useEffect(() => { - const handleMove = () => {}; - myRef.current.addEventListener('mousemove', handleMove); - return () => myRef.current.removeEventListener('mousemove', handleMove); - }, [myRef]); - } - `, - output: ` + code: normalizeIndent` function useMyThing(myRef) { useEffect(() => { const handleMove = () => {}; @@ -3072,29 +3828,18 @@ const tests = { } `, errors: [ - `The ref value 'myRef.current' will likely have changed by the time ` + - `this effect cleanup function runs. If this ref points to a node ` + - `rendered by React, copy 'myRef.current' to a variable inside the effect, ` + - `and use that variable in the cleanup function.`, + { + message: + `The ref value 'myRef.current' will likely have changed by the time ` + + `this effect cleanup function runs. If this ref points to a node ` + + `rendered by React, copy 'myRef.current' to a variable inside the effect, ` + + `and use that variable in the cleanup function.`, + suggestions: undefined, + }, ], }, { - code: ` - function useMyThing(myRef) { - useEffect(() => { - const handleMouse = () => {}; - myRef.current.addEventListener('mousemove', handleMouse); - myRef.current.addEventListener('mousein', handleMouse); - return function() { - setTimeout(() => { - myRef.current.removeEventListener('mousemove', handleMouse); - myRef.current.removeEventListener('mousein', handleMouse); - }); - } - }, [myRef]); - } - `, - output: ` + code: normalizeIndent` function useMyThing(myRef) { useEffect(() => { const handleMouse = () => {}; @@ -3110,29 +3855,18 @@ const tests = { } `, errors: [ - `The ref value 'myRef.current' will likely have changed by the time ` + - `this effect cleanup function runs. If this ref points to a node ` + - `rendered by React, copy 'myRef.current' to a variable inside the effect, ` + - `and use that variable in the cleanup function.`, + { + message: + `The ref value 'myRef.current' will likely have changed by the time ` + + `this effect cleanup function runs. If this ref points to a node ` + + `rendered by React, copy 'myRef.current' to a variable inside the effect, ` + + `and use that variable in the cleanup function.`, + suggestions: undefined, + }, ], }, { - code: ` - function useMyThing(myRef, active) { - useEffect(() => { - const handleMove = () => {}; - if (active) { - myRef.current.addEventListener('mousemove', handleMove); - return function() { - setTimeout(() => { - myRef.current.removeEventListener('mousemove', handleMove); - }); - } - } - }, [myRef, active]); - } - `, - output: ` + code: normalizeIndent` function useMyThing(myRef, active) { useEffect(() => { const handleMove = () => {}; @@ -3148,10 +3882,14 @@ const tests = { } `, errors: [ - `The ref value 'myRef.current' will likely have changed by the time ` + - `this effect cleanup function runs. If this ref points to a node ` + - `rendered by React, copy 'myRef.current' to a variable inside the effect, ` + - `and use that variable in the cleanup function.`, + { + message: + `The ref value 'myRef.current' will likely have changed by the time ` + + `this effect cleanup function runs. If this ref points to a node ` + + `rendered by React, copy 'myRef.current' to a variable inside the effect, ` + + `and use that variable in the cleanup function.`, + suggestions: undefined, + }, ], }, { @@ -3187,41 +3925,50 @@ const tests = { }, { // Autofix ignores constant primitives (leaving the ones that are there). - code: ` - function MyComponent() { - const local1 = 42; - const local2 = '42'; - const local3 = null; - const local4 = {}; - useEffect(() => { - console.log(local1); - console.log(local2); - console.log(local3); - console.log(local4); - }, [local1, local3]); - } - `, - output: ` - function MyComponent() { - const local1 = 42; - const local2 = '42'; - const local3 = null; - const local4 = {}; - useEffect(() => { - console.log(local1); - console.log(local2); - console.log(local3); - console.log(local4); - }, [local1, local3, local4]); - } - `, + code: normalizeIndent` + function MyComponent() { + const local1 = 42; + const local2 = '42'; + const local3 = null; + const local4 = {}; + useEffect(() => { + console.log(local1); + console.log(local2); + console.log(local3); + console.log(local4); + }, [local1, local3]); + } + `, errors: [ - "React Hook useEffect has a missing dependency: 'local4'. " + - 'Either include it or remove the dependency array.', + { + message: + "React Hook useEffect has a missing dependency: 'local4'. " + + 'Either include it or remove the dependency array.', + suggestions: [ + { + desc: + 'Update the dependencies array to be: [local1, local3, local4]', + output: normalizeIndent` + function MyComponent() { + const local1 = 42; + const local2 = '42'; + const local3 = null; + const local4 = {}; + useEffect(() => { + console.log(local1); + console.log(local2); + console.log(local3); + console.log(local4); + }, [local1, local3, local4]); + } + `, + }, + ], + }, ], }, { - code: ` + code: normalizeIndent` function MyComponent() { useEffect(() => { window.scrollTo(0, 0); @@ -3229,14 +3976,29 @@ const tests = { } `, errors: [ - "React Hook useEffect has an unnecessary dependency: 'window'. " + - 'Either exclude it or remove the dependency array. ' + - "Outer scope values like 'window' aren't valid dependencies " + - "because mutating them doesn't re-render the component.", + { + message: + "React Hook useEffect has an unnecessary dependency: 'window'. " + + 'Either exclude it or remove the dependency array. ' + + "Outer scope values like 'window' aren't valid dependencies " + + "because mutating them doesn't re-render the component.", + suggestions: [ + { + desc: 'Update the dependencies array to be: []', + output: normalizeIndent` + function MyComponent() { + useEffect(() => { + window.scrollTo(0, 0); + }, []); + } + `, + }, + ], + }, ], }, { - code: ` + code: normalizeIndent` import MutableStore from 'store'; function MyComponent() { @@ -3245,24 +4007,32 @@ const tests = { }, [MutableStore.hello]); } `, - output: ` - import MutableStore from 'store'; - - function MyComponent() { - useEffect(() => { - console.log(MutableStore.hello); - }, []); - } - `, errors: [ - "React Hook useEffect has an unnecessary dependency: 'MutableStore.hello'. " + - 'Either exclude it or remove the dependency array. ' + - "Outer scope values like 'MutableStore.hello' aren't valid dependencies " + - "because mutating them doesn't re-render the component.", + { + message: + "React Hook useEffect has an unnecessary dependency: 'MutableStore.hello'. " + + 'Either exclude it or remove the dependency array. ' + + "Outer scope values like 'MutableStore.hello' aren't valid dependencies " + + "because mutating them doesn't re-render the component.", + suggestions: [ + { + desc: 'Update the dependencies array to be: []', + output: normalizeIndent` + import MutableStore from 'store'; + + function MyComponent() { + useEffect(() => { + console.log(MutableStore.hello); + }, []); + } + `, + }, + ], + }, ], }, { - code: ` + code: normalizeIndent` import MutableStore from 'store'; let z = {}; @@ -3276,30 +4046,38 @@ const tests = { } } `, - output: ` - import MutableStore from 'store'; - let z = {}; - - function MyComponent(props) { - let x = props.foo; - { - let y = props.bar; - useEffect(() => { - console.log(MutableStore.hello.world, props.foo, x, y, z, global.stuff); - }, [props.foo, x, y]); - } - } - `, - errors: [ - 'React Hook useEffect has unnecessary dependencies: ' + - "'MutableStore.hello.world', 'global.stuff', and 'z'. " + - 'Either exclude them or remove the dependency array. ' + - "Outer scope values like 'MutableStore.hello.world' aren't valid dependencies " + - "because mutating them doesn't re-render the component.", - ], - }, - { - code: ` + errors: [ + { + message: + 'React Hook useEffect has unnecessary dependencies: ' + + "'MutableStore.hello.world', 'global.stuff', and 'z'. " + + 'Either exclude them or remove the dependency array. ' + + "Outer scope values like 'MutableStore.hello.world' aren't valid dependencies " + + "because mutating them doesn't re-render the component.", + suggestions: [ + { + desc: 'Update the dependencies array to be: [props.foo, x, y]', + output: normalizeIndent` + import MutableStore from 'store'; + let z = {}; + + function MyComponent(props) { + let x = props.foo; + { + let y = props.bar; + useEffect(() => { + console.log(MutableStore.hello.world, props.foo, x, y, z, global.stuff); + }, [props.foo, x, y]); + } + } + `, + }, + ], + }, + ], + }, + { + code: normalizeIndent` import MutableStore from 'store'; let z = {}; @@ -3313,32 +4091,40 @@ const tests = { } } `, - // The output should contain the ones that are inside a component - // since there are legit reasons to over-specify them for effects. - output: ` - import MutableStore from 'store'; - let z = {}; - - function MyComponent(props) { - let x = props.foo; - { - let y = props.bar; - useEffect(() => { - // nothing - }, [props.foo, x, y]); - } - } - `, errors: [ - 'React Hook useEffect has unnecessary dependencies: ' + - "'MutableStore.hello.world', 'global.stuff', and 'z'. " + - 'Either exclude them or remove the dependency array. ' + - "Outer scope values like 'MutableStore.hello.world' aren't valid dependencies " + - "because mutating them doesn't re-render the component.", + { + message: + 'React Hook useEffect has unnecessary dependencies: ' + + "'MutableStore.hello.world', 'global.stuff', and 'z'. " + + 'Either exclude them or remove the dependency array. ' + + "Outer scope values like 'MutableStore.hello.world' aren't valid dependencies " + + "because mutating them doesn't re-render the component.", + // The output should contain the ones that are inside a component + // since there are legit reasons to over-specify them for effects. + suggestions: [ + { + desc: 'Update the dependencies array to be: [props.foo, x, y]', + output: normalizeIndent` + import MutableStore from 'store'; + let z = {}; + + function MyComponent(props) { + let x = props.foo; + { + let y = props.bar; + useEffect(() => { + // nothing + }, [props.foo, x, y]); + } + } + `, + }, + ], + }, ], }, { - code: ` + code: normalizeIndent` import MutableStore from 'store'; let z = {}; @@ -3352,31 +4138,39 @@ const tests = { } } `, - output: ` - import MutableStore from 'store'; - let z = {}; - - function MyComponent(props) { - let x = props.foo; - { - let y = props.bar; - const fn = useCallback(() => { - // nothing - }, []); - } - } - `, errors: [ - 'React Hook useCallback has unnecessary dependencies: ' + - "'MutableStore.hello.world', 'global.stuff', 'props.foo', 'x', 'y', and 'z'. " + - 'Either exclude them or remove the dependency array. ' + - "Outer scope values like 'MutableStore.hello.world' aren't valid dependencies " + - "because mutating them doesn't re-render the component.", + { + message: + 'React Hook useCallback has unnecessary dependencies: ' + + "'MutableStore.hello.world', 'global.stuff', 'props.foo', 'x', 'y', and 'z'. " + + 'Either exclude them or remove the dependency array. ' + + "Outer scope values like 'MutableStore.hello.world' aren't valid dependencies " + + "because mutating them doesn't re-render the component.", + suggestions: [ + { + desc: 'Update the dependencies array to be: []', + output: normalizeIndent` + import MutableStore from 'store'; + let z = {}; + + function MyComponent(props) { + let x = props.foo; + { + let y = props.bar; + const fn = useCallback(() => { + // nothing + }, []); + } + } + `, + }, + ], + }, ], }, { // Every almost-static function is tainted by a dynamic value. - code: ` + code: normalizeIndent` function MyComponent(props) { let [, setState] = useState(); let [, dispatch] = React.useReducer(); @@ -3406,48 +4200,132 @@ const tests = { }, []); } `, - output: ` - function MyComponent(props) { - let [, setState] = useState(); - let [, dispatch] = React.useReducer(); - let taint = props.foo; - - function handleNext1(value) { - let value2 = value * taint; - setState(value2); - console.log('hello'); - } - const handleNext2 = (value) => { - setState(taint(value)); - console.log('hello'); - }; - let handleNext3 = function(value) { - setTimeout(() => console.log(taint)); - dispatch({ type: 'x', value }); - }; - useEffect(() => { - return Store.subscribe(handleNext1); - }, [handleNext1]); - useLayoutEffect(() => { - return Store.subscribe(handleNext2); - }, [handleNext2]); - useMemo(() => { - return Store.subscribe(handleNext3); - }, [handleNext3]); - } - `, errors: [ - "React Hook useEffect has a missing dependency: 'handleNext1'. " + - 'Either include it or remove the dependency array.', - "React Hook useLayoutEffect has a missing dependency: 'handleNext2'. " + - 'Either include it or remove the dependency array.', - "React Hook useMemo has a missing dependency: 'handleNext3'. " + - 'Either include it or remove the dependency array.', + { + message: + "React Hook useEffect has a missing dependency: 'handleNext1'. " + + 'Either include it or remove the dependency array.', + suggestions: [ + { + desc: 'Update the dependencies array to be: [handleNext1]', + output: normalizeIndent` + function MyComponent(props) { + let [, setState] = useState(); + let [, dispatch] = React.useReducer(); + let taint = props.foo; + + function handleNext1(value) { + let value2 = value * taint; + setState(value2); + console.log('hello'); + } + const handleNext2 = (value) => { + setState(taint(value)); + console.log('hello'); + }; + let handleNext3 = function(value) { + setTimeout(() => console.log(taint)); + dispatch({ type: 'x', value }); + }; + useEffect(() => { + return Store.subscribe(handleNext1); + }, [handleNext1]); + useLayoutEffect(() => { + return Store.subscribe(handleNext2); + }, []); + useMemo(() => { + return Store.subscribe(handleNext3); + }, []); + } + `, + }, + ], + }, + { + message: + "React Hook useLayoutEffect has a missing dependency: 'handleNext2'. " + + 'Either include it or remove the dependency array.', + suggestions: [ + { + desc: 'Update the dependencies array to be: [handleNext2]', + output: normalizeIndent` + function MyComponent(props) { + let [, setState] = useState(); + let [, dispatch] = React.useReducer(); + let taint = props.foo; + + function handleNext1(value) { + let value2 = value * taint; + setState(value2); + console.log('hello'); + } + const handleNext2 = (value) => { + setState(taint(value)); + console.log('hello'); + }; + let handleNext3 = function(value) { + setTimeout(() => console.log(taint)); + dispatch({ type: 'x', value }); + }; + useEffect(() => { + return Store.subscribe(handleNext1); + }, []); + useLayoutEffect(() => { + return Store.subscribe(handleNext2); + }, [handleNext2]); + useMemo(() => { + return Store.subscribe(handleNext3); + }, []); + } + `, + }, + ], + }, + { + message: + "React Hook useMemo has a missing dependency: 'handleNext3'. " + + 'Either include it or remove the dependency array.', + suggestions: [ + { + desc: 'Update the dependencies array to be: [handleNext3]', + output: normalizeIndent` + function MyComponent(props) { + let [, setState] = useState(); + let [, dispatch] = React.useReducer(); + let taint = props.foo; + + function handleNext1(value) { + let value2 = value * taint; + setState(value2); + console.log('hello'); + } + const handleNext2 = (value) => { + setState(taint(value)); + console.log('hello'); + }; + let handleNext3 = function(value) { + setTimeout(() => console.log(taint)); + dispatch({ type: 'x', value }); + }; + useEffect(() => { + return Store.subscribe(handleNext1); + }, []); + useLayoutEffect(() => { + return Store.subscribe(handleNext2); + }, []); + useMemo(() => { + return Store.subscribe(handleNext3); + }, [handleNext3]); + } + `, + }, + ], + }, ], }, { // Regression test - code: ` + code: normalizeIndent` function MyComponent(props) { let [, setState] = useState(); let [, dispatch] = React.useReducer(); @@ -3480,51 +4358,141 @@ const tests = { }, []); } `, - output: ` - function MyComponent(props) { - let [, setState] = useState(); - let [, dispatch] = React.useReducer(); - let taint = props.foo; + errors: [ + { + message: + "React Hook useEffect has a missing dependency: 'handleNext1'. " + + 'Either include it or remove the dependency array.', + suggestions: [ + { + desc: 'Update the dependencies array to be: [handleNext1]', + output: normalizeIndent` + function MyComponent(props) { + let [, setState] = useState(); + let [, dispatch] = React.useReducer(); + let taint = props.foo; - // Shouldn't affect anything - function handleChange() {} + // Shouldn't affect anything + function handleChange() {} - function handleNext1(value) { - let value2 = value * taint; - setState(value2); - console.log('hello'); - } - const handleNext2 = (value) => { - setState(taint(value)); - console.log('hello'); - }; - let handleNext3 = function(value) { - console.log(taint); - dispatch({ type: 'x', value }); - }; - useEffect(() => { - return Store.subscribe(handleNext1); - }, [handleNext1]); - useLayoutEffect(() => { - return Store.subscribe(handleNext2); - }, [handleNext2]); - useMemo(() => { - return Store.subscribe(handleNext3); - }, [handleNext3]); - } - `, - errors: [ - "React Hook useEffect has a missing dependency: 'handleNext1'. " + - 'Either include it or remove the dependency array.', - "React Hook useLayoutEffect has a missing dependency: 'handleNext2'. " + - 'Either include it or remove the dependency array.', - "React Hook useMemo has a missing dependency: 'handleNext3'. " + - 'Either include it or remove the dependency array.', + function handleNext1(value) { + let value2 = value * taint; + setState(value2); + console.log('hello'); + } + const handleNext2 = (value) => { + setState(taint(value)); + console.log('hello'); + }; + let handleNext3 = function(value) { + console.log(taint); + dispatch({ type: 'x', value }); + }; + useEffect(() => { + return Store.subscribe(handleNext1); + }, [handleNext1]); + useLayoutEffect(() => { + return Store.subscribe(handleNext2); + }, []); + useMemo(() => { + return Store.subscribe(handleNext3); + }, []); + } + `, + }, + ], + }, + { + message: + "React Hook useLayoutEffect has a missing dependency: 'handleNext2'. " + + 'Either include it or remove the dependency array.', + suggestions: [ + { + desc: 'Update the dependencies array to be: [handleNext2]', + output: normalizeIndent` + function MyComponent(props) { + let [, setState] = useState(); + let [, dispatch] = React.useReducer(); + let taint = props.foo; + + // Shouldn't affect anything + function handleChange() {} + + function handleNext1(value) { + let value2 = value * taint; + setState(value2); + console.log('hello'); + } + const handleNext2 = (value) => { + setState(taint(value)); + console.log('hello'); + }; + let handleNext3 = function(value) { + console.log(taint); + dispatch({ type: 'x', value }); + }; + useEffect(() => { + return Store.subscribe(handleNext1); + }, []); + useLayoutEffect(() => { + return Store.subscribe(handleNext2); + }, [handleNext2]); + useMemo(() => { + return Store.subscribe(handleNext3); + }, []); + } + `, + }, + ], + }, + { + message: + "React Hook useMemo has a missing dependency: 'handleNext3'. " + + 'Either include it or remove the dependency array.', + suggestions: [ + { + desc: 'Update the dependencies array to be: [handleNext3]', + output: normalizeIndent` + function MyComponent(props) { + let [, setState] = useState(); + let [, dispatch] = React.useReducer(); + let taint = props.foo; + + // Shouldn't affect anything + function handleChange() {} + + function handleNext1(value) { + let value2 = value * taint; + setState(value2); + console.log('hello'); + } + const handleNext2 = (value) => { + setState(taint(value)); + console.log('hello'); + }; + let handleNext3 = function(value) { + console.log(taint); + dispatch({ type: 'x', value }); + }; + useEffect(() => { + return Store.subscribe(handleNext1); + }, []); + useLayoutEffect(() => { + return Store.subscribe(handleNext2); + }, []); + useMemo(() => { + return Store.subscribe(handleNext3); + }, [handleNext3]); + } + `, + }, + ], + }, ], }, { // Regression test - code: ` + code: normalizeIndent` function MyComponent(props) { let [, setState] = useState(); let [, dispatch] = React.useReducer(); @@ -3557,67 +4525,140 @@ const tests = { }, []); } `, - output: ` - function MyComponent(props) { - let [, setState] = useState(); - let [, dispatch] = React.useReducer(); - let taint = props.foo; + errors: [ + { + message: + "React Hook useEffect has a missing dependency: 'handleNext1'. " + + 'Either include it or remove the dependency array.', + suggestions: [ + { + desc: 'Update the dependencies array to be: [handleNext1]', + output: normalizeIndent` + function MyComponent(props) { + let [, setState] = useState(); + let [, dispatch] = React.useReducer(); + let taint = props.foo; - // Shouldn't affect anything - const handleChange = () => {}; + // Shouldn't affect anything + const handleChange = () => {}; - function handleNext1(value) { - let value2 = value * taint; - setState(value2); - console.log('hello'); - } - const handleNext2 = (value) => { - setState(taint(value)); - console.log('hello'); - }; - let handleNext3 = function(value) { - console.log(taint); - dispatch({ type: 'x', value }); - }; - useEffect(() => { - return Store.subscribe(handleNext1); - }, [handleNext1]); - useLayoutEffect(() => { - return Store.subscribe(handleNext2); - }, [handleNext2]); - useMemo(() => { - return Store.subscribe(handleNext3); - }, [handleNext3]); - } - `, - errors: [ - "React Hook useEffect has a missing dependency: 'handleNext1'. " + - 'Either include it or remove the dependency array.', - "React Hook useLayoutEffect has a missing dependency: 'handleNext2'. " + - 'Either include it or remove the dependency array.', - "React Hook useMemo has a missing dependency: 'handleNext3'. " + - 'Either include it or remove the dependency array.', + function handleNext1(value) { + let value2 = value * taint; + setState(value2); + console.log('hello'); + } + const handleNext2 = (value) => { + setState(taint(value)); + console.log('hello'); + }; + let handleNext3 = function(value) { + console.log(taint); + dispatch({ type: 'x', value }); + }; + useEffect(() => { + return Store.subscribe(handleNext1); + }, [handleNext1]); + useLayoutEffect(() => { + return Store.subscribe(handleNext2); + }, []); + useMemo(() => { + return Store.subscribe(handleNext3); + }, []); + } + `, + }, + ], + }, + { + message: + "React Hook useLayoutEffect has a missing dependency: 'handleNext2'. " + + 'Either include it or remove the dependency array.', + suggestions: [ + { + desc: 'Update the dependencies array to be: [handleNext2]', + output: normalizeIndent` + function MyComponent(props) { + let [, setState] = useState(); + let [, dispatch] = React.useReducer(); + let taint = props.foo; + + // Shouldn't affect anything + const handleChange = () => {}; + + function handleNext1(value) { + let value2 = value * taint; + setState(value2); + console.log('hello'); + } + const handleNext2 = (value) => { + setState(taint(value)); + console.log('hello'); + }; + let handleNext3 = function(value) { + console.log(taint); + dispatch({ type: 'x', value }); + }; + useEffect(() => { + return Store.subscribe(handleNext1); + }, []); + useLayoutEffect(() => { + return Store.subscribe(handleNext2); + }, [handleNext2]); + useMemo(() => { + return Store.subscribe(handleNext3); + }, []); + } + `, + }, + ], + }, + { + message: + "React Hook useMemo has a missing dependency: 'handleNext3'. " + + 'Either include it or remove the dependency array.', + suggestions: [ + { + desc: 'Update the dependencies array to be: [handleNext3]', + output: normalizeIndent` + function MyComponent(props) { + let [, setState] = useState(); + let [, dispatch] = React.useReducer(); + let taint = props.foo; + + // Shouldn't affect anything + const handleChange = () => {}; + + function handleNext1(value) { + let value2 = value * taint; + setState(value2); + console.log('hello'); + } + const handleNext2 = (value) => { + setState(taint(value)); + console.log('hello'); + }; + let handleNext3 = function(value) { + console.log(taint); + dispatch({ type: 'x', value }); + }; + useEffect(() => { + return Store.subscribe(handleNext1); + }, []); + useLayoutEffect(() => { + return Store.subscribe(handleNext2); + }, []); + useMemo(() => { + return Store.subscribe(handleNext3); + }, [handleNext3]); + } + `, + }, + ], + }, ], }, { - // Even if the function only references static values, - // once you specify it in deps, it will invalidate them. - code: ` - function MyComponent(props) { - let [, setState] = useState(); - - function handleNext(value) { - setState(value); - } - - useEffect(() => { - return Store.subscribe(handleNext); - }, [handleNext]); - } - `, - // Not gonna autofix a function definition - // because it's not always safe due to hoisting. - output: ` + code: normalizeIndent` function MyComponent(props) { let [, setState] = useState(); @@ -3631,31 +4672,22 @@ const tests = { } `, errors: [ - `The 'handleNext' function makes the dependencies of ` + - `useEffect Hook (at line 11) change on every render. ` + - `Move it inside the useEffect callback. Alternatively, ` + - `wrap the 'handleNext' definition into its own useCallback() Hook.`, + { + message: + `The 'handleNext' function makes the dependencies of ` + + `useEffect Hook (at line 11) change on every render. ` + + `Move it inside the useEffect callback. Alternatively, ` + + `wrap the 'handleNext' definition into its own useCallback() Hook.`, + // Not gonna fix a function definition + // because it's not always safe due to hoisting. + suggestions: undefined, + }, ], }, { // Even if the function only references static values, // once you specify it in deps, it will invalidate them. - code: ` - function MyComponent(props) { - let [, setState] = useState(); - - const handleNext = (value) => { - setState(value); - }; - - useEffect(() => { - return Store.subscribe(handleNext); - }, [handleNext]); - } - `, - // We don't autofix moving (too invasive). But that's the suggested fix - // when only effect uses this function. Otherwise, we'd useCallback. - output: ` + code: normalizeIndent` function MyComponent(props) { let [, setState] = useState(); @@ -3669,10 +4701,16 @@ const tests = { } `, errors: [ - `The 'handleNext' function makes the dependencies of ` + - `useEffect Hook (at line 11) change on every render. ` + - `Move it inside the useEffect callback. Alternatively, ` + - `wrap the 'handleNext' definition into its own useCallback() Hook.`, + { + message: + `The 'handleNext' function makes the dependencies of ` + + `useEffect Hook (at line 11) change on every render. ` + + `Move it inside the useEffect callback. Alternatively, ` + + `wrap the 'handleNext' definition into its own useCallback() Hook.`, + // We don't fix moving (too invasive). But that's the suggested fix + // when only effect uses this function. Otherwise, we'd useCallback. + suggestions: undefined, + }, ], }, { @@ -3681,7 +4719,7 @@ const tests = { // However, we can't suggest moving handleNext into the // effect because it is *also* used outside of it. // So our suggestion is useCallback(). - code: ` + code: normalizeIndent` function MyComponent(props) { let [, setState] = useState(); @@ -3696,55 +4734,40 @@ const tests = { return
; } `, - // We autofix this one with useCallback since it's - // the easy fix and you can't just move it into effect. - output: ` - function MyComponent(props) { - let [, setState] = useState(); + errors: [ + { + message: + `The 'handleNext' function makes the dependencies of ` + + `useEffect Hook (at line 11) change on every render. ` + + `To fix this, wrap the 'handleNext' definition into its own useCallback() Hook.`, + // We fix this one with useCallback since it's + // the easy fix and you can't just move it into effect. + suggestions: [ + { + desc: + "Wrap the 'handleNext' definition into its own useCallback() Hook.", + output: normalizeIndent` + function MyComponent(props) { + let [, setState] = useState(); - const handleNext = useCallback((value) => { - setState(value); - }); + const handleNext = useCallback((value) => { + setState(value); + }); - useEffect(() => { - return Store.subscribe(handleNext); - }, [handleNext]); + useEffect(() => { + return Store.subscribe(handleNext); + }, [handleNext]); - return
; - } - `, - errors: [ - `The 'handleNext' function makes the dependencies of ` + - `useEffect Hook (at line 11) change on every render. ` + - `To fix this, wrap the 'handleNext' definition into its own useCallback() Hook.`, + return
; + } + `, + }, + ], + }, ], }, { - code: ` - function MyComponent(props) { - function handleNext1() { - console.log('hello'); - } - const handleNext2 = () => { - console.log('hello'); - }; - let handleNext3 = function() { - console.log('hello'); - }; - useEffect(() => { - return Store.subscribe(handleNext1); - }, [handleNext1]); - useLayoutEffect(() => { - return Store.subscribe(handleNext2); - }, [handleNext2]); - useMemo(() => { - return Store.subscribe(handleNext3); - }, [handleNext3]); - } - `, - // Autofix doesn't wrap into useCallback here - // because they are only referenced by effect itself. - output: ` + code: normalizeIndent` function MyComponent(props) { function handleNext1() { console.log('hello'); @@ -3767,19 +4790,31 @@ const tests = { } `, errors: [ - "The 'handleNext1' function makes the dependencies of useEffect Hook " + - '(at line 14) change on every render. Move it inside the useEffect callback. ' + - "Alternatively, wrap the 'handleNext1' definition into its own useCallback() Hook.", - "The 'handleNext2' function makes the dependencies of useLayoutEffect Hook " + - '(at line 17) change on every render. Move it inside the useLayoutEffect callback. ' + - "Alternatively, wrap the 'handleNext2' definition into its own useCallback() Hook.", - "The 'handleNext3' function makes the dependencies of useMemo Hook " + - '(at line 20) change on every render. Move it inside the useMemo callback. ' + - "Alternatively, wrap the 'handleNext3' definition into its own useCallback() Hook.", + { + message: + "The 'handleNext1' function makes the dependencies of useEffect Hook " + + '(at line 14) change on every render. Move it inside the useEffect callback. ' + + "Alternatively, wrap the 'handleNext1' definition into its own useCallback() Hook.", + suggestions: undefined, + }, + { + message: + "The 'handleNext2' function makes the dependencies of useLayoutEffect Hook " + + '(at line 17) change on every render. Move it inside the useLayoutEffect callback. ' + + "Alternatively, wrap the 'handleNext2' definition into its own useCallback() Hook.", + suggestions: undefined, + }, + { + message: + "The 'handleNext3' function makes the dependencies of useMemo Hook " + + '(at line 20) change on every render. Move it inside the useMemo callback. ' + + "Alternatively, wrap the 'handleNext3' definition into its own useCallback() Hook.", + suggestions: undefined, + }, ], }, { - code: ` + code: normalizeIndent` function MyComponent(props) { function handleNext1() { console.log('hello'); @@ -3804,47 +4839,34 @@ const tests = { }, [handleNext3]); } `, - // Autofix doesn't wrap into useCallback here + // Suggestions don't wrap into useCallback here // because they are only referenced by effect itself. - output: ` - function MyComponent(props) { - function handleNext1() { - console.log('hello'); - } - const handleNext2 = () => { - console.log('hello'); - }; - let handleNext3 = function() { - console.log('hello'); - }; - useEffect(() => { - handleNext1(); - return Store.subscribe(() => handleNext1()); - }, [handleNext1]); - useLayoutEffect(() => { - handleNext2(); - return Store.subscribe(() => handleNext2()); - }, [handleNext2]); - useMemo(() => { - handleNext3(); - return Store.subscribe(() => handleNext3()); - }, [handleNext3]); - } - `, errors: [ - "The 'handleNext1' function makes the dependencies of useEffect Hook " + - '(at line 15) change on every render. Move it inside the useEffect callback. ' + - "Alternatively, wrap the 'handleNext1' definition into its own useCallback() Hook.", - "The 'handleNext2' function makes the dependencies of useLayoutEffect Hook " + - '(at line 19) change on every render. Move it inside the useLayoutEffect callback. ' + - "Alternatively, wrap the 'handleNext2' definition into its own useCallback() Hook.", - "The 'handleNext3' function makes the dependencies of useMemo Hook " + - '(at line 23) change on every render. Move it inside the useMemo callback. ' + - "Alternatively, wrap the 'handleNext3' definition into its own useCallback() Hook.", + { + message: + "The 'handleNext1' function makes the dependencies of useEffect Hook " + + '(at line 15) change on every render. Move it inside the useEffect callback. ' + + "Alternatively, wrap the 'handleNext1' definition into its own useCallback() Hook.", + suggestions: undefined, + }, + { + message: + "The 'handleNext2' function makes the dependencies of useLayoutEffect Hook " + + '(at line 19) change on every render. Move it inside the useLayoutEffect callback. ' + + "Alternatively, wrap the 'handleNext2' definition into its own useCallback() Hook.", + suggestions: undefined, + }, + { + message: + "The 'handleNext3' function makes the dependencies of useMemo Hook " + + '(at line 23) change on every render. Move it inside the useMemo callback. ' + + "Alternatively, wrap the 'handleNext3' definition into its own useCallback() Hook.", + suggestions: undefined, + }, ], }, { - code: ` + code: normalizeIndent` function MyComponent(props) { function handleNext1() { console.log('hello'); @@ -3880,58 +4902,118 @@ const tests = { ); } `, - // Autofix wraps into useCallback where possible (variables only) - // because they are only referenced outside the effect. - output: ` - function MyComponent(props) { - function handleNext1() { - console.log('hello'); - } - const handleNext2 = useCallback(() => { - console.log('hello'); - }); - let handleNext3 = useCallback(function() { - console.log('hello'); - }); - useEffect(() => { - handleNext1(); - return Store.subscribe(() => handleNext1()); - }, [handleNext1]); - useLayoutEffect(() => { - handleNext2(); - return Store.subscribe(() => handleNext2()); - }, [handleNext2]); - useMemo(() => { - handleNext3(); - return Store.subscribe(() => handleNext3()); - }, [handleNext3]); - return ( -
{ - handleNext1(); - setTimeout(handleNext2); - setTimeout(() => { - handleNext3(); - }); - }} - /> - ); - } - `, errors: [ - "The 'handleNext1' function makes the dependencies of useEffect Hook " + - '(at line 15) change on every render. To fix this, wrap the ' + - "'handleNext1' definition into its own useCallback() Hook.", - "The 'handleNext2' function makes the dependencies of useLayoutEffect Hook " + - '(at line 19) change on every render. To fix this, wrap the ' + - "'handleNext2' definition into its own useCallback() Hook.", - "The 'handleNext3' function makes the dependencies of useMemo Hook " + - '(at line 23) change on every render. To fix this, wrap the ' + - "'handleNext3' definition into its own useCallback() Hook.", + { + message: + "The 'handleNext1' function makes the dependencies of useEffect Hook " + + '(at line 15) change on every render. To fix this, wrap the ' + + "'handleNext1' definition into its own useCallback() Hook.", + suggestions: undefined, + }, + { + message: + "The 'handleNext2' function makes the dependencies of useLayoutEffect Hook " + + '(at line 19) change on every render. To fix this, wrap the ' + + "'handleNext2' definition into its own useCallback() Hook.", + // Suggestion wraps into useCallback where possible (variables only) + // because they are only referenced outside the effect. + suggestions: [ + { + desc: + "Wrap the 'handleNext2' definition into its own useCallback() Hook.", + output: normalizeIndent` + function MyComponent(props) { + function handleNext1() { + console.log('hello'); + } + const handleNext2 = useCallback(() => { + console.log('hello'); + }); + let handleNext3 = function() { + console.log('hello'); + }; + useEffect(() => { + handleNext1(); + return Store.subscribe(() => handleNext1()); + }, [handleNext1]); + useLayoutEffect(() => { + handleNext2(); + return Store.subscribe(() => handleNext2()); + }, [handleNext2]); + useMemo(() => { + handleNext3(); + return Store.subscribe(() => handleNext3()); + }, [handleNext3]); + return ( +
{ + handleNext1(); + setTimeout(handleNext2); + setTimeout(() => { + handleNext3(); + }); + }} + /> + ); + } + `, + }, + ], + }, + { + message: + "The 'handleNext3' function makes the dependencies of useMemo Hook " + + '(at line 23) change on every render. To fix this, wrap the ' + + "'handleNext3' definition into its own useCallback() Hook.", + // Autofix wraps into useCallback where possible (variables only) + // because they are only referenced outside the effect. + suggestions: [ + { + desc: + "Wrap the 'handleNext3' definition into its own useCallback() Hook.", + output: normalizeIndent` + function MyComponent(props) { + function handleNext1() { + console.log('hello'); + } + const handleNext2 = () => { + console.log('hello'); + }; + let handleNext3 = useCallback(function() { + console.log('hello'); + }); + useEffect(() => { + handleNext1(); + return Store.subscribe(() => handleNext1()); + }, [handleNext1]); + useLayoutEffect(() => { + handleNext2(); + return Store.subscribe(() => handleNext2()); + }, [handleNext2]); + useMemo(() => { + handleNext3(); + return Store.subscribe(() => handleNext3()); + }, [handleNext3]); + return ( +
{ + handleNext1(); + setTimeout(handleNext2); + setTimeout(() => { + handleNext3(); + }); + }} + /> + ); + } + `, + }, + ], + }, ], }, { - code: ` + code: normalizeIndent` function MyComponent(props) { const handleNext1 = () => { console.log('hello'); @@ -3953,42 +5035,86 @@ const tests = { // effect. But it's used by more than one. So we // suggest useCallback() and use it for the autofix // where possible (variable but not declaration). - output: ` - function MyComponent(props) { - const handleNext1 = useCallback(() => { - console.log('hello'); - }); - function handleNext2() { - console.log('hello'); - } - useEffect(() => { - return Store.subscribe(handleNext1); - return Store.subscribe(handleNext2); - }, [handleNext1, handleNext2]); - useEffect(() => { - return Store.subscribe(handleNext1); - return Store.subscribe(handleNext2); - }, [handleNext1, handleNext2]); - } - `, // TODO: we could coalesce messages for the same function if it affects multiple Hooks. errors: [ - "The 'handleNext1' function makes the dependencies of useEffect Hook " + - '(at line 12) change on every render. To fix this, wrap the ' + - "'handleNext1' definition into its own useCallback() Hook.", - "The 'handleNext1' function makes the dependencies of useEffect Hook " + - '(at line 16) change on every render. To fix this, wrap the ' + - "'handleNext1' definition into its own useCallback() Hook.", - "The 'handleNext2' function makes the dependencies of useEffect Hook " + - '(at line 12) change on every render. To fix this, wrap the ' + - "'handleNext2' definition into its own useCallback() Hook.", - "The 'handleNext2' function makes the dependencies of useEffect Hook " + - '(at line 16) change on every render. To fix this, wrap the ' + - "'handleNext2' definition into its own useCallback() Hook.", + { + message: + "The 'handleNext1' function makes the dependencies of useEffect Hook " + + '(at line 12) change on every render. To fix this, wrap the ' + + "'handleNext1' definition into its own useCallback() Hook.", + suggestions: [ + { + desc: + "Wrap the 'handleNext1' definition into its own useCallback() Hook.", + output: normalizeIndent` + function MyComponent(props) { + const handleNext1 = useCallback(() => { + console.log('hello'); + }); + function handleNext2() { + console.log('hello'); + } + useEffect(() => { + return Store.subscribe(handleNext1); + return Store.subscribe(handleNext2); + }, [handleNext1, handleNext2]); + useEffect(() => { + return Store.subscribe(handleNext1); + return Store.subscribe(handleNext2); + }, [handleNext1, handleNext2]); + } + `, + }, + ], + }, + { + message: + "The 'handleNext1' function makes the dependencies of useEffect Hook " + + '(at line 16) change on every render. To fix this, wrap the ' + + "'handleNext1' definition into its own useCallback() Hook.", + suggestions: [ + { + desc: + "Wrap the 'handleNext1' definition into its own useCallback() Hook.", + output: normalizeIndent` + function MyComponent(props) { + const handleNext1 = useCallback(() => { + console.log('hello'); + }); + function handleNext2() { + console.log('hello'); + } + useEffect(() => { + return Store.subscribe(handleNext1); + return Store.subscribe(handleNext2); + }, [handleNext1, handleNext2]); + useEffect(() => { + return Store.subscribe(handleNext1); + return Store.subscribe(handleNext2); + }, [handleNext1, handleNext2]); + } + `, + }, + ], + }, + { + message: + "The 'handleNext2' function makes the dependencies of useEffect Hook " + + '(at line 12) change on every render. To fix this, wrap the ' + + "'handleNext2' definition into its own useCallback() Hook.", + suggestions: undefined, + }, + { + message: + "The 'handleNext2' function makes the dependencies of useEffect Hook " + + '(at line 16) change on every render. To fix this, wrap the ' + + "'handleNext2' definition into its own useCallback() Hook.", + suggestions: undefined, + }, ], }, { - code: ` + code: normalizeIndent` function MyComponent(props) { let handleNext = () => { console.log('hello'); @@ -4003,49 +5129,42 @@ const tests = { }, [handleNext]); } `, - // Normally we'd suggest moving handleNext inside an - // effect. But it's used more than once. - // TODO: our autofix here isn't quite sufficient because - // it only wraps the first definition. But seems ok. - output: ` - function MyComponent(props) { - let handleNext = useCallback(() => { - console.log('hello'); - }); - if (props.foo) { - handleNext = () => { - console.log('hello'); - }; - } - useEffect(() => { - return Store.subscribe(handleNext); - }, [handleNext]); - } - `, errors: [ - "The 'handleNext' function makes the dependencies of useEffect Hook " + - '(at line 13) change on every render. To fix this, wrap the ' + - "'handleNext' definition into its own useCallback() Hook.", + { + message: + "The 'handleNext' function makes the dependencies of useEffect Hook " + + '(at line 13) change on every render. To fix this, wrap the ' + + "'handleNext' definition into its own useCallback() Hook.", + // Normally we'd suggest moving handleNext inside an + // effect. But it's used more than once. + // TODO: our autofix here isn't quite sufficient because + // it only wraps the first definition. But seems ok. + suggestions: [ + { + desc: + "Wrap the 'handleNext' definition into its own useCallback() Hook.", + output: normalizeIndent` + function MyComponent(props) { + let handleNext = useCallback(() => { + console.log('hello'); + }); + if (props.foo) { + handleNext = () => { + console.log('hello'); + }; + } + useEffect(() => { + return Store.subscribe(handleNext); + }, [handleNext]); + } + `, + }, + ], + }, ], }, { - code: ` - function MyComponent(props) { - let [, setState] = useState(); - let taint = props.foo; - - function handleNext(value) { - let value2 = value * taint; - setState(value2); - console.log('hello'); - } - - useEffect(() => { - return Store.subscribe(handleNext); - }, [handleNext]); - } - `, - output: ` + code: normalizeIndent` function MyComponent(props) { let [, setState] = useState(); let taint = props.foo; @@ -4056,101 +5175,30 @@ const tests = { console.log('hello'); } - useEffect(() => { - return Store.subscribe(handleNext); - }, [handleNext]); - } - `, - errors: [ - `The 'handleNext' function makes the dependencies of ` + - `useEffect Hook (at line 14) change on every render. ` + - `Move it inside the useEffect callback. Alternatively, wrap the ` + - `'handleNext' definition into its own useCallback() Hook.`, - ], - }, - { - code: ` - function Counter() { - let [count, setCount] = useState(0); - - useEffect(() => { - let id = setInterval(() => { - setCount(count + 1); - }, 1000); - return () => clearInterval(id); - }, []); - - return

{count}

; - } - `, - output: ` - function Counter() { - let [count, setCount] = useState(0); - - useEffect(() => { - let id = setInterval(() => { - setCount(count + 1); - }, 1000); - return () => clearInterval(id); - }, [count]); - - return

{count}

; - } - `, - errors: [ - "React Hook useEffect has a missing dependency: 'count'. " + - 'Either include it or remove the dependency array. ' + - `You can also do a functional update 'setCount(c => ...)' if you ` + - `only need 'count' in the 'setCount' call.`, - ], - }, - { - code: ` - function Counter() { - let [count, setCount] = useState(0); - let [increment, setIncrement] = useState(0); - - useEffect(() => { - let id = setInterval(() => { - setCount(count + increment); - }, 1000); - return () => clearInterval(id); - }, []); - - return

{count}

; - } - `, - output: ` - function Counter() { - let [count, setCount] = useState(0); - let [increment, setIncrement] = useState(0); - - useEffect(() => { - let id = setInterval(() => { - setCount(count + increment); - }, 1000); - return () => clearInterval(id); - }, [count, increment]); - - return

{count}

; + useEffect(() => { + return Store.subscribe(handleNext); + }, [handleNext]); } `, errors: [ - "React Hook useEffect has missing dependencies: 'count' and 'increment'. " + - 'Either include them or remove the dependency array. ' + - `You can also do a functional update 'setCount(c => ...)' if you ` + - `only need 'count' in the 'setCount' call.`, + { + message: + `The 'handleNext' function makes the dependencies of ` + + `useEffect Hook (at line 14) change on every render. ` + + `Move it inside the useEffect callback. Alternatively, wrap the ` + + `'handleNext' definition into its own useCallback() Hook.`, + suggestions: undefined, + }, ], }, { - code: ` + code: normalizeIndent` function Counter() { let [count, setCount] = useState(0); - let [increment, setIncrement] = useState(0); useEffect(() => { let id = setInterval(() => { - setCount(count => count + increment); + setCount(count + 1); }, 1000); return () => clearInterval(id); }, []); @@ -4158,33 +5206,86 @@ const tests = { return

{count}

; } `, - output: ` + errors: [ + { + message: + "React Hook useEffect has a missing dependency: 'count'. " + + 'Either include it or remove the dependency array. ' + + `You can also do a functional update 'setCount(c => ...)' if you ` + + `only need 'count' in the 'setCount' call.`, + suggestions: [ + { + desc: 'Update the dependencies array to be: [count]', + output: normalizeIndent` + function Counter() { + let [count, setCount] = useState(0); + + useEffect(() => { + let id = setInterval(() => { + setCount(count + 1); + }, 1000); + return () => clearInterval(id); + }, [count]); + + return

{count}

; + } + `, + }, + ], + }, + ], + }, + { + code: normalizeIndent` function Counter() { let [count, setCount] = useState(0); let [increment, setIncrement] = useState(0); useEffect(() => { let id = setInterval(() => { - setCount(count => count + increment); + setCount(count + increment); }, 1000); return () => clearInterval(id); - }, [increment]); + }, []); return

{count}

; } `, errors: [ - "React Hook useEffect has a missing dependency: 'increment'. " + - 'Either include it or remove the dependency array. ' + - `You can also replace multiple useState variables with useReducer ` + - `if 'setCount' needs the current value of 'increment'.`, + { + message: + "React Hook useEffect has missing dependencies: 'count' and 'increment'. " + + 'Either include them or remove the dependency array. ' + + `You can also do a functional update 'setCount(c => ...)' if you ` + + `only need 'count' in the 'setCount' call.`, + suggestions: [ + { + desc: 'Update the dependencies array to be: [count, increment]', + output: normalizeIndent` + function Counter() { + let [count, setCount] = useState(0); + let [increment, setIncrement] = useState(0); + + useEffect(() => { + let id = setInterval(() => { + setCount(count + increment); + }, 1000); + return () => clearInterval(id); + }, [count, increment]); + + return

{count}

; + } + `, + }, + ], + }, ], }, { - code: ` + code: normalizeIndent` function Counter() { let [count, setCount] = useState(0); - let increment = useCustomHook(); + let [increment, setIncrement] = useState(0); useEffect(() => { let id = setInterval(() => { @@ -4196,7 +5297,38 @@ const tests = { return

{count}

; } `, - output: ` + errors: [ + { + message: + "React Hook useEffect has a missing dependency: 'increment'. " + + 'Either include it or remove the dependency array. ' + + `You can also replace multiple useState variables with useReducer ` + + `if 'setCount' needs the current value of 'increment'.`, + suggestions: [ + { + desc: 'Update the dependencies array to be: [increment]', + output: normalizeIndent` + function Counter() { + let [count, setCount] = useState(0); + let [increment, setIncrement] = useState(0); + + useEffect(() => { + let id = setInterval(() => { + setCount(count => count + increment); + }, 1000); + return () => clearInterval(id); + }, [increment]); + + return

{count}

; + } + `, + }, + ], + }, + ], + }, + { + code: normalizeIndent` function Counter() { let [count, setCount] = useState(0); let increment = useCustomHook(); @@ -4206,7 +5338,7 @@ const tests = { setCount(count => count + increment); }, 1000); return () => clearInterval(id); - }, [increment]); + }, []); return

{count}

; } @@ -4215,12 +5347,35 @@ const tests = { // because we don't know if it's safe for it to close over a value. // We only show it for state variables (and possibly props). errors: [ - "React Hook useEffect has a missing dependency: 'increment'. " + - 'Either include it or remove the dependency array.', + { + message: + "React Hook useEffect has a missing dependency: 'increment'. " + + 'Either include it or remove the dependency array.', + suggestions: [ + { + desc: 'Update the dependencies array to be: [increment]', + output: normalizeIndent` + function Counter() { + let [count, setCount] = useState(0); + let increment = useCustomHook(); + + useEffect(() => { + let id = setInterval(() => { + setCount(count => count + increment); + }, 1000); + return () => clearInterval(id); + }, [increment]); + + return

{count}

; + } + `, + }, + ], + }, ], }, { - code: ` + code: normalizeIndent` function Counter({ step }) { let [count, setCount] = useState(0); @@ -4238,52 +5393,42 @@ const tests = { return

{count}

; } `, - output: ` - function Counter({ step }) { - let [count, setCount] = useState(0); - - function increment(x) { - return x + step; - } - - useEffect(() => { - let id = setInterval(() => { - setCount(count => increment(count)); - }, 1000); - return () => clearInterval(id); - }, [increment]); - - return

{count}

; - } - `, // This intentionally doesn't show the reducer message // because we don't know if it's safe for it to close over a value. // We only show it for state variables (and possibly props). errors: [ - "React Hook useEffect has a missing dependency: 'increment'. " + - 'Either include it or remove the dependency array.', - ], - }, - { - code: ` - function Counter({ step }) { - let [count, setCount] = useState(0); + { + message: + "React Hook useEffect has a missing dependency: 'increment'. " + + 'Either include it or remove the dependency array.', + suggestions: [ + { + desc: 'Update the dependencies array to be: [increment]', + output: normalizeIndent` + function Counter({ step }) { + let [count, setCount] = useState(0); - function increment(x) { - return x + step; - } + function increment(x) { + return x + step; + } - useEffect(() => { - let id = setInterval(() => { - setCount(count => increment(count)); - }, 1000); - return () => clearInterval(id); - }, [increment]); + useEffect(() => { + let id = setInterval(() => { + setCount(count => increment(count)); + }, 1000); + return () => clearInterval(id); + }, [increment]); - return

{count}

; - } - `, - output: ` + return

{count}

; + } + `, + }, + ], + }, + ], + }, + { + code: normalizeIndent` function Counter({ step }) { let [count, setCount] = useState(0); @@ -4302,14 +5447,18 @@ const tests = { } `, errors: [ - `The 'increment' function makes the dependencies of useEffect Hook ` + - `(at line 14) change on every render. Move it inside the useEffect callback. ` + - `Alternatively, wrap the \'increment\' definition into its own ` + - `useCallback() Hook.`, + { + message: + `The 'increment' function makes the dependencies of useEffect Hook ` + + `(at line 14) change on every render. Move it inside the useEffect callback. ` + + `Alternatively, wrap the \'increment\' definition into its own ` + + `useCallback() Hook.`, + suggestions: undefined, + }, ], }, { - code: ` + code: normalizeIndent` function Counter({ increment }) { let [count, setCount] = useState(0); @@ -4323,29 +5472,37 @@ const tests = { return

{count}

; } `, - output: ` - function Counter({ increment }) { - let [count, setCount] = useState(0); + errors: [ + { + message: + "React Hook useEffect has a missing dependency: 'increment'. " + + 'Either include it or remove the dependency array. ' + + `If 'setCount' needs the current value of 'increment', ` + + `you can also switch to useReducer instead of useState and read 'increment' in the reducer.`, + suggestions: [ + { + desc: 'Update the dependencies array to be: [increment]', + output: normalizeIndent` + function Counter({ increment }) { + let [count, setCount] = useState(0); - useEffect(() => { - let id = setInterval(() => { - setCount(count => count + increment); - }, 1000); - return () => clearInterval(id); - }, [increment]); + useEffect(() => { + let id = setInterval(() => { + setCount(count => count + increment); + }, 1000); + return () => clearInterval(id); + }, [increment]); - return

{count}

; - } - `, - errors: [ - "React Hook useEffect has a missing dependency: 'increment'. " + - 'Either include it or remove the dependency array. ' + - `If 'setCount' needs the current value of 'increment', ` + - `you can also switch to useReducer instead of useState and read 'increment' in the reducer.`, + return

{count}

; + } + `, + }, + ], + }, ], }, { - code: ` + code: normalizeIndent` function Counter() { const [count, setCount] = useState(0); @@ -4363,36 +5520,44 @@ const tests = { return

{count}

; } `, - output: ` - function Counter() { - const [count, setCount] = useState(0); - - function tick() { - setCount(count + 1); - } - - useEffect(() => { - let id = setInterval(() => { - tick(); - }, 1000); - return () => clearInterval(id); - }, [tick]); - - return

{count}

; - } - `, // TODO: ideally this should suggest useState updater form // since this code doesn't actually work. The autofix could // at least avoid suggesting 'tick' since it's obviously // always different, and thus useless. errors: [ - "React Hook useEffect has a missing dependency: 'tick'. " + - 'Either include it or remove the dependency array.', + { + message: + "React Hook useEffect has a missing dependency: 'tick'. " + + 'Either include it or remove the dependency array.', + suggestions: [ + { + desc: 'Update the dependencies array to be: [tick]', + output: normalizeIndent` + function Counter() { + const [count, setCount] = useState(0); + + function tick() { + setCount(count + 1); + } + + useEffect(() => { + let id = setInterval(() => { + tick(); + }, 1000); + return () => clearInterval(id); + }, [tick]); + + return

{count}

; + } + `, + }, + ], + }, ], }, { // Regression test for a crash - code: ` + code: normalizeIndent` function Podcasts() { useEffect(() => { alert(podcasts); @@ -4400,24 +5565,32 @@ const tests = { let [podcasts, setPodcasts] = useState(null); } `, - // Note: this autofix is shady because - // the variable is used before declaration. - // TODO: Maybe we can catch those fixes and not autofix. - output: ` - function Podcasts() { - useEffect(() => { - alert(podcasts); - }, [podcasts]); - let [podcasts, setPodcasts] = useState(null); - } - `, errors: [ - `React Hook useEffect has a missing dependency: 'podcasts'. ` + - `Either include it or remove the dependency array.`, + { + message: + `React Hook useEffect has a missing dependency: 'podcasts'. ` + + `Either include it or remove the dependency array.`, + // Note: this autofix is shady because + // the variable is used before declaration. + // TODO: Maybe we can catch those fixes and not autofix. + suggestions: [ + { + desc: 'Update the dependencies array to be: [podcasts]', + output: normalizeIndent` + function Podcasts() { + useEffect(() => { + alert(podcasts); + }, [podcasts]); + let [podcasts, setPodcasts] = useState(null); + } + `, + }, + ], + }, ], }, { - code: ` + code: normalizeIndent` function Podcasts({ fetchPodcasts, id }) { let [podcasts, setPodcasts] = useState(null); useEffect(() => { @@ -4425,23 +5598,31 @@ const tests = { }, [id]); } `, - output: ` - function Podcasts({ fetchPodcasts, id }) { - let [podcasts, setPodcasts] = useState(null); - useEffect(() => { - fetchPodcasts(id).then(setPodcasts); - }, [fetchPodcasts, id]); - } - `, errors: [ - `React Hook useEffect has a missing dependency: 'fetchPodcasts'. ` + - `Either include it or remove the dependency array. ` + - `If 'fetchPodcasts' changes too often, ` + - `find the parent component that defines it and wrap that definition in useCallback.`, + { + message: + `React Hook useEffect has a missing dependency: 'fetchPodcasts'. ` + + `Either include it or remove the dependency array. ` + + `If 'fetchPodcasts' changes too often, ` + + `find the parent component that defines it and wrap that definition in useCallback.`, + suggestions: [ + { + desc: 'Update the dependencies array to be: [fetchPodcasts, id]', + output: normalizeIndent` + function Podcasts({ fetchPodcasts, id }) { + let [podcasts, setPodcasts] = useState(null); + useEffect(() => { + fetchPodcasts(id).then(setPodcasts); + }, [fetchPodcasts, id]); + } + `, + }, + ], + }, ], }, { - code: ` + code: normalizeIndent` function Podcasts({ api: { fetchPodcasts }, id }) { let [podcasts, setPodcasts] = useState(null); useEffect(() => { @@ -4449,23 +5630,31 @@ const tests = { }, [id]); } `, - output: ` - function Podcasts({ api: { fetchPodcasts }, id }) { - let [podcasts, setPodcasts] = useState(null); - useEffect(() => { - fetchPodcasts(id).then(setPodcasts); - }, [fetchPodcasts, id]); - } - `, errors: [ - `React Hook useEffect has a missing dependency: 'fetchPodcasts'. ` + - `Either include it or remove the dependency array. ` + - `If 'fetchPodcasts' changes too often, ` + - `find the parent component that defines it and wrap that definition in useCallback.`, + { + message: + `React Hook useEffect has a missing dependency: 'fetchPodcasts'. ` + + `Either include it or remove the dependency array. ` + + `If 'fetchPodcasts' changes too often, ` + + `find the parent component that defines it and wrap that definition in useCallback.`, + suggestions: [ + { + desc: 'Update the dependencies array to be: [fetchPodcasts, id]', + output: normalizeIndent` + function Podcasts({ api: { fetchPodcasts }, id }) { + let [podcasts, setPodcasts] = useState(null); + useEffect(() => { + fetchPodcasts(id).then(setPodcasts); + }, [fetchPodcasts, id]); + } + `, + }, + ], + }, ], }, { - code: ` + code: normalizeIndent` function Podcasts({ fetchPodcasts, fetchPodcasts2, id }) { let [podcasts, setPodcasts] = useState(null); useEffect(() => { @@ -4477,27 +5666,36 @@ const tests = { }, [id]); } `, - output: ` - function Podcasts({ fetchPodcasts, fetchPodcasts2, id }) { - let [podcasts, setPodcasts] = useState(null); - useEffect(() => { - setTimeout(() => { - console.log(id); - fetchPodcasts(id).then(setPodcasts); - fetchPodcasts2(id).then(setPodcasts); - }); - }, [fetchPodcasts, fetchPodcasts2, id]); - } - `, errors: [ - `React Hook useEffect has missing dependencies: 'fetchPodcasts' and 'fetchPodcasts2'. ` + - `Either include them or remove the dependency array. ` + - `If 'fetchPodcasts' changes too often, ` + - `find the parent component that defines it and wrap that definition in useCallback.`, + { + message: + `React Hook useEffect has missing dependencies: 'fetchPodcasts' and 'fetchPodcasts2'. ` + + `Either include them or remove the dependency array. ` + + `If 'fetchPodcasts' changes too often, ` + + `find the parent component that defines it and wrap that definition in useCallback.`, + suggestions: [ + { + desc: + 'Update the dependencies array to be: [fetchPodcasts, fetchPodcasts2, id]', + output: normalizeIndent` + function Podcasts({ fetchPodcasts, fetchPodcasts2, id }) { + let [podcasts, setPodcasts] = useState(null); + useEffect(() => { + setTimeout(() => { + console.log(id); + fetchPodcasts(id).then(setPodcasts); + fetchPodcasts2(id).then(setPodcasts); + }); + }, [fetchPodcasts, fetchPodcasts2, id]); + } + `, + }, + ], + }, ], }, { - code: ` + code: normalizeIndent` function Podcasts({ fetchPodcasts, id }) { let [podcasts, setPodcasts] = useState(null); useEffect(() => { @@ -4506,26 +5704,34 @@ const tests = { }, [id]); } `, - output: ` - function Podcasts({ fetchPodcasts, id }) { - let [podcasts, setPodcasts] = useState(null); - useEffect(() => { - console.log(fetchPodcasts); - fetchPodcasts(id).then(setPodcasts); - }, [fetchPodcasts, id]); - } - `, errors: [ - `React Hook useEffect has a missing dependency: 'fetchPodcasts'. ` + - `Either include it or remove the dependency array. ` + - `If 'fetchPodcasts' changes too often, ` + - `find the parent component that defines it and wrap that definition in useCallback.`, + { + message: + `React Hook useEffect has a missing dependency: 'fetchPodcasts'. ` + + `Either include it or remove the dependency array. ` + + `If 'fetchPodcasts' changes too often, ` + + `find the parent component that defines it and wrap that definition in useCallback.`, + suggestions: [ + { + desc: 'Update the dependencies array to be: [fetchPodcasts, id]', + output: normalizeIndent` + function Podcasts({ fetchPodcasts, id }) { + let [podcasts, setPodcasts] = useState(null); + useEffect(() => { + console.log(fetchPodcasts); + fetchPodcasts(id).then(setPodcasts); + }, [fetchPodcasts, id]); + } + `, + }, + ], + }, ], }, { // The mistake here is that it was moved inside the effect // so it can't be referenced in the deps array. - code: ` + code: normalizeIndent` function Thing() { useEffect(() => { const fetchData = async () => {}; @@ -4533,21 +5739,29 @@ const tests = { }, [fetchData]); } `, - output: ` - function Thing() { - useEffect(() => { - const fetchData = async () => {}; - fetchData(); - }, []); - } - `, errors: [ - `React Hook useEffect has an unnecessary dependency: 'fetchData'. ` + - `Either exclude it or remove the dependency array.`, + { + message: + `React Hook useEffect has an unnecessary dependency: 'fetchData'. ` + + `Either exclude it or remove the dependency array.`, + suggestions: [ + { + desc: 'Update the dependencies array to be: []', + output: normalizeIndent` + function Thing() { + useEffect(() => { + const fetchData = async () => {}; + fetchData(); + }, []); + } + `, + }, + ], + }, ], }, { - code: ` + code: normalizeIndent` function Hello() { const [state, setState] = useState(0); useEffect(() => { @@ -4555,22 +5769,30 @@ const tests = { }); } `, - output: ` - function Hello() { - const [state, setState] = useState(0); - useEffect(() => { - setState({}); - }, []); - } - `, errors: [ - `React Hook useEffect contains a call to 'setState'. ` + - `Without a list of dependencies, this can lead to an infinite chain of updates. ` + - `To fix this, pass [] as a second argument to the useEffect Hook.`, + { + message: + `React Hook useEffect contains a call to 'setState'. ` + + `Without a list of dependencies, this can lead to an infinite chain of updates. ` + + `To fix this, pass [] as a second argument to the useEffect Hook.`, + suggestions: [ + { + desc: 'Add dependencies array: []', + output: normalizeIndent` + function Hello() { + const [state, setState] = useState(0); + useEffect(() => { + setState({}); + }, []); + } + `, + }, + ], + }, ], }, { - code: ` + code: normalizeIndent` function Hello() { const [data, setData] = useState(0); useEffect(() => { @@ -4578,22 +5800,30 @@ const tests = { }); } `, - output: ` - function Hello() { - const [data, setData] = useState(0); - useEffect(() => { - fetchData.then(setData); - }, []); - } - `, errors: [ - `React Hook useEffect contains a call to 'setData'. ` + - `Without a list of dependencies, this can lead to an infinite chain of updates. ` + - `To fix this, pass [] as a second argument to the useEffect Hook.`, + { + message: + `React Hook useEffect contains a call to 'setData'. ` + + `Without a list of dependencies, this can lead to an infinite chain of updates. ` + + `To fix this, pass [] as a second argument to the useEffect Hook.`, + suggestions: [ + { + desc: 'Add dependencies array: []', + output: normalizeIndent` + function Hello() { + const [data, setData] = useState(0); + useEffect(() => { + fetchData.then(setData); + }, []); + } + `, + }, + ], + }, ], }, { - code: ` + code: normalizeIndent` function Hello({ country }) { const [data, setData] = useState(0); useEffect(() => { @@ -4601,22 +5831,30 @@ const tests = { }); } `, - output: ` - function Hello({ country }) { - const [data, setData] = useState(0); - useEffect(() => { - fetchData(country).then(setData); - }, [country]); - } - `, errors: [ - `React Hook useEffect contains a call to 'setData'. ` + - `Without a list of dependencies, this can lead to an infinite chain of updates. ` + - `To fix this, pass [country] as a second argument to the useEffect Hook.`, + { + message: + `React Hook useEffect contains a call to 'setData'. ` + + `Without a list of dependencies, this can lead to an infinite chain of updates. ` + + `To fix this, pass [country] as a second argument to the useEffect Hook.`, + suggestions: [ + { + desc: 'Add dependencies array: [country]', + output: normalizeIndent` + function Hello({ country }) { + const [data, setData] = useState(0); + useEffect(() => { + fetchData(country).then(setData); + }, [country]); + } + `, + }, + ], + }, ], }, { - code: ` + code: normalizeIndent` function Hello({ prop1, prop2 }) { const [state, setState] = useState(0); useEffect(() => { @@ -4626,94 +5864,108 @@ const tests = { }); } `, - output: ` - function Hello({ prop1, prop2 }) { - const [state, setState] = useState(0); - useEffect(() => { - if (prop1) { - setState(prop2); - } - }, [prop1, prop2]); - } - `, errors: [ - `React Hook useEffect contains a call to 'setState'. ` + - `Without a list of dependencies, this can lead to an infinite chain of updates. ` + - `To fix this, pass [prop1, prop2] as a second argument to the useEffect Hook.`, + { + message: + `React Hook useEffect contains a call to 'setState'. ` + + `Without a list of dependencies, this can lead to an infinite chain of updates. ` + + `To fix this, pass [prop1, prop2] as a second argument to the useEffect Hook.`, + suggestions: [ + { + desc: 'Add dependencies array: [prop1, prop2]', + output: normalizeIndent` + function Hello({ prop1, prop2 }) { + const [state, setState] = useState(0); + useEffect(() => { + if (prop1) { + setState(prop2); + } + }, [prop1, prop2]); + } + `, + }, + ], + }, ], }, { - code: ` - function Thing() { - useEffect(async () => {}, []); - } - `, - output: ` + code: normalizeIndent` function Thing() { useEffect(async () => {}, []); } `, errors: [ - `Effect callbacks are synchronous to prevent race conditions. ` + - `Put the async function inside:\n\n` + - 'useEffect(() => {\n' + - ' async function fetchData() {\n' + - ' // You can await here\n' + - ' const response = await MyAPI.getData(someId);\n' + - ' // ...\n' + - ' }\n' + - ' fetchData();\n' + - `}, [someId]); // Or [] if effect doesn't need props or state\n\n` + - 'Learn more about data fetching with Hooks: https://fb.me/react-hooks-data-fetching', + { + message: + `Effect callbacks are synchronous to prevent race conditions. ` + + `Put the async function inside:\n\n` + + 'useEffect(() => {\n' + + ' async function fetchData() {\n' + + ' // You can await here\n' + + ' const response = await MyAPI.getData(someId);\n' + + ' // ...\n' + + ' }\n' + + ' fetchData();\n' + + `}, [someId]); // Or [] if effect doesn't need props or state\n\n` + + 'Learn more about data fetching with Hooks: https://fb.me/react-hooks-data-fetching', + suggestions: undefined, + }, ], }, { - code: ` - function Thing() { - useEffect(async () => {}); - } - `, - output: ` + code: normalizeIndent` function Thing() { useEffect(async () => {}); } `, errors: [ - `Effect callbacks are synchronous to prevent race conditions. ` + - `Put the async function inside:\n\n` + - 'useEffect(() => {\n' + - ' async function fetchData() {\n' + - ' // You can await here\n' + - ' const response = await MyAPI.getData(someId);\n' + - ' // ...\n' + - ' }\n' + - ' fetchData();\n' + - `}, [someId]); // Or [] if effect doesn't need props or state\n\n` + - 'Learn more about data fetching with Hooks: https://fb.me/react-hooks-data-fetching', + { + message: + `Effect callbacks are synchronous to prevent race conditions. ` + + `Put the async function inside:\n\n` + + 'useEffect(() => {\n' + + ' async function fetchData() {\n' + + ' // You can await here\n' + + ' const response = await MyAPI.getData(someId);\n' + + ' // ...\n' + + ' }\n' + + ' fetchData();\n' + + `}, [someId]); // Or [] if effect doesn't need props or state\n\n` + + 'Learn more about data fetching with Hooks: https://fb.me/react-hooks-data-fetching', + suggestions: undefined, + }, ], }, { - code: ` + code: normalizeIndent` function Example() { const foo = useCallback(() => { foo(); }, [foo]); } `, - output: ` - function Example() { - const foo = useCallback(() => { - foo(); - }, []); - } - `, errors: [ - "React Hook useCallback has an unnecessary dependency: 'foo'. " + - 'Either exclude it or remove the dependency array.', + { + message: + "React Hook useCallback has an unnecessary dependency: 'foo'. " + + 'Either exclude it or remove the dependency array.', + suggestions: [ + { + desc: 'Update the dependencies array to be: []', + output: normalizeIndent` + function Example() { + const foo = useCallback(() => { + foo(); + }, []); + } + `, + }, + ], + }, ], }, { - code: ` + code: normalizeIndent` function Example({ prop }) { const foo = useCallback(() => { prop.hello(foo); @@ -4723,19 +5975,27 @@ const tests = { }, [foo]); } `, - output: ` - function Example({ prop }) { - const foo = useCallback(() => { - prop.hello(foo); - }, [prop]); - const bar = useCallback(() => { - foo(); - }, [foo]); - } - `, errors: [ - "React Hook useCallback has a missing dependency: 'prop'. " + - 'Either include it or remove the dependency array.', + { + message: + "React Hook useCallback has a missing dependency: 'prop'. " + + 'Either include it or remove the dependency array.', + suggestions: [ + { + desc: 'Update the dependencies array to be: [prop]', + output: normalizeIndent` + function Example({ prop }) { + const foo = useCallback(() => { + prop.hello(foo); + }, [prop]); + const bar = useCallback(() => { + foo(); + }, [foo]); + } + `, + }, + ], + }, ], }, ], diff --git a/packages/eslint-plugin-react-hooks/__tests__/ESLintRulesOfHooks-test.js b/packages/eslint-plugin-react-hooks/__tests__/ESLintRulesOfHooks-test.js index 662e9cc39b963..ad932fe4d792b 100644 --- a/packages/eslint-plugin-react-hooks/__tests__/ESLintRulesOfHooks-test.js +++ b/packages/eslint-plugin-react-hooks/__tests__/ESLintRulesOfHooks-test.js @@ -405,6 +405,18 @@ const tests = { useHook(); } `, + ` + // Valid because the neither the condition nor the loop affect the hook call. + function App(props) { + const someObject = {propA: true}; + for (const propName in someObject) { + if (propName === true) { + } else { + } + } + const [myState, setMyState] = useState(null); + } + `, ], invalid: [ { @@ -640,14 +652,7 @@ const tests = { } } `, - errors: [ - loopError('useHook1'), - - // NOTE: Small imprecision in error reporting due to caching means we - // have a conditional error here instead of a loop error. However, - // we will always get an error so this is acceptable. - conditionalError('useHook2', true), - ], + errors: [loopError('useHook1'), loopError('useHook2', true)], }, { code: ` diff --git a/packages/eslint-plugin-react-hooks/index.js b/packages/eslint-plugin-react-hooks/index.js index 7ab3284345f0b..c626dfc20ae1c 100644 --- a/packages/eslint-plugin-react-hooks/index.js +++ b/packages/eslint-plugin-react-hooks/index.js @@ -5,6 +5,4 @@ * LICENSE file in the root directory of this source tree. */ -'use strict'; - -module.exports = require('./src/index'); +export * from './src/index'; diff --git a/packages/eslint-plugin-react-hooks/package.json b/packages/eslint-plugin-react-hooks/package.json index 6f523bcd5b6fc..a9c9a4ed4b12b 100644 --- a/packages/eslint-plugin-react-hooks/package.json +++ b/packages/eslint-plugin-react-hooks/package.json @@ -1,7 +1,7 @@ { "name": "eslint-plugin-react-hooks", "description": "ESLint rules for React Hooks", - "version": "2.3.0", + "version": "2.5.0", "repository": { "type": "git", "url": "https://github.com/facebook/react.git", diff --git a/packages/eslint-plugin-react-hooks/src/ExhaustiveDeps.js b/packages/eslint-plugin-react-hooks/src/ExhaustiveDeps.js index 9ec5e562641eb..b30694014e25e 100644 --- a/packages/eslint-plugin-react-hooks/src/ExhaustiveDeps.js +++ b/packages/eslint-plugin-react-hooks/src/ExhaustiveDeps.js @@ -11,7 +11,6 @@ export default { meta: { - fixable: 'code', schema: [ { type: 'object', @@ -94,7 +93,7 @@ export default { reactiveHookName === 'useMemo' || reactiveHookName === 'useCallback' ) { - // TODO: Can this have an autofix? + // TODO: Can this have a suggestion? context.report({ node: node.parent.callee, message: @@ -558,12 +557,19 @@ export default { `To fix this, pass [` + suggestedDependencies.join(', ') + `] as a second argument to the ${reactiveHookName} Hook.`, - fix(fixer) { - return fixer.insertTextAfter( - node, - `, [${suggestedDependencies.join(', ')}]`, - ); - }, + suggest: [ + { + desc: `Add dependencies array: [${suggestedDependencies.join( + ', ', + )}]`, + fix(fixer) { + return fixer.insertTextAfter( + node, + `, [${suggestedDependencies.join(', ')}]`, + ); + }, + }, + ], }); } return; @@ -702,27 +708,35 @@ export default { ` Move it inside the ${reactiveHookName} callback. ` + `Alternatively, wrap the '${fn.name.name}' definition into its own useCallback() Hook.`; } + + let suggest; + // Only handle the simple case: arrow functions. + // Wrapping function declarations can mess up hoisting. + if (suggestUseCallback && fn.type === 'Variable') { + suggest = [ + { + desc: `Wrap the '${fn.name.name}' definition into its own useCallback() Hook.`, + fix(fixer) { + return [ + // TODO: also add an import? + fixer.insertTextBefore(fn.node.init, 'useCallback('), + // TODO: ideally we'd gather deps here but it would require + // restructuring the rule code. This will cause a new lint + // error to appear immediately for useCallback. Note we're + // not adding [] because would that changes semantics. + fixer.insertTextAfter(fn.node.init, ')'), + ]; + }, + }, + ]; + } // TODO: What if the function needs to change on every render anyway? // Should we suggest removing effect deps as an appropriate fix too? context.report({ // TODO: Why not report this at the dependency site? node: fn.node, message, - fix(fixer) { - // Only handle the simple case: arrow functions. - // Wrapping function declarations can mess up hoisting. - if (suggestUseCallback && fn.type === 'Variable') { - return [ - // TODO: also add an import? - fixer.insertTextBefore(fn.node.init, 'useCallback('), - // TODO: ideally we'd gather deps here but it would require - // restructuring the rule code. This will cause a new lint - // error to appear immediately for useCallback. Note we're - // not adding [] because would that changes semantics. - fixer.insertTextAfter(fn.node.init, ')'), - ]; - } - }, + suggest, }); }); return; @@ -1008,13 +1022,20 @@ export default { 'omit', )) + extraWarning, - fix(fixer) { - // TODO: consider preserving the comments or formatting? - return fixer.replaceText( - declaredDependenciesNode, - `[${suggestedDependencies.join(', ')}]`, - ); - }, + suggest: [ + { + desc: `Update the dependencies array to be: [${suggestedDependencies.join( + ', ', + )}]`, + fix(fixer) { + // TODO: consider preserving the comments or formatting? + return fixer.replaceText( + declaredDependenciesNode, + `[${suggestedDependencies.join(', ')}]`, + ); + }, + }, + ], }); } }, diff --git a/packages/eslint-plugin-react-hooks/src/RulesOfHooks.js b/packages/eslint-plugin-react-hooks/src/RulesOfHooks.js index dbcc9c3de59bb..0e5dac014611b 100644 --- a/packages/eslint-plugin-react-hooks/src/RulesOfHooks.js +++ b/packages/eslint-plugin-react-hooks/src/RulesOfHooks.js @@ -152,31 +152,33 @@ export default { * Populates `cyclic` with cyclic segments. */ - function countPathsFromStart(segment) { + function countPathsFromStart(segment, pathHistory) { const {cache} = countPathsFromStart; let paths = cache.get(segment.id); - - // If `paths` is null then we've found a cycle! Add it to `cyclic` and - // any other segments which are a part of this cycle. - if (paths === null) { - if (cyclic.has(segment.id)) { - return 0; - } else { - cyclic.add(segment.id); - for (const prevSegment of segment.prevSegments) { - countPathsFromStart(prevSegment); - } - return 0; + const pathList = new Set(pathHistory); + + // If `pathList` includes the current segment then we've found a cycle! + // We need to fill `cyclic` with all segments inside cycle + if (pathList.has(segment.id)) { + const pathArray = [...pathList]; + const cyclicSegments = pathArray.slice( + pathArray.indexOf(segment.id) + 1, + ); + for (const cyclicSegment of cyclicSegments) { + cyclic.add(cyclicSegment); } + + return 0; } + // add the current segment to pathList + pathList.add(segment.id); + // We have a cached `paths`. Return it. if (paths !== undefined) { return paths; } - // Compute `paths` and cache it. Guarding against cycles. - cache.set(segment.id, null); if (codePath.thrownSegments.includes(segment)) { paths = 0; } else if (segment.prevSegments.length === 0) { @@ -184,7 +186,7 @@ export default { } else { paths = 0; for (const prevSegment of segment.prevSegments) { - paths += countPathsFromStart(prevSegment); + paths += countPathsFromStart(prevSegment, pathList); } } @@ -221,31 +223,33 @@ export default { * Populates `cyclic` with cyclic segments. */ - function countPathsToEnd(segment) { + function countPathsToEnd(segment, pathHistory) { const {cache} = countPathsToEnd; let paths = cache.get(segment.id); - - // If `paths` is null then we've found a cycle! Add it to `cyclic` and - // any other segments which are a part of this cycle. - if (paths === null) { - if (cyclic.has(segment.id)) { - return 0; - } else { - cyclic.add(segment.id); - for (const nextSegment of segment.nextSegments) { - countPathsToEnd(nextSegment); - } - return 0; + let pathList = new Set(pathHistory); + + // If `pathList` includes the current segment then we've found a cycle! + // We need to fill `cyclic` with all segments inside cycle + if (pathList.has(segment.id)) { + const pathArray = Array.from(pathList); + const cyclicSegments = pathArray.slice( + pathArray.indexOf(segment.id) + 1, + ); + for (const cyclicSegment of cyclicSegments) { + cyclic.add(cyclicSegment); } + + return 0; } + // add the current segment to pathList + pathList.add(segment.id); + // We have a cached `paths`. Return it. if (paths !== undefined) { return paths; } - // Compute `paths` and cache it. Guarding against cycles. - cache.set(segment.id, null); if (codePath.thrownSegments.includes(segment)) { paths = 0; } else if (segment.nextSegments.length === 0) { @@ -253,11 +257,11 @@ export default { } else { paths = 0; for (const nextSegment of segment.nextSegments) { - paths += countPathsToEnd(nextSegment); + paths += countPathsToEnd(nextSegment, pathList); } } - cache.set(segment.id, paths); + cache.set(segment.id, paths); return paths; } diff --git a/packages/eslint-plugin-react-hooks/src/index.js b/packages/eslint-plugin-react-hooks/src/index.js index 606a6dff4e253..dca7b5c559eef 100644 --- a/packages/eslint-plugin-react-hooks/src/index.js +++ b/packages/eslint-plugin-react-hooks/src/index.js @@ -10,6 +10,16 @@ import RuleOfHooks from './RulesOfHooks'; import ExhaustiveDeps from './ExhaustiveDeps'; +export const configs = { + recommended: { + plugins: ['react-hooks'], + rules: { + 'react-hooks/rules-of-hooks': 'error', + 'react-hooks/exhaustive-deps': 'warn', + }, + }, +}; + export const rules = { 'rules-of-hooks': RuleOfHooks, 'exhaustive-deps': ExhaustiveDeps, diff --git a/packages/jest-react/package.json b/packages/jest-react/package.json index 6856964f79a94..506eb22a8e35a 100644 --- a/packages/jest-react/package.json +++ b/packages/jest-react/package.json @@ -1,6 +1,6 @@ { "name": "jest-react", - "version": "0.10.0", + "version": "0.11.0", "description": "Jest matchers and utilities for testing React components.", "main": "index.js", "repository": { @@ -23,6 +23,9 @@ "react": "^16.0.0", "react-test-renderer": "^16.0.0" }, + "dependencies": { + "object-assign": "^4.1.1" + }, "files": [ "LICENSE", "README.md", diff --git a/packages/legacy-events/EventPluginHub.js b/packages/legacy-events/EventPluginHub.js deleted file mode 100644 index 407946023028a..0000000000000 --- a/packages/legacy-events/EventPluginHub.js +++ /dev/null @@ -1,176 +0,0 @@ -/** - * Copyright (c) Facebook, Inc. and its affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - * @flow - */ - -import invariant from 'shared/invariant'; - -import { - injectEventPluginOrder, - injectEventPluginsByName, - plugins, -} from './EventPluginRegistry'; -import {getFiberCurrentPropsFromNode} from './EventPluginUtils'; -import accumulateInto from './accumulateInto'; -import {runEventsInBatch} from './EventBatching'; - -import type {PluginModule} from './PluginModuleType'; -import type {ReactSyntheticEvent} from './ReactSyntheticEventType'; -import type {Fiber} from 'react-reconciler/src/ReactFiber'; -import type {AnyNativeEvent} from './PluginModuleType'; -import type {TopLevelType} from './TopLevelEventTypes'; -import type {EventSystemFlags} from 'legacy-events/EventSystemFlags'; - -function isInteractive(tag) { - return ( - tag === 'button' || - tag === 'input' || - tag === 'select' || - tag === 'textarea' - ); -} - -function shouldPreventMouseEvent(name, type, props) { - switch (name) { - case 'onClick': - case 'onClickCapture': - case 'onDoubleClick': - case 'onDoubleClickCapture': - case 'onMouseDown': - case 'onMouseDownCapture': - case 'onMouseMove': - case 'onMouseMoveCapture': - case 'onMouseUp': - case 'onMouseUpCapture': - case 'onMouseEnter': - return !!(props.disabled && isInteractive(type)); - default: - return false; - } -} - -/** - * This is a unified interface for event plugins to be installed and configured. - * - * Event plugins can implement the following properties: - * - * `extractEvents` {function(string, DOMEventTarget, string, object): *} - * Required. When a top-level event is fired, this method is expected to - * extract synthetic events that will in turn be queued and dispatched. - * - * `eventTypes` {object} - * Optional, plugins that fire events must publish a mapping of registration - * names that are used to register listeners. Values of this mapping must - * be objects that contain `registrationName` or `phasedRegistrationNames`. - * - * `executeDispatch` {function(object, function, string)} - * Optional, allows plugins to override how an event gets dispatched. By - * default, the listener is simply invoked. - * - * Each plugin that is injected into `EventsPluginHub` is immediately operable. - * - * @public - */ - -/** - * Methods for injecting dependencies. - */ -export const injection = { - /** - * @param {array} InjectedEventPluginOrder - * @public - */ - injectEventPluginOrder, - - /** - * @param {object} injectedNamesToPlugins Map from names to plugin modules. - */ - injectEventPluginsByName, -}; - -/** - * @param {object} inst The instance, which is the source of events. - * @param {string} registrationName Name of listener (e.g. `onClick`). - * @return {?function} The stored callback. - */ -export function getListener(inst: Fiber, registrationName: string) { - let listener; - - // TODO: shouldPreventMouseEvent is DOM-specific and definitely should not - // live here; needs to be moved to a better place soon - const stateNode = inst.stateNode; - if (!stateNode) { - // Work in progress (ex: onload events in incremental mode). - return null; - } - const props = getFiberCurrentPropsFromNode(stateNode); - if (!props) { - // Work in progress. - return null; - } - listener = props[registrationName]; - if (shouldPreventMouseEvent(registrationName, inst.type, props)) { - return null; - } - invariant( - !listener || typeof listener === 'function', - 'Expected `%s` listener to be a function, instead got a value of `%s` type.', - registrationName, - typeof listener, - ); - return listener; -} - -/** - * Allows registered plugins an opportunity to extract events from top-level - * native browser events. - * - * @return {*} An accumulation of synthetic events. - * @internal - */ -function extractPluginEvents( - topLevelType: TopLevelType, - targetInst: null | Fiber, - nativeEvent: AnyNativeEvent, - nativeEventTarget: null | EventTarget, - eventSystemFlags: EventSystemFlags, -): Array | ReactSyntheticEvent | null { - let events = null; - for (let i = 0; i < plugins.length; i++) { - // Not every plugin in the ordering may be loaded at runtime. - const possiblePlugin: PluginModule = plugins[i]; - if (possiblePlugin) { - const extractedEvents = possiblePlugin.extractEvents( - topLevelType, - targetInst, - nativeEvent, - nativeEventTarget, - eventSystemFlags, - ); - if (extractedEvents) { - events = accumulateInto(events, extractedEvents); - } - } - } - return events; -} - -export function runExtractedPluginEventsInBatch( - topLevelType: TopLevelType, - targetInst: null | Fiber, - nativeEvent: AnyNativeEvent, - nativeEventTarget: null | EventTarget, - eventSystemFlags: EventSystemFlags, -) { - const events = extractPluginEvents( - topLevelType, - targetInst, - nativeEvent, - nativeEventTarget, - eventSystemFlags, - ); - runEventsInBatch(events); -} diff --git a/packages/legacy-events/EventPluginRegistry.js b/packages/legacy-events/EventPluginRegistry.js index 64c2a9a1e03e8..067e260313e73 100644 --- a/packages/legacy-events/EventPluginRegistry.js +++ b/packages/legacy-events/EventPluginRegistry.js @@ -89,7 +89,7 @@ function publishEventForPlugin( ): boolean { invariant( !eventNameDispatchConfigs.hasOwnProperty(eventName), - 'EventPluginHub: More than one plugin attempted to publish the same ' + + 'EventPluginRegistry: More than one plugin attempted to publish the same ' + 'event name, `%s`.', eventName, ); @@ -133,7 +133,7 @@ function publishRegistrationName( ): void { invariant( !registrationNameModules[registrationName], - 'EventPluginHub: More than one plugin attempted to publish the same ' + + 'EventPluginRegistry: More than one plugin attempted to publish the same ' + 'registration name, `%s`.', registrationName, ); @@ -153,8 +153,6 @@ function publishRegistrationName( /** * Registers plugins so that they can extract and dispatch events. - * - * @see {EventPluginHub} */ /** @@ -193,7 +191,6 @@ export const possibleRegistrationNames = __DEV__ ? {} : (null: any); * * @param {array} InjectedEventPluginOrder * @internal - * @see {EventPluginHub.injection.injectEventPluginOrder} */ export function injectEventPluginOrder( injectedEventPluginOrder: EventPluginOrder, @@ -209,14 +206,13 @@ export function injectEventPluginOrder( } /** - * Injects plugins to be used by `EventPluginHub`. The plugin names must be + * Injects plugins to be used by plugin event system. The plugin names must be * in the ordering injected by `injectEventPluginOrder`. * * Plugins can be injected as part of page initialization or on-the-fly. * * @param {object} injectedNamesToPlugins Map from names to plugin modules. * @internal - * @see {EventPluginHub.injection.injectEventPluginsByName} */ export function injectEventPluginsByName( injectedNamesToPlugins: NamesToPlugins, diff --git a/packages/legacy-events/EventPropagators.js b/packages/legacy-events/EventPropagators.js index dfaacdab4ddfc..d916de99a3b4e 100644 --- a/packages/legacy-events/EventPropagators.js +++ b/packages/legacy-events/EventPropagators.js @@ -11,7 +11,7 @@ import { traverseEnterLeave, } from 'shared/ReactTreeTraversal'; -import {getListener} from './EventPluginHub'; +import getListener from 'legacy-events/getListener'; import accumulateInto from './accumulateInto'; import forEachAccumulated from './forEachAccumulated'; diff --git a/packages/legacy-events/PluginModuleType.js b/packages/legacy-events/PluginModuleType.js index 04ec9e67357e5..988fdd3296c4d 100644 --- a/packages/legacy-events/PluginModuleType.js +++ b/packages/legacy-events/PluginModuleType.js @@ -29,7 +29,7 @@ export type PluginModule = { nativeTarget: NativeEvent, nativeEventTarget: null | EventTarget, eventSystemFlags: EventSystemFlags, + container?: Document | Element, ) => ?ReactSyntheticEvent, tapMoveThreshold?: number, - ... }; diff --git a/packages/legacy-events/ReactGenericBatching.js b/packages/legacy-events/ReactGenericBatching.js index e5f536ee15c8f..f3c8d4d768fbf 100644 --- a/packages/legacy-events/ReactGenericBatching.js +++ b/packages/legacy-events/ReactGenericBatching.js @@ -23,8 +23,8 @@ import {invokeGuardedCallbackAndCatchFirstError} from 'shared/ReactErrorUtils'; let batchedUpdatesImpl = function(fn, bookkeeping) { return fn(bookkeeping); }; -let discreteUpdatesImpl = function(fn, a, b, c) { - return fn(a, b, c); +let discreteUpdatesImpl = function(fn, a, b, c, d) { + return fn(a, b, c, d); }; let flushDiscreteUpdatesImpl = function() {}; let batchedEventUpdatesImpl = batchedUpdatesImpl; @@ -89,11 +89,11 @@ export function executeUserEventHandler(fn: any => void, value: any): void { } } -export function discreteUpdates(fn, a, b, c) { +export function discreteUpdates(fn, a, b, c, d) { const prevIsInsideEventHandler = isInsideEventHandler; isInsideEventHandler = true; try { - return discreteUpdatesImpl(fn, a, b, c); + return discreteUpdatesImpl(fn, a, b, c, d); } finally { isInsideEventHandler = prevIsInsideEventHandler; if (!isInsideEventHandler) { diff --git a/packages/legacy-events/ReactSyntheticEventType.js b/packages/legacy-events/ReactSyntheticEventType.js index 922d49d5c30f8..51929b8da2b6c 100644 --- a/packages/legacy-events/ReactSyntheticEventType.js +++ b/packages/legacy-events/ReactSyntheticEventType.js @@ -31,4 +31,4 @@ export type ReactSyntheticEvent = {| nativeEventTarget: EventTarget, ) => ReactSyntheticEvent, isPersistent: () => boolean, -|} & SyntheticEvent<>; +|}; diff --git a/packages/legacy-events/ResponderEventPlugin.js b/packages/legacy-events/ResponderEventPlugin.js index a5d4fe0251e53..a1e9ae7f0affa 100644 --- a/packages/legacy-events/ResponderEventPlugin.js +++ b/packages/legacy-events/ResponderEventPlugin.js @@ -282,7 +282,7 @@ to return true:wantsResponderID| | + + */ /** - * A note about event ordering in the `EventPluginHub`. + * A note about event ordering in the `EventPluginRegistry`. * * Suppose plugins are injected in the following order: * @@ -301,7 +301,7 @@ to return true:wantsResponderID| | * - When returned from `extractEvents`, deferred-dispatched events contain an * "accumulation" of deferred dispatches. * - These deferred dispatches are accumulated/collected before they are - * returned, but processed at a later time by the `EventPluginHub` (hence the + * returned, but processed at a later time by the `EventPluginRegistry` (hence the * name deferred). * * In the process of returning their deferred-dispatched events, event plugins @@ -325,9 +325,9 @@ to return true:wantsResponderID| | * - `R`s on-demand events (if any) (dispatched by `R` on-demand) * - `S`s on-demand events (if any) (dispatched by `S` on-demand) * - `C`s on-demand events (if any) (dispatched by `C` on-demand) - * - `R`s extracted events (if any) (dispatched by `EventPluginHub`) - * - `S`s extracted events (if any) (dispatched by `EventPluginHub`) - * - `C`s extracted events (if any) (dispatched by `EventPluginHub`) + * - `R`s extracted events (if any) (dispatched by `EventPluginRegistry`) + * - `S`s extracted events (if any) (dispatched by `EventPluginRegistry`) + * - `C`s extracted events (if any) (dispatched by `EventPluginRegistry`) * * In the case of `ResponderEventPlugin`: If the `startShouldSetResponder` * on-demand dispatch returns `true` (and some other details are satisfied) the @@ -336,9 +336,9 @@ to return true:wantsResponderID| | * will appear as follows: * * - `startShouldSetResponder` (`ResponderEventPlugin` dispatches on-demand) - * - `touchStartCapture` (`EventPluginHub` dispatches as usual) - * - `touchStart` (`EventPluginHub` dispatches as usual) - * - `responderGrant/Reject` (`EventPluginHub` dispatches as usual) + * - `touchStartCapture` (`EventPluginRegistry` dispatches as usual) + * - `touchStart` (`EventPluginRegistry` dispatches as usual) + * - `responderGrant/Reject` (`EventPluginRegistry` dispatches as usual) */ function setResponderAndExtractTransfer( diff --git a/packages/legacy-events/__tests__/EventPluginRegistry-test.internal.js b/packages/legacy-events/__tests__/EventPluginRegistry-test.internal.js index c56f2e5748166..2f30399cd8f0a 100644 --- a/packages/legacy-events/__tests__/EventPluginRegistry-test.internal.js +++ b/packages/legacy-events/__tests__/EventPluginRegistry-test.internal.js @@ -211,7 +211,7 @@ describe('EventPluginRegistry', () => { expect(function() { EventPluginRegistry.injectEventPluginOrder(['one', 'two']); }).toThrowError( - 'EventPluginHub: More than one plugin attempted to publish the same ' + + 'EventPluginRegistry: More than one plugin attempted to publish the same ' + 'registration name, `onPhotoCapture`.', ); }); diff --git a/packages/legacy-events/getListener.js b/packages/legacy-events/getListener.js new file mode 100644 index 0000000000000..8ef609c7b3913 --- /dev/null +++ b/packages/legacy-events/getListener.js @@ -0,0 +1,74 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * @flow + */ + +import type {Fiber} from 'react-reconciler/src/ReactFiber'; + +import invariant from 'shared/invariant'; + +import {getFiberCurrentPropsFromNode} from './EventPluginUtils'; + +function isInteractive(tag) { + return ( + tag === 'button' || + tag === 'input' || + tag === 'select' || + tag === 'textarea' + ); +} + +function shouldPreventMouseEvent(name, type, props) { + switch (name) { + case 'onClick': + case 'onClickCapture': + case 'onDoubleClick': + case 'onDoubleClickCapture': + case 'onMouseDown': + case 'onMouseDownCapture': + case 'onMouseMove': + case 'onMouseMoveCapture': + case 'onMouseUp': + case 'onMouseUpCapture': + case 'onMouseEnter': + return !!(props.disabled && isInteractive(type)); + default: + return false; + } +} + +/** + * @param {object} inst The instance, which is the source of events. + * @param {string} registrationName Name of listener (e.g. `onClick`). + * @return {?function} The stored callback. + */ +export default function getListener(inst: Fiber, registrationName: string) { + let listener; + + // TODO: shouldPreventMouseEvent is DOM-specific and definitely should not + // live here; needs to be moved to a better place soon + const stateNode = inst.stateNode; + if (!stateNode) { + // Work in progress (ex: onload events in incremental mode). + return null; + } + const props = getFiberCurrentPropsFromNode(stateNode); + if (!props) { + // Work in progress. + return null; + } + listener = props[registrationName]; + if (shouldPreventMouseEvent(registrationName, inst.type, props)) { + return null; + } + invariant( + !listener || typeof listener === 'function', + 'Expected `%s` listener to be a function, instead got a value of `%s` type.', + registrationName, + typeof listener, + ); + return listener; +} diff --git a/packages/react-art/Circle.js b/packages/react-art/Circle.js index 3e416db478be1..da13862b6b069 100644 --- a/packages/react-art/Circle.js +++ b/packages/react-art/Circle.js @@ -7,8 +7,4 @@ * @flow */ -'use strict'; - -const Circle = require('./npm/Circle'); - -module.exports = Circle; +export {default} from './npm/Circle'; diff --git a/packages/react-art/Rectangle.js b/packages/react-art/Rectangle.js index 49ac69654db46..b0f45951b2f5b 100644 --- a/packages/react-art/Rectangle.js +++ b/packages/react-art/Rectangle.js @@ -7,8 +7,4 @@ * @flow */ -'use strict'; - -const Rectangle = require('./npm/Rectangle'); - -module.exports = Rectangle; +export {default} from './npm/Rectangle'; diff --git a/packages/react-art/Wedge.js b/packages/react-art/Wedge.js index 46746200a8595..24c5cd5a13276 100644 --- a/packages/react-art/Wedge.js +++ b/packages/react-art/Wedge.js @@ -7,8 +7,4 @@ * @flow */ -'use strict'; - -const Wedge = require('./npm/Wedge'); - -module.exports = Wedge; +export {default} from './npm/Wedge'; diff --git a/packages/react-art/index.js b/packages/react-art/index.js index a379b066ff068..c6f706087b388 100644 --- a/packages/react-art/index.js +++ b/packages/react-art/index.js @@ -7,8 +7,4 @@ * @flow */ -'use strict'; - -const ReactART = require('./src/ReactART'); - -module.exports = ReactART; +export * from './src/ReactART'; diff --git a/packages/react-art/package.json b/packages/react-art/package.json index 863bb025dd96f..628e6964758b9 100644 --- a/packages/react-art/package.json +++ b/packages/react-art/package.json @@ -1,7 +1,7 @@ { "name": "react-art", "description": "React ART is a JavaScript library for drawing vector graphics using React. It provides declarative and reactive bindings to the ART library. Using the same declarative API you can render the output to either Canvas, SVG or VML (IE8).", - "version": "16.12.0", + "version": "16.13.0", "main": "index.js", "repository": { "type": "git", @@ -26,8 +26,7 @@ "create-react-class": "^15.6.2", "loose-envify": "^1.1.0", "object-assign": "^4.1.1", - "prop-types": "^15.6.2", - "scheduler": "^0.18.0" + "scheduler": "^0.19.0" }, "peerDependencies": { "react": "^16.0.0" diff --git a/packages/react-art/src/ReactART.js b/packages/react-art/src/ReactART.js index 9b2c5382e1db3..71414f607c029 100644 --- a/packages/react-art/src/ReactART.js +++ b/packages/react-art/src/ReactART.js @@ -5,7 +5,7 @@ * LICENSE file in the root directory of this source tree. */ -import React from 'react'; +import * as React from 'react'; import ReactVersion from 'shared/ReactVersion'; import {LegacyRoot} from 'shared/ReactRootTags'; import { diff --git a/packages/react-art/src/__tests__/ReactART-test.js b/packages/react-art/src/__tests__/ReactART-test.js index 758415ef38044..1613360ce6369 100644 --- a/packages/react-art/src/__tests__/ReactART-test.js +++ b/packages/react-art/src/__tests__/ReactART-test.js @@ -11,7 +11,19 @@ 'use strict'; -const React = require('react'); +import * as React from 'react'; + +import * as ReactART from 'react-art'; +import ARTSVGMode from 'art/modes/svg'; +import ARTCurrentMode from 'art/modes/current'; +// Since these are default exports, we need to import them using ESM. +// Since they must be on top, we need to import this before ReactDOM. +import Circle from 'react-art/Circle'; +import Rectangle from 'react-art/Rectangle'; +import Wedge from 'react-art/Wedge'; + +// Isolate DOM renderer. +jest.resetModules(); const ReactDOM = require('react-dom'); const ReactTestUtils = require('react-dom/test-utils'); @@ -19,15 +31,6 @@ const ReactTestUtils = require('react-dom/test-utils'); jest.resetModules(); const ReactTestRenderer = require('react-test-renderer'); -// Isolate ART renderer. -jest.resetModules(); -const ReactART = require('react-art'); -const ARTSVGMode = require('art/modes/svg'); -const ARTCurrentMode = require('art/modes/current'); -const Circle = require('react-art/Circle'); -const Rectangle = require('react-art/Rectangle'); -const Wedge = require('react-art/Wedge'); - // Isolate the noop renderer jest.resetModules(); const ReactNoop = require('react-noop-renderer'); diff --git a/packages/react-cache/src/ReactCache.js b/packages/react-cache/src/ReactCache.js index ec8d93d7cd85f..7453b1c539200 100644 --- a/packages/react-cache/src/ReactCache.js +++ b/packages/react-cache/src/ReactCache.js @@ -7,7 +7,7 @@ * @flow */ -import React from 'react'; +import * as React from 'react'; import {createLRU} from './LRU'; diff --git a/packages/react-debug-tools/index.js b/packages/react-debug-tools/index.js index c381393770b5f..f10fd94270361 100644 --- a/packages/react-debug-tools/index.js +++ b/packages/react-debug-tools/index.js @@ -5,9 +5,4 @@ * LICENSE file in the root directory of this source tree. */ -'use strict'; - -const ReactDebugTools = require('./src/ReactDebugTools'); - -// This is hacky but makes it work with both Rollup and Jest. -module.exports = ReactDebugTools.default || ReactDebugTools; +export * from './src/ReactDebugTools'; diff --git a/packages/react-debug-tools/package.json b/packages/react-debug-tools/package.json index 1a2e787d12bc4..0d69a5f55f509 100644 --- a/packages/react-debug-tools/package.json +++ b/packages/react-debug-tools/package.json @@ -29,6 +29,7 @@ "react": "^16.0.0" }, "dependencies": { - "error-stack-parser": "^2.0.2" + "error-stack-parser": "^2.0.2", + "object-assign": "^4.1.1" } } diff --git a/packages/react-debug-tools/src/ReactDebugHooks.js b/packages/react-debug-tools/src/ReactDebugHooks.js index 99f7ae89056f7..c2d3ed0a2accf 100644 --- a/packages/react-debug-tools/src/ReactDebugHooks.js +++ b/packages/react-debug-tools/src/ReactDebugHooks.js @@ -25,7 +25,7 @@ import { SimpleMemoComponent, ContextProvider, ForwardRef, - Chunk, + Block, } from 'shared/ReactWorkTags'; type CurrentDispatcherRef = typeof ReactSharedInternals.ReactCurrentDispatcher; @@ -243,7 +243,11 @@ function useResponder( function useTransition( config: SuspenseConfig | null | void, ): [(() => void) => void, boolean] { - nextHook(); + // useTransition() composes multiple hooks internally. + // Advance the current hook index the same number of times + // so that subsequent hooks have the right memoized state. + nextHook(); // State + nextHook(); // Callback hookLog.push({ primitive: 'Transition', stackError: new Error(), @@ -253,7 +257,11 @@ function useTransition( } function useDeferredValue(value: T, config: TimeoutConfig | null | void): T { - nextHook(); + // useDeferredValue() composes multiple hooks internally. + // Advance the current hook index the same number of times + // so that subsequent hooks have the right memoized state. + nextHook(); // State + nextHook(); // Effect hookLog.push({ primitive: 'DeferredValue', stackError: new Error(), @@ -628,7 +636,7 @@ export function inspectHooksOfFiber( fiber.tag !== FunctionComponent && fiber.tag !== SimpleMemoComponent && fiber.tag !== ForwardRef && - fiber.tag !== Chunk + fiber.tag !== Block ) { throw new Error( 'Unknown Fiber. Needs to be a function component to inspect hooks.', diff --git a/packages/react-debug-tools/src/__tests__/ReactHooksInspection-test.internal.js b/packages/react-debug-tools/src/__tests__/ReactHooksInspection-test.internal.js index 3e1c67087c12a..d075b7097de2a 100644 --- a/packages/react-debug-tools/src/__tests__/ReactHooksInspection-test.internal.js +++ b/packages/react-debug-tools/src/__tests__/ReactHooksInspection-test.internal.js @@ -22,6 +22,11 @@ describe('ReactHooksInspection', () => { ReactDebugTools = require('react-debug-tools'); }); + if (!__EXPERIMENTAL__) { + it("empty test so Jest doesn't complain", () => {}); + return; + } + it('should inspect a simple useResponder hook', () => { const TestResponder = React.DEPRECATED_createResponder('TestResponder', {}); diff --git a/packages/react-debug-tools/src/__tests__/ReactHooksInspectionIntegration-test.js b/packages/react-debug-tools/src/__tests__/ReactHooksInspectionIntegration-test.js index a3542d86ceba3..f92191beaf217 100644 --- a/packages/react-debug-tools/src/__tests__/ReactHooksInspectionIntegration-test.js +++ b/packages/react-debug-tools/src/__tests__/ReactHooksInspectionIntegration-test.js @@ -366,6 +366,64 @@ describe('ReactHooksInspectionIntegration', () => { ]); }); + if (__EXPERIMENTAL__) { + it('should support composite useTransition hook', () => { + function Foo(props) { + React.useTransition(); + const memoizedValue = React.useMemo(() => 'hello', []); + return
{memoizedValue}
; + } + let renderer = ReactTestRenderer.create(); + let childFiber = renderer.root.findByType(Foo)._currentFiber(); + let tree = ReactDebugTools.inspectHooksOfFiber(childFiber); + expect(tree).toEqual([ + { + id: 0, + isStateEditable: false, + name: 'Transition', + value: undefined, + subHooks: [], + }, + { + id: 1, + isStateEditable: false, + name: 'Memo', + value: 'hello', + subHooks: [], + }, + ]); + }); + + it('should support composite useDeferredValue hook', () => { + function Foo(props) { + React.useDeferredValue('abc', { + timeoutMs: 500, + }); + const [state] = React.useState(() => 'hello', []); + return
{state}
; + } + let renderer = ReactTestRenderer.create(); + let childFiber = renderer.root.findByType(Foo)._currentFiber(); + let tree = ReactDebugTools.inspectHooksOfFiber(childFiber); + expect(tree).toEqual([ + { + id: 0, + isStateEditable: false, + name: 'DeferredValue', + value: 'abc', + subHooks: [], + }, + { + id: 1, + isStateEditable: true, + name: 'State', + value: 'hello', + subHooks: [], + }, + ]); + }); + } + describe('useDebugValue', () => { it('should support inspectable values for multiple custom hooks', () => { function useLabeledValue(label) { diff --git a/packages/react-devtools-extensions/deploy.edge.html b/packages/react-devtools-extensions/deploy.edge.html new file mode 100644 index 0000000000000..10f51c570e633 --- /dev/null +++ b/packages/react-devtools-extensions/deploy.edge.html @@ -0,0 +1,8 @@ +
    +
  1. download extension
  2. +
  3. Double-click to extract
  4. +
  5. Navigate to edge://extensions/
  6. +
  7. Enable "Developer mode"
  8. +
  9. Click "LOAD UNPACKED"
  10. +
  11. Select extracted extension folder (ReactDevTools)
  12. +
\ No newline at end of file diff --git a/packages/react-devtools-extensions/edge/README.md b/packages/react-devtools-extensions/edge/README.md new file mode 100644 index 0000000000000..af7ff30a20405 --- /dev/null +++ b/packages/react-devtools-extensions/edge/README.md @@ -0,0 +1,12 @@ +# The Microsoft Edge extension + +The source code for this extension has moved to `shells/webextension`. + +Modify the source code there and then rebuild this extension by running `node build` from this directory or `yarn run build:extension:edge` from the root directory. + +## Testing in Microsoft Edge + +You can test a local build of the web extension like so: + + 1. Build the extension: `node build` + 1. Follow the on-screen instructions. diff --git a/packages/react-devtools-extensions/edge/build.js b/packages/react-devtools-extensions/edge/build.js new file mode 100644 index 0000000000000..21fa6e5383c22 --- /dev/null +++ b/packages/react-devtools-extensions/edge/build.js @@ -0,0 +1,46 @@ +#!/usr/bin/env node + +'use strict'; + +const chalk = require('chalk'); +const {execSync} = require('child_process'); +const {join} = require('path'); +const {argv} = require('yargs'); +const build = require('../build'); + +const main = async () => { + const {crx} = argv; + + await build('edge'); + + const cwd = join(__dirname, 'build'); + if (crx) { + const crxPath = join( + __dirname, + '..', + '..', + '..', + 'node_modules', + '.bin', + 'crx' + ); + + execSync(`${crxPath} pack ./unpacked -o ReactDevTools.crx`, { + cwd, + }); + } + + console.log(chalk.green('\nThe Microsoft Edge extension has been built!')); + + console.log(chalk.green('\nTo load this extension:')); + console.log(chalk.yellow('Navigate to edge://extensions/')); + console.log(chalk.yellow('Enable "Developer mode"')); + console.log(chalk.yellow('Click "LOAD UNPACKED"')); + console.log(chalk.yellow('Select extension folder - ' + cwd + '\\unpacked')); + + console.log(chalk.green('\nYou can test this build by running:')); + console.log(chalk.gray('\n# From the react-devtools root directory:')); + console.log('yarn run test:edge\n'); +}; + +main(); diff --git a/packages/react-devtools-extensions/edge/deploy.js b/packages/react-devtools-extensions/edge/deploy.js new file mode 100644 index 0000000000000..33d102c688468 --- /dev/null +++ b/packages/react-devtools-extensions/edge/deploy.js @@ -0,0 +1,9 @@ +#!/usr/bin/env node + +'use strict'; + +const deploy = require('../deploy'); + +const main = async () => await deploy('edge'); + +main(); diff --git a/packages/react-devtools-extensions/edge/manifest.json b/packages/react-devtools-extensions/edge/manifest.json new file mode 100644 index 0000000000000..46d683f81e3eb --- /dev/null +++ b/packages/react-devtools-extensions/edge/manifest.json @@ -0,0 +1,52 @@ +{ + "manifest_version": 2, + "name": "React Developer Tools", + "description": "Adds React debugging tools to the Microsoft Edge Developer Tools.", + "version": "4.4.0", + "version_name": "4.4.0", + + "minimum_chrome_version": "49", + + "icons": { + "16": "icons/16-production.png", + "32": "icons/32-production.png", + "48": "icons/48-production.png", + "128": "icons/128-production.png" + }, + + "browser_action": { + "default_icon": { + "16": "icons/16-disabled.png", + "32": "icons/32-disabled.png", + "48": "icons/48-disabled.png", + "128": "icons/128-disabled.png" + }, + + "default_popup": "popups/disabled.html" + }, + + "devtools_page": "main.html", + + "content_security_policy": "script-src 'self' 'unsafe-eval'; object-src 'self'", + "web_accessible_resources": [ + "main.html", + "panel.html", + "build/react_devtools_backend.js", + "build/renderer.js" + ], + + "background": { + "scripts": ["build/background.js"], + "persistent": false + }, + + "permissions": ["file:///*", "http://*/*", "https://*/*"], + + "content_scripts": [ + { + "matches": [""], + "js": ["build/injectGlobalHook.js"], + "run_at": "document_start" + } + ] +} diff --git a/packages/react-devtools-extensions/edge/now.json b/packages/react-devtools-extensions/edge/now.json new file mode 100644 index 0000000000000..6faa13a7efefd --- /dev/null +++ b/packages/react-devtools-extensions/edge/now.json @@ -0,0 +1,5 @@ +{ + "name": "react-devtools-experimental-edge", + "alias": ["react-devtools-experimental-edge"], + "files": ["index.html", "ReactDevTools.zip"] +} diff --git a/packages/react-devtools-extensions/edge/test.js b/packages/react-devtools-extensions/edge/test.js new file mode 100644 index 0000000000000..b18ec386d9ba0 --- /dev/null +++ b/packages/react-devtools-extensions/edge/test.js @@ -0,0 +1,29 @@ +#!/usr/bin/env node + +'use strict'; + +const open = require('open'); +const os = require('os'); +const osName = require('os-name'); +const START_URL = 'https://facebook.github.io/react/'; +const {resolve} = require('path'); + +const EXTENSION_PATH = resolve('./edge/build/unpacked'); +const extargs = `--load-extension=${EXTENSION_PATH}`; + +const osname = osName(os.platform()); +let appname; + +if (osname && osname.toLocaleLowerCase().startsWith('windows')) { + appname = 'msedge'; +} else if (osname && osname.toLocaleLowerCase().startsWith('mac')) { + appname = 'Microsoft Edge'; +} else if (osname && osname.toLocaleLowerCase().startsWith('linux')) { + //Coming soon +} + +if (appname) { + (async () => { + await open(START_URL, {app: [appname, extargs]}); + })(); +} diff --git a/packages/react-devtools-extensions/package.json b/packages/react-devtools-extensions/package.json index ce42fc7799f87..deb6834dd4453 100644 --- a/packages/react-devtools-extensions/package.json +++ b/packages/react-devtools-extensions/package.json @@ -3,15 +3,19 @@ "version": "0.0.0", "private": true, "scripts": { - "build": "cross-env NODE_ENV=production yarn run build:chrome && yarn run build:firefox", - "build:dev": "cross-env NODE_ENV=development yarn run build:chrome && yarn run build:firefox", + "build": "cross-env NODE_ENV=production yarn run build:chrome && yarn run build:firefox && yarn run build:edge", + "build:dev": "cross-env NODE_ENV=development yarn run build:chrome && yarn run build:firefox && yarn run build:edge", "build:chrome": "cross-env NODE_ENV=production node ./chrome/build", "build:chrome:crx": "cross-env NODE_ENV=production node ./chrome/build --crx", "build:chrome:dev": "cross-env NODE_ENV=development node ./chrome/build", "build:firefox": "cross-env NODE_ENV=production node ./firefox/build", "build:firefox:dev": "cross-env NODE_ENV=development node ./firefox/build", + "build:edge": "cross-env NODE_ENV=production node ./edge/build", + "build:edge:crx": "cross-env NODE_ENV=production node ./edge/build --crx", + "build:edge:dev": "cross-env NODE_ENV=development node ./edge/build", "test:chrome": "node ./chrome/test", - "test:firefox": "node ./firefox/test" + "test:firefox": "node ./firefox/test", + "test:edge": "node ./edge/test" }, "devDependencies": { "@babel/core": "^7.1.6", @@ -32,6 +36,8 @@ "firefox-profile": "^1.0.2", "node-libs-browser": "0.5.3", "nullthrows": "^1.0.0", + "open": "^7.0.2", + "os-name": "^3.1.0", "raw-loader": "^3.1.0", "style-loader": "^0.23.1", "web-ext": "^3.0.0", diff --git a/packages/react-devtools-inline/src/frontend.js b/packages/react-devtools-inline/src/frontend.js index 86f3300d4b1c2..0172c8bbf8cfd 100644 --- a/packages/react-devtools-inline/src/frontend.js +++ b/packages/react-devtools-inline/src/frontend.js @@ -1,6 +1,7 @@ /** @flow */ -import React, {forwardRef} from 'react'; +import * as React from 'react'; +import {forwardRef} from 'react'; import Bridge from 'react-devtools-shared/src/bridge'; import Store from 'react-devtools-shared/src/devtools/store'; import DevTools from 'react-devtools-shared/src/devtools/views/DevTools'; diff --git a/packages/react-devtools-shared/src/devtools/ContextMenu/ContextMenu.js b/packages/react-devtools-shared/src/devtools/ContextMenu/ContextMenu.js index 891d24c0518b5..467a6eaa15ea5 100644 --- a/packages/react-devtools-shared/src/devtools/ContextMenu/ContextMenu.js +++ b/packages/react-devtools-shared/src/devtools/ContextMenu/ContextMenu.js @@ -7,13 +7,8 @@ * @flow */ -import React, { - useContext, - useEffect, - useLayoutEffect, - useRef, - useState, -} from 'react'; +import * as React from 'react'; +import {useContext, useEffect, useLayoutEffect, useRef, useState} from 'react'; import {createPortal} from 'react-dom'; import {RegistryContext} from './Contexts'; diff --git a/packages/react-devtools-shared/src/devtools/ContextMenu/ContextMenuItem.js b/packages/react-devtools-shared/src/devtools/ContextMenu/ContextMenuItem.js index ddfa61171b23d..1b8ebb7cdcb75 100644 --- a/packages/react-devtools-shared/src/devtools/ContextMenu/ContextMenuItem.js +++ b/packages/react-devtools-shared/src/devtools/ContextMenu/ContextMenuItem.js @@ -7,7 +7,8 @@ * @flow */ -import React, {useContext} from 'react'; +import * as React from 'react'; +import {useContext} from 'react'; import {RegistryContext} from './Contexts'; import styles from './ContextMenuItem.css'; diff --git a/packages/react-devtools-shared/src/devtools/ContextMenu/useContextMenu.js b/packages/react-devtools-shared/src/devtools/ContextMenu/useContextMenu.js index 1639befc76bb9..3829a4377ee34 100644 --- a/packages/react-devtools-shared/src/devtools/ContextMenu/useContextMenu.js +++ b/packages/react-devtools-shared/src/devtools/ContextMenu/useContextMenu.js @@ -19,18 +19,22 @@ export default function useContextMenu({ }: {| data: Object, id: string, - ref: ElementRef, + ref: {current: ElementRef<'div'> | null}, |}) { const {showMenu} = useContext(RegistryContext); useEffect(() => { if (ref.current !== null) { - const handleContextMenu = event => { + const handleContextMenu = (event: MouseEvent | TouchEvent) => { event.preventDefault(); event.stopPropagation(); - const pageX = event.pageX || (event.touches && event.touches[0].pageX); - const pageY = event.pageY || (event.touches && event.touches[0].pageY); + const pageX = + event.pageX || + (event.touches && ((event: any): TouchEvent).touches[0].pageX); + const pageY = + event.pageY || + (event.touches && ((event: any): TouchEvent).touches[0].pageY); showMenu({data, id, pageX, pageY}); }; diff --git a/packages/react-devtools-shared/src/devtools/cache.js b/packages/react-devtools-shared/src/devtools/cache.js index e389a594e896e..7b0a0766a422f 100644 --- a/packages/react-devtools-shared/src/devtools/cache.js +++ b/packages/react-devtools-shared/src/devtools/cache.js @@ -7,7 +7,8 @@ * @flow */ -import React, {createContext} from 'react'; +import * as React from 'react'; +import {createContext} from 'react'; // Cache implementation was forked from the React repo: // https://github.com/facebook/react/blob/master/packages/react-cache/src/ReactCache.js diff --git a/packages/react-devtools-shared/src/devtools/views/Button.js b/packages/react-devtools-shared/src/devtools/views/Button.js index 76028d53dde90..c6d2525c0b784 100644 --- a/packages/react-devtools-shared/src/devtools/views/Button.js +++ b/packages/react-devtools-shared/src/devtools/views/Button.js @@ -7,7 +7,7 @@ * @flow */ -import React from 'react'; +import * as React from 'react'; import Tooltip from '@reach/tooltip'; import styles from './Button.css'; diff --git a/packages/react-devtools-shared/src/devtools/views/ButtonIcon.js b/packages/react-devtools-shared/src/devtools/views/ButtonIcon.js index 5edc6821bd3fe..0c7fe00b42aac 100644 --- a/packages/react-devtools-shared/src/devtools/views/ButtonIcon.js +++ b/packages/react-devtools-shared/src/devtools/views/ButtonIcon.js @@ -7,7 +7,7 @@ * @flow */ -import React from 'react'; +import * as React from 'react'; import styles from './ButtonIcon.css'; export type IconType = diff --git a/packages/react-devtools-shared/src/devtools/views/Components/Badge.js b/packages/react-devtools-shared/src/devtools/views/Components/Badge.js index 660e4ab4002be..8362055cb3966 100644 --- a/packages/react-devtools-shared/src/devtools/views/Components/Badge.js +++ b/packages/react-devtools-shared/src/devtools/views/Components/Badge.js @@ -7,7 +7,8 @@ * @flow */ -import React, {Fragment} from 'react'; +import * as React from 'react'; +import {Fragment} from 'react'; import { ElementTypeMemo, ElementTypeForwardRef, diff --git a/packages/react-devtools-shared/src/devtools/views/Components/Components.css b/packages/react-devtools-shared/src/devtools/views/Components/Components.css index f37b3eaa73d3f..7eaa396ccd40b 100644 --- a/packages/react-devtools-shared/src/devtools/views/Components/Components.css +++ b/packages/react-devtools-shared/src/devtools/views/Components/Components.css @@ -10,27 +10,48 @@ } .TreeWrapper { - flex: 0 0 65%; + flex: 0 0 var(--horizontal-resize-percentage); overflow: auto; } .SelectedElementWrapper { - flex: 0 0 35%; + flex: 1 1 35%; overflow-x: hidden; overflow-y: auto; } +.ResizeBarWrapper { + flex: 0 0 0px; + position: relative; +} + +.ResizeBar { + position: absolute; + left: -2px; + width: 5px; + height: 100%; + cursor: ew-resize; +} + @media screen and (max-width: 600px) { .Components { flex-direction: column; } .TreeWrapper { - flex: 0 0 50%; + flex: 0 0 var(--vertical-resize-percentage); } .SelectedElementWrapper { - flex: 0 0 50%; + flex: 1 1 50%; + } + + .ResizeBar { + top: -2px; + left: 0; + width: 100%; + height: 5px; + cursor: ns-resize; } } diff --git a/packages/react-devtools-shared/src/devtools/views/Components/Components.js b/packages/react-devtools-shared/src/devtools/views/Components/Components.js index 107ef1393ef31..9e666548b6dd3 100644 --- a/packages/react-devtools-shared/src/devtools/views/Components/Components.js +++ b/packages/react-devtools-shared/src/devtools/views/Components/Components.js @@ -7,38 +7,155 @@ * @flow */ -import React, {Suspense} from 'react'; +import * as React from 'react'; +import { + Fragment, + Suspense, + useEffect, + useLayoutEffect, + useReducer, + useRef, +} from 'react'; import Tree from './Tree'; -import SelectedElement from './SelectedElement'; import {InspectedElementContextController} from './InspectedElementContext'; -import {NativeStyleContextController} from './NativeStyleEditor/context'; import {OwnersListContextController} from './OwnersListContext'; import portaledContent from '../portaledContent'; +import {SettingsModalContextController} from 'react-devtools-shared/src/devtools/views/Settings/SettingsModalContext'; +import { + localStorageGetItem, + localStorageSetItem, +} from 'react-devtools-shared/src/storage'; +import SelectedElement from './SelectedElement'; import {ModalDialog} from '../ModalDialog'; import SettingsModal from 'react-devtools-shared/src/devtools/views/Settings/SettingsModal'; -import {SettingsModalContextController} from 'react-devtools-shared/src/devtools/views/Settings/SettingsModalContext'; +import {NativeStyleContextController} from './NativeStyleEditor/context'; import styles from './Components.css'; function Components(_: {||}) { - // TODO Flex wrappers below should be user resizable. + const wrapperElementRef = useRef(null); + const resizeElementRef = useRef(null); + + const [state, dispatch] = useReducer( + resizeReducer, + null, + initResizeState, + ); + + const {horizontalPercentage, verticalPercentage} = state; + + useLayoutEffect(() => { + const resizeElement = resizeElementRef.current; + + setResizeCSSVariable( + resizeElement, + 'horizontal', + horizontalPercentage * 100, + ); + setResizeCSSVariable(resizeElement, 'vertical', verticalPercentage * 100); + }, []); + + useEffect(() => { + const timeoutID = setTimeout(() => { + localStorageSetItem( + LOCAL_STORAGE_KEY, + JSON.stringify({ + horizontalPercentage, + verticalPercentage, + }), + ); + }, 500); + + return () => clearTimeout(timeoutID); + }, [horizontalPercentage, verticalPercentage]); + + const {isResizing} = state; + + const onResizeStart = () => + dispatch({type: 'ACTION_SET_IS_RESIZING', payload: true}); + + let onResize; + let onResizeEnd; + if (isResizing) { + onResizeEnd = () => + dispatch({type: 'ACTION_SET_IS_RESIZING', payload: false}); + + onResize = event => { + const resizeElement = resizeElementRef.current; + const wrapperElement = wrapperElementRef.current; + + if (!isResizing || wrapperElement === null || resizeElement === null) { + return; + } + + event.preventDefault(); + + const orientation = getOrientation(wrapperElement); + + const {height, width, left, top} = wrapperElement.getBoundingClientRect(); + + const currentMousePosition = + orientation === 'horizontal' + ? event.clientX - left + : event.clientY - top; + + const boundaryMin = MINIMUM_SIZE; + const boundaryMax = + orientation === 'horizontal' + ? width - MINIMUM_SIZE + : height - MINIMUM_SIZE; + + const isMousePositionInBounds = + currentMousePosition > boundaryMin && + currentMousePosition < boundaryMax; + + if (isMousePositionInBounds) { + const resizedElementDimension = + orientation === 'horizontal' ? width : height; + const actionType = + orientation === 'horizontal' + ? 'ACTION_SET_HORIZONTAL_PERCENTAGE' + : 'ACTION_SET_VERTICAL_PERCENTAGE'; + const percentage = + (currentMousePosition / resizedElementDimension) * 100; + + setResizeCSSVariable(resizeElement, orientation, percentage); + + dispatch({ + type: actionType, + payload: currentMousePosition / resizedElementDimension, + }); + } + }; + } + return ( -
-
- -
-
- - }> - - - -
- - +
+ +
+ +
+
+
+
+
+ + }> + + + +
+ + +
@@ -50,4 +167,92 @@ function Loading() { return
Loading...
; } +const LOCAL_STORAGE_KEY = 'React::DevTools::createResizeReducer'; +const VERTICAL_MODE_MAX_WIDTH = 600; +const MINIMUM_SIZE = 50; + +type Orientation = 'horizontal' | 'vertical'; + +type ResizeActionType = + | 'ACTION_SET_DID_MOUNT' + | 'ACTION_SET_IS_RESIZING' + | 'ACTION_SET_HORIZONTAL_PERCENTAGE' + | 'ACTION_SET_VERTICAL_PERCENTAGE'; + +type ResizeAction = {| + type: ResizeActionType, + payload: any, +|}; + +type ResizeState = {| + horizontalPercentage: number, + isResizing: boolean, + verticalPercentage: number, +|}; + +function initResizeState(): ResizeState { + let horizontalPercentage = 0.65; + let verticalPercentage = 0.5; + + try { + let data = localStorageGetItem(LOCAL_STORAGE_KEY); + if (data != null) { + data = JSON.parse(data); + horizontalPercentage = data.horizontalPercentage; + verticalPercentage = data.verticalPercentage; + } + } catch (error) {} + + return { + horizontalPercentage, + isResizing: false, + verticalPercentage, + }; +} + +function resizeReducer(state: ResizeState, action: ResizeAction): ResizeState { + switch (action.type) { + case 'ACTION_SET_IS_RESIZING': + return { + ...state, + isResizing: action.payload, + }; + case 'ACTION_SET_HORIZONTAL_PERCENTAGE': + return { + ...state, + horizontalPercentage: action.payload, + }; + case 'ACTION_SET_VERTICAL_PERCENTAGE': + return { + ...state, + verticalPercentage: action.payload, + }; + default: + return state; + } +} + +function getOrientation( + wrapperElement: null | HTMLElement, +): null | Orientation { + if (wrapperElement != null) { + const {width} = wrapperElement.getBoundingClientRect(); + return width > VERTICAL_MODE_MAX_WIDTH ? 'horizontal' : 'vertical'; + } + return null; +} + +function setResizeCSSVariable( + resizeElement: null | HTMLElement, + orientation: null | Orientation, + percentage: number, +): void { + if (resizeElement !== null && orientation !== null) { + resizeElement.style.setProperty( + `--${orientation}-resize-percentage`, + `${percentage}%`, + ); + } +} + export default portaledContent(Components); diff --git a/packages/react-devtools-shared/src/devtools/views/Components/EditableName.js b/packages/react-devtools-shared/src/devtools/views/Components/EditableName.js index 73b85cddf8c42..e73f725407839 100644 --- a/packages/react-devtools-shared/src/devtools/views/Components/EditableName.js +++ b/packages/react-devtools-shared/src/devtools/views/Components/EditableName.js @@ -7,7 +7,8 @@ * @flow */ -import React, {useCallback, useState} from 'react'; +import * as React from 'react'; +import {useCallback, useState} from 'react'; import AutoSizeInput from './NativeStyleEditor/AutoSizeInput'; import styles from './EditableName.css'; diff --git a/packages/react-devtools-shared/src/devtools/views/Components/EditableValue.js b/packages/react-devtools-shared/src/devtools/views/Components/EditableValue.js index e501c39adee04..4660eba7352f0 100644 --- a/packages/react-devtools-shared/src/devtools/views/Components/EditableValue.js +++ b/packages/react-devtools-shared/src/devtools/views/Components/EditableValue.js @@ -7,7 +7,8 @@ * @flow */ -import React, {Fragment, useRef} from 'react'; +import * as React from 'react'; +import {Fragment, useRef} from 'react'; import styles from './EditableValue.css'; import {useEditableValue} from '../hooks'; diff --git a/packages/react-devtools-shared/src/devtools/views/Components/Element.js b/packages/react-devtools-shared/src/devtools/views/Components/Element.js index 4143bd01ce7f6..1fa40035efdd7 100644 --- a/packages/react-devtools-shared/src/devtools/views/Components/Element.js +++ b/packages/react-devtools-shared/src/devtools/views/Components/Element.js @@ -7,7 +7,8 @@ * @flow */ -import React, {Fragment, useContext, useMemo, useState} from 'react'; +import * as React from 'react'; +import {Fragment, useContext, useMemo, useState} from 'react'; import Store from 'react-devtools-shared/src/devtools/store'; import Badge from './Badge'; import ButtonIcon from '../ButtonIcon'; diff --git a/packages/react-devtools-shared/src/devtools/views/Components/ExpandCollapseToggle.js b/packages/react-devtools-shared/src/devtools/views/Components/ExpandCollapseToggle.js index 234930c900b12..8950696b7e4fc 100644 --- a/packages/react-devtools-shared/src/devtools/views/Components/ExpandCollapseToggle.js +++ b/packages/react-devtools-shared/src/devtools/views/Components/ExpandCollapseToggle.js @@ -7,7 +7,7 @@ * @flow */ -import React from 'react'; +import * as React from 'react'; import Button from '../Button'; import ButtonIcon from '../ButtonIcon'; diff --git a/packages/react-devtools-shared/src/devtools/views/Components/HocBadges.js b/packages/react-devtools-shared/src/devtools/views/Components/HocBadges.js index 6cf25ef234a8a..4cf88d596d7bb 100644 --- a/packages/react-devtools-shared/src/devtools/views/Components/HocBadges.js +++ b/packages/react-devtools-shared/src/devtools/views/Components/HocBadges.js @@ -7,7 +7,7 @@ * @flow */ -import React from 'react'; +import * as React from 'react'; import { ElementTypeForwardRef, ElementTypeMemo, diff --git a/packages/react-devtools-shared/src/devtools/views/Components/HooksTree.js b/packages/react-devtools-shared/src/devtools/views/Components/HooksTree.js index f479d477f4255..1c710c5e7c859 100644 --- a/packages/react-devtools-shared/src/devtools/views/Components/HooksTree.js +++ b/packages/react-devtools-shared/src/devtools/views/Components/HooksTree.js @@ -8,7 +8,8 @@ */ import {copy} from 'clipboard-js'; -import React, {useCallback, useContext, useRef, useState} from 'react'; +import * as React from 'react'; +import {useCallback, useContext, useRef, useState} from 'react'; import {BridgeContext, StoreContext} from '../context'; import Button from '../Button'; import ButtonIcon from '../ButtonIcon'; diff --git a/packages/react-devtools-shared/src/devtools/views/Components/InspectHostNodesToggle.js b/packages/react-devtools-shared/src/devtools/views/Components/InspectHostNodesToggle.js index 7740542881623..29782b2b303dd 100644 --- a/packages/react-devtools-shared/src/devtools/views/Components/InspectHostNodesToggle.js +++ b/packages/react-devtools-shared/src/devtools/views/Components/InspectHostNodesToggle.js @@ -7,7 +7,8 @@ * @flow */ -import React, {useCallback, useContext, useEffect, useState} from 'react'; +import * as React from 'react'; +import {useCallback, useContext, useEffect, useState} from 'react'; import {BridgeContext} from '../context'; import Toggle from '../Toggle'; import ButtonIcon from '../ButtonIcon'; diff --git a/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementContext.js b/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementContext.js index 0467ffd1c6e8d..5d3ae1f74f201 100644 --- a/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementContext.js +++ b/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementContext.js @@ -7,7 +7,8 @@ * @flow */ -import React, { +import * as React from 'react'; +import { createContext, useCallback, useContext, diff --git a/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementTree.js b/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementTree.js index ca6616fdbe3ff..6e480a80ed19d 100644 --- a/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementTree.js +++ b/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementTree.js @@ -8,7 +8,8 @@ */ import {copy} from 'clipboard-js'; -import React, {useCallback, useState} from 'react'; +import * as React from 'react'; +import {useCallback, useState} from 'react'; import Button from '../Button'; import ButtonIcon from '../ButtonIcon'; import KeyValue from './KeyValue'; diff --git a/packages/react-devtools-shared/src/devtools/views/Components/KeyValue.js b/packages/react-devtools-shared/src/devtools/views/Components/KeyValue.js index e45a5488c4a7a..54726c7e04223 100644 --- a/packages/react-devtools-shared/src/devtools/views/Components/KeyValue.js +++ b/packages/react-devtools-shared/src/devtools/views/Components/KeyValue.js @@ -7,7 +7,8 @@ * @flow */ -import React, {useEffect, useRef, useState} from 'react'; +import * as React from 'react'; +import {useEffect, useRef, useState} from 'react'; import EditableValue from './EditableValue'; import ExpandCollapseToggle from './ExpandCollapseToggle'; import {alphaSortEntries, getMetaValueLabel} from '../utils'; diff --git a/packages/react-devtools-shared/src/devtools/views/Components/NativeStyleEditor/AutoSizeInput.js b/packages/react-devtools-shared/src/devtools/views/Components/NativeStyleEditor/AutoSizeInput.js index 389013ee7d5a6..2f7113193d2a2 100644 --- a/packages/react-devtools-shared/src/devtools/views/Components/NativeStyleEditor/AutoSizeInput.js +++ b/packages/react-devtools-shared/src/devtools/views/Components/NativeStyleEditor/AutoSizeInput.js @@ -7,7 +7,8 @@ * @flow */ -import React, {Fragment, useLayoutEffect, useRef} from 'react'; +import * as React from 'react'; +import {Fragment, useLayoutEffect, useRef} from 'react'; import styles from './AutoSizeInput.css'; type Props = { diff --git a/packages/react-devtools-shared/src/devtools/views/Components/NativeStyleEditor/LayoutViewer.js b/packages/react-devtools-shared/src/devtools/views/Components/NativeStyleEditor/LayoutViewer.js index 22287647c8b16..da5530d4a39bd 100644 --- a/packages/react-devtools-shared/src/devtools/views/Components/NativeStyleEditor/LayoutViewer.js +++ b/packages/react-devtools-shared/src/devtools/views/Components/NativeStyleEditor/LayoutViewer.js @@ -7,7 +7,7 @@ * @flow */ -import React from 'react'; +import * as React from 'react'; import styles from './LayoutViewer.css'; import type {Layout} from './types'; diff --git a/packages/react-devtools-shared/src/devtools/views/Components/NativeStyleEditor/StyleEditor.js b/packages/react-devtools-shared/src/devtools/views/Components/NativeStyleEditor/StyleEditor.js index 8e0210d44b49c..31f55da245690 100644 --- a/packages/react-devtools-shared/src/devtools/views/Components/NativeStyleEditor/StyleEditor.js +++ b/packages/react-devtools-shared/src/devtools/views/Components/NativeStyleEditor/StyleEditor.js @@ -7,7 +7,8 @@ * @flow */ -import React, {useContext, useMemo, useRef, useState} from 'react'; +import * as React from 'react'; +import {useContext, useMemo, useRef, useState} from 'react'; import {unstable_batchedUpdates as batchedUpdates} from 'react-dom'; import {copy} from 'clipboard-js'; import { diff --git a/packages/react-devtools-shared/src/devtools/views/Components/NativeStyleEditor/context.js b/packages/react-devtools-shared/src/devtools/views/Components/NativeStyleEditor/context.js index 2542957f882fb..52e22fe23c974 100644 --- a/packages/react-devtools-shared/src/devtools/views/Components/NativeStyleEditor/context.js +++ b/packages/react-devtools-shared/src/devtools/views/Components/NativeStyleEditor/context.js @@ -7,7 +7,8 @@ * @flow */ -import React, { +import * as React from 'react'; +import { createContext, useCallback, useContext, diff --git a/packages/react-devtools-shared/src/devtools/views/Components/NativeStyleEditor/index.js b/packages/react-devtools-shared/src/devtools/views/Components/NativeStyleEditor/index.js index d9032e2f61ac4..521a0642cac90 100644 --- a/packages/react-devtools-shared/src/devtools/views/Components/NativeStyleEditor/index.js +++ b/packages/react-devtools-shared/src/devtools/views/Components/NativeStyleEditor/index.js @@ -7,7 +7,8 @@ * @flow */ -import React, {Fragment, useContext, useMemo} from 'react'; +import * as React from 'react'; +import {Fragment, useContext, useMemo} from 'react'; import {StoreContext} from 'react-devtools-shared/src/devtools/views/context'; import {useSubscription} from 'react-devtools-shared/src/devtools/views/hooks'; import {NativeStyleContext} from './context'; diff --git a/packages/react-devtools-shared/src/devtools/views/Components/OwnersListContext.js b/packages/react-devtools-shared/src/devtools/views/Components/OwnersListContext.js index aae74b4f84b50..493ee8d966122 100644 --- a/packages/react-devtools-shared/src/devtools/views/Components/OwnersListContext.js +++ b/packages/react-devtools-shared/src/devtools/views/Components/OwnersListContext.js @@ -7,7 +7,8 @@ * @flow */ -import React, {createContext, useCallback, useContext, useEffect} from 'react'; +import * as React from 'react'; +import {createContext, useCallback, useContext, useEffect} from 'react'; import {createResource} from '../../cache'; import {BridgeContext, StoreContext} from '../context'; import {TreeStateContext} from './TreeContext'; diff --git a/packages/react-devtools-shared/src/devtools/views/Components/OwnersStack.js b/packages/react-devtools-shared/src/devtools/views/Components/OwnersStack.js index a42ff9ada0d37..6be2a699991e8 100644 --- a/packages/react-devtools-shared/src/devtools/views/Components/OwnersStack.js +++ b/packages/react-devtools-shared/src/devtools/views/Components/OwnersStack.js @@ -6,7 +6,8 @@ * * @flow */ -import React, { +import * as React from 'react'; +import { Fragment, useCallback, useContext, diff --git a/packages/react-devtools-shared/src/devtools/views/Components/SearchInput.js b/packages/react-devtools-shared/src/devtools/views/Components/SearchInput.js index ded2f2ef06f3a..0c8c56530f749 100644 --- a/packages/react-devtools-shared/src/devtools/views/Components/SearchInput.js +++ b/packages/react-devtools-shared/src/devtools/views/Components/SearchInput.js @@ -7,7 +7,8 @@ * @flow */ -import React, {useCallback, useContext, useEffect, useRef} from 'react'; +import * as React from 'react'; +import {useCallback, useContext, useEffect, useRef} from 'react'; import {TreeDispatcherContext, TreeStateContext} from './TreeContext'; import Button from '../Button'; import ButtonIcon from '../ButtonIcon'; diff --git a/packages/react-devtools-shared/src/devtools/views/Components/SelectedElement.js b/packages/react-devtools-shared/src/devtools/views/Components/SelectedElement.js index 432f5a5436126..563cd6f6374fe 100644 --- a/packages/react-devtools-shared/src/devtools/views/Components/SelectedElement.js +++ b/packages/react-devtools-shared/src/devtools/views/Components/SelectedElement.js @@ -8,7 +8,8 @@ */ import {copy} from 'clipboard-js'; -import React, {Fragment, useCallback, useContext} from 'react'; +import * as React from 'react'; +import {Fragment, useCallback, useContext} from 'react'; import {TreeDispatcherContext, TreeStateContext} from './TreeContext'; import {BridgeContext, ContextMenuContext, StoreContext} from '../context'; import ContextMenu from '../../ContextMenu/ContextMenu'; diff --git a/packages/react-devtools-shared/src/devtools/views/Components/SelectedTreeHighlight.js b/packages/react-devtools-shared/src/devtools/views/Components/SelectedTreeHighlight.js index ea94dbff78c0f..1d86d59a3762e 100644 --- a/packages/react-devtools-shared/src/devtools/views/Components/SelectedTreeHighlight.js +++ b/packages/react-devtools-shared/src/devtools/views/Components/SelectedTreeHighlight.js @@ -7,7 +7,8 @@ * @flow */ -import React, {useContext, useMemo} from 'react'; +import * as React from 'react'; +import {useContext, useMemo} from 'react'; import {TreeStateContext} from './TreeContext'; import {SettingsContext} from '../Settings/SettingsContext'; import TreeFocusedContext from './TreeFocusedContext'; diff --git a/packages/react-devtools-shared/src/devtools/views/Components/Tree.js b/packages/react-devtools-shared/src/devtools/views/Components/Tree.js index 3b6357ae8535e..07c779d758b26 100644 --- a/packages/react-devtools-shared/src/devtools/views/Components/Tree.js +++ b/packages/react-devtools-shared/src/devtools/views/Components/Tree.js @@ -7,7 +7,8 @@ * @flow */ -import React, { +import * as React from 'react'; +import { Fragment, Suspense, useCallback, diff --git a/packages/react-devtools-shared/src/devtools/views/Components/TreeContext.js b/packages/react-devtools-shared/src/devtools/views/Components/TreeContext.js index bdd1fc04cfe54..8d23660dbcdb6 100644 --- a/packages/react-devtools-shared/src/devtools/views/Components/TreeContext.js +++ b/packages/react-devtools-shared/src/devtools/views/Components/TreeContext.js @@ -24,7 +24,8 @@ // For this reason, changes to the tree context are processed in sequence: tree -> search -> owners // This enables each section to potentially override (or mask) previous values. -import React, { +import * as React from 'react'; +import { createContext, useCallback, useContext, diff --git a/packages/react-devtools-shared/src/devtools/views/DevTools.js b/packages/react-devtools-shared/src/devtools/views/DevTools.js index 3d19c9390e682..4eac118696637 100644 --- a/packages/react-devtools-shared/src/devtools/views/DevTools.js +++ b/packages/react-devtools-shared/src/devtools/views/DevTools.js @@ -12,7 +12,8 @@ import '@reach/menu-button/styles.css'; import '@reach/tooltip/styles.css'; -import React, {useEffect, useMemo, useState} from 'react'; +import * as React from 'react'; +import {useEffect, useMemo, useState} from 'react'; import Store from '../store'; import {BridgeContext, ContextMenuContext, StoreContext} from './context'; import Components from './Components/Components'; diff --git a/packages/react-devtools-shared/src/devtools/views/ErrorBoundary.js b/packages/react-devtools-shared/src/devtools/views/ErrorBoundary.js index 2c8cd35b31026..6d5724d7a059d 100644 --- a/packages/react-devtools-shared/src/devtools/views/ErrorBoundary.js +++ b/packages/react-devtools-shared/src/devtools/views/ErrorBoundary.js @@ -7,7 +7,8 @@ * @flow */ -import React, {Component} from 'react'; +import * as React from 'react'; +import {Component} from 'react'; import styles from './ErrorBoundary.css'; type Props = {| diff --git a/packages/react-devtools-shared/src/devtools/views/Icon.js b/packages/react-devtools-shared/src/devtools/views/Icon.js index ca1f24075a209..676db1c9e2cdf 100644 --- a/packages/react-devtools-shared/src/devtools/views/Icon.js +++ b/packages/react-devtools-shared/src/devtools/views/Icon.js @@ -7,7 +7,7 @@ * @flow */ -import React from 'react'; +import * as React from 'react'; import styles from './Icon.css'; export type IconType = diff --git a/packages/react-devtools-shared/src/devtools/views/ModalDialog.js b/packages/react-devtools-shared/src/devtools/views/ModalDialog.js index 34aa4b85257ec..014a317049d01 100644 --- a/packages/react-devtools-shared/src/devtools/views/ModalDialog.js +++ b/packages/react-devtools-shared/src/devtools/views/ModalDialog.js @@ -7,7 +7,8 @@ * @flow */ -import React, { +import * as React from 'react'; +import { createContext, useCallback, useContext, diff --git a/packages/react-devtools-shared/src/devtools/views/Profiler/ChartNode.js b/packages/react-devtools-shared/src/devtools/views/Profiler/ChartNode.js index 4a85d18fe8478..82c58e86da460 100644 --- a/packages/react-devtools-shared/src/devtools/views/Profiler/ChartNode.js +++ b/packages/react-devtools-shared/src/devtools/views/Profiler/ChartNode.js @@ -7,7 +7,7 @@ * @flow */ -import React from 'react'; +import * as React from 'react'; import styles from './ChartNode.css'; @@ -18,6 +18,8 @@ type Props = {| label: string, onClick: (event: SyntheticMouseEvent<*>) => mixed, onDoubleClick?: (event: SyntheticMouseEvent<*>) => mixed, + onMouseEnter: (event: SyntheticMouseEvent<*>) => mixed, + onMouseLeave: (event: SyntheticMouseEvent<*>) => mixed, placeLabelAboveNode?: boolean, textStyle?: Object, width: number, @@ -33,6 +35,8 @@ export default function ChartNode({ isDimmed = false, label, onClick, + onMouseEnter, + onMouseLeave, onDoubleClick, textStyle, width, @@ -41,12 +45,13 @@ export default function ChartNode({ }: Props) { return ( - {label} void, scaleX: (value: number, fallbackValue: number) => number, selectedChartNode: ChartNode | null, selectedChartNodeIndex: number, @@ -91,6 +96,7 @@ type Props = {| |}; function CommitFlamegraph({chartData, commitTree, height, width}: Props) { + const [hoveredFiberData, hoverFiber] = useState(null); const {lineHeight} = useContext(SettingsContext); const {selectFiber, selectedFiberID} = useContext(ProfilerContext); @@ -118,6 +124,7 @@ function CommitFlamegraph({chartData, commitTree, height, width}: Props) { const itemData = useMemo( () => ({ chartData, + hoverFiber, scaleX: scale( 0, selectedChartNode !== null @@ -131,19 +138,37 @@ function CommitFlamegraph({chartData, commitTree, height, width}: Props) { selectFiber, width, }), - [chartData, selectedChartNode, selectedChartNodeIndex, selectFiber, width], + [ + chartData, + hoverFiber, + selectedChartNode, + selectedChartNodeIndex, + selectFiber, + width, + ], + ); + + // Tooltip used to show summary of fiber info on hover + const tooltipLabel = useMemo( + () => + hoveredFiberData !== null ? ( + + ) : null, + [hoveredFiberData], ); return ( - - {CommitFlamegraphListItem} - + + + {CommitFlamegraphListItem} + + ); } diff --git a/packages/react-devtools-shared/src/devtools/views/Profiler/CommitFlamegraphListItem.js b/packages/react-devtools-shared/src/devtools/views/Profiler/CommitFlamegraphListItem.js index 66d2db36962c4..c998998cb4f45 100644 --- a/packages/react-devtools-shared/src/devtools/views/Profiler/CommitFlamegraphListItem.js +++ b/packages/react-devtools-shared/src/devtools/views/Profiler/CommitFlamegraphListItem.js @@ -7,13 +7,15 @@ * @flow */ -import React, {Fragment, memo, useCallback, useContext} from 'react'; +import * as React from 'react'; +import {Fragment, memo, useCallback, useContext} from 'react'; import {areEqual} from 'react-window'; import {barWidthThreshold} from './constants'; import {getGradientColor} from './utils'; import ChartNode from './ChartNode'; import {SettingsContext} from '../Settings/SettingsContext'; +import type {ChartNode as ChartNodeType} from './FlamegraphChartBuilder'; import type {ItemData} from './CommitFlamegraph'; type Props = { @@ -26,6 +28,7 @@ type Props = { function CommitFlamegraphListItem({data, index, style}: Props) { const { chartData, + hoverFiber, scaleX, selectedChartNode, selectedChartNodeIndex, @@ -35,6 +38,7 @@ function CommitFlamegraphListItem({data, index, style}: Props) { const {renderPathNodes, maxSelfDuration, rows} = chartData; const {lineHeight} = useContext(SettingsContext); + const handleClick = useCallback( (event: SyntheticMouseEvent<*>, id: number, name: string) => { event.stopPropagation(); @@ -43,6 +47,15 @@ function CommitFlamegraphListItem({data, index, style}: Props) { [selectFiber], ); + const handleMouseEnter = (nodeData: ChartNodeType) => { + const {id, name} = nodeData; + hoverFiber({id, name}); + }; + + const handleMouseLeave = () => { + hoverFiber(null); + }; + // List items are absolutely positioned using the CSS "top" attribute. // The "left" value will always be 0. // Since height is fixed, and width is based on the node's duration, @@ -104,6 +117,8 @@ function CommitFlamegraphListItem({data, index, style}: Props) { key={id} label={label} onClick={event => handleClick(event, id, name)} + onMouseEnter={() => handleMouseEnter(chartNode)} + onMouseLeave={handleMouseLeave} textStyle={{color: textColor}} width={nodeWidth} x={nodeOffset - selectedNodeOffset} diff --git a/packages/react-devtools-shared/src/devtools/views/Profiler/CommitRanked.js b/packages/react-devtools-shared/src/devtools/views/Profiler/CommitRanked.js index f230f67359920..e20692fd4df94 100644 --- a/packages/react-devtools-shared/src/devtools/views/Profiler/CommitRanked.js +++ b/packages/react-devtools-shared/src/devtools/views/Profiler/CommitRanked.js @@ -7,23 +7,28 @@ * @flow */ -import React, {useCallback, useContext, useMemo} from 'react'; +import * as React from 'react'; +import {useCallback, useContext, useMemo, useState} from 'react'; import AutoSizer from 'react-virtualized-auto-sizer'; import {FixedSizeList} from 'react-window'; import {ProfilerContext} from './ProfilerContext'; import NoCommitData from './NoCommitData'; import CommitRankedListItem from './CommitRankedListItem'; +import HoveredFiberInfo from './HoveredFiberInfo'; import {scale} from './utils'; import {StoreContext} from '../context'; import {SettingsContext} from '../Settings/SettingsContext'; +import Tooltip from './Tooltip'; import styles from './CommitRanked.css'; +import type {TooltipFiberData} from './HoveredFiberInfo'; import type {ChartData} from './RankedChartBuilder'; import type {CommitTree} from './types'; export type ItemData = {| chartData: ChartData, + hoverFiber: (fiberData: TooltipFiberData | null) => void, scaleX: (value: number, fallbackValue: number) => number, selectedFiberID: number | null, selectedFiberIndex: number, @@ -89,6 +94,7 @@ type Props = {| |}; function CommitRanked({chartData, commitTree, height, width}: Props) { + const [hoveredFiberData, hoverFiber] = useState(null); const {lineHeight} = useContext(SettingsContext); const {selectedFiberID, selectFiber} = useContext(ProfilerContext); @@ -100,6 +106,7 @@ function CommitRanked({chartData, commitTree, height, width}: Props) { const itemData = useMemo( () => ({ chartData, + hoverFiber, scaleX: scale(0, chartData.nodes[selectedFiberIndex].value, 0, width), selectedFiberID, selectedFiberIndex, @@ -109,16 +116,28 @@ function CommitRanked({chartData, commitTree, height, width}: Props) { [chartData, selectedFiberID, selectedFiberIndex, selectFiber, width], ); + // Tooltip used to show summary of fiber info on hover + const tooltipLabel = useMemo( + () => + hoveredFiberData !== null ? ( + + ) : null, + [hoveredFiberData], + ); + return ( - - {CommitRankedListItem} - + + + {CommitRankedListItem} + + > + ); } diff --git a/packages/react-devtools-shared/src/devtools/views/Profiler/CommitRankedListItem.js b/packages/react-devtools-shared/src/devtools/views/Profiler/CommitRankedListItem.js index 8c46e7de2730b..314c928cc50e9 100644 --- a/packages/react-devtools-shared/src/devtools/views/Profiler/CommitRankedListItem.js +++ b/packages/react-devtools-shared/src/devtools/views/Profiler/CommitRankedListItem.js @@ -7,7 +7,8 @@ * @flow */ -import React, {memo, useCallback, useContext} from 'react'; +import * as React from 'react'; +import {memo, useCallback, useContext} from 'react'; import {areEqual} from 'react-window'; import {minBarWidth} from './constants'; import {getGradientColor} from './utils'; @@ -24,7 +25,14 @@ type Props = { }; function CommitRankedListItem({data, index, style}: Props) { - const {chartData, scaleX, selectedFiberIndex, selectFiber, width} = data; + const { + chartData, + hoverFiber, + scaleX, + selectedFiberIndex, + selectFiber, + width, + } = data; const node = chartData.nodes[index]; @@ -33,11 +41,21 @@ function CommitRankedListItem({data, index, style}: Props) { const handleClick = useCallback( event => { event.stopPropagation(); - selectFiber(node.id, node.name); + const {id, name} = node; + selectFiber(id, name); }, [node, selectFiber], ); + const handleMouseEnter = () => { + const {id, name} = node; + hoverFiber({id, name}); + }; + + const handleMouseLeave = () => { + hoverFiber(null); + }; + // List items are absolutely positioned using the CSS "top" attribute. // The "left" value will always be 0. // Since height is fixed, and width is based on the node's duration, @@ -52,6 +70,8 @@ function CommitRankedListItem({data, index, style}: Props) { key={node.id} label={node.label} onClick={handleClick} + onMouseEnter={handleMouseEnter} + onMouseLeave={handleMouseLeave} width={Math.max(minBarWidth, scaleX(node.value, width))} x={0} y={top} diff --git a/packages/react-devtools-shared/src/devtools/views/Profiler/HoveredFiberInfo.css b/packages/react-devtools-shared/src/devtools/views/Profiler/HoveredFiberInfo.css new file mode 100644 index 0000000000000..d50e36935f20c --- /dev/null +++ b/packages/react-devtools-shared/src/devtools/views/Profiler/HoveredFiberInfo.css @@ -0,0 +1,36 @@ +.Toolbar { + padding: 0.25rem 0; + margin-bottom: 0.25rem; + flex: 0 0 auto; + display: flex; + align-items: center; + border-bottom: 1px solid var(--color-border); +} + +.Content { + user-select: none; + overflow-y: auto; +} + +.Component { + flex: 1; + font-weight: bold; + font-family: var(--font-family-monospace); + font-size: var(--font-size-monospace-normal); + white-space: nowrap; + overflow-x: hidden; + text-overflow: ellipsis; +} + +.Label { + font-weight: bold; +} + +.CurrentCommit { + margin-top: 0.25rem; + display: block; + width: 100%; + text-align: left; + background: none; + border: none; +} diff --git a/packages/react-devtools-shared/src/devtools/views/Profiler/HoveredFiberInfo.js b/packages/react-devtools-shared/src/devtools/views/Profiler/HoveredFiberInfo.js new file mode 100644 index 0000000000000..fa07d02b1d7cd --- /dev/null +++ b/packages/react-devtools-shared/src/devtools/views/Profiler/HoveredFiberInfo.js @@ -0,0 +1,78 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +import * as React from 'react'; +import {Fragment, useContext} from 'react'; +import {ProfilerContext} from './ProfilerContext'; +import {formatDuration, formatTime} from './utils'; +import WhatChanged from './WhatChanged'; +import {StoreContext} from '../context'; + +import styles from './HoveredFiberInfo.css'; + +import type {ChartNode} from './FlamegraphChartBuilder'; + +export type TooltipFiberData = {| + id: number, + name: string, +|}; + +export type Props = { + fiberData: ChartNode, +}; + +export default function HoveredFiberInfo({fiberData}: Props) { + const {profilerStore} = useContext(StoreContext); + const {rootID, selectedCommitIndex} = useContext(ProfilerContext); + + const {id, name} = fiberData; + const {profilingCache} = profilerStore; + + const commitIndices = profilingCache.getFiberCommits({ + fiberID: ((id: any): number), + rootID: ((rootID: any): number), + }); + + let renderDurationInfo; + let i = 0; + for (i = 0; i < commitIndices.length; i++) { + const commitIndex = commitIndices[i]; + if (selectedCommitIndex === commitIndex) { + const {duration, timestamp} = profilerStore.getCommitData( + ((rootID: any): number), + commitIndex, + ); + + renderDurationInfo = ( + + +
+ {formatTime(timestamp)}s for {formatDuration(duration)}ms +
+
+ ); + + break; + } + } + + return ( + +
+
{name}
+
+
+ + {renderDurationInfo || ( +
Did not render during this profiling session.
+ )} +
+
+ ); +} diff --git a/packages/react-devtools-shared/src/devtools/views/Profiler/InteractionListItem.js b/packages/react-devtools-shared/src/devtools/views/Profiler/InteractionListItem.js index cfc5d1229a9bc..b720ca3e0d98e 100644 --- a/packages/react-devtools-shared/src/devtools/views/Profiler/InteractionListItem.js +++ b/packages/react-devtools-shared/src/devtools/views/Profiler/InteractionListItem.js @@ -7,7 +7,8 @@ * @flow */ -import React, {memo, useCallback} from 'react'; +import * as React from 'react'; +import {memo, useCallback} from 'react'; import {areEqual} from 'react-window'; import {getGradientColor} from './utils'; diff --git a/packages/react-devtools-shared/src/devtools/views/Profiler/Interactions.js b/packages/react-devtools-shared/src/devtools/views/Profiler/Interactions.js index ffbd07f8863bf..b39d973053b88 100644 --- a/packages/react-devtools-shared/src/devtools/views/Profiler/Interactions.js +++ b/packages/react-devtools-shared/src/devtools/views/Profiler/Interactions.js @@ -7,7 +7,8 @@ * @flow */ -import React, {useCallback, useContext, useMemo} from 'react'; +import * as React from 'react'; +import {useCallback, useContext, useMemo} from 'react'; import AutoSizer from 'react-virtualized-auto-sizer'; import {FixedSizeList} from 'react-window'; import {ProfilerContext} from './ProfilerContext'; diff --git a/packages/react-devtools-shared/src/devtools/views/Profiler/NoCommitData.js b/packages/react-devtools-shared/src/devtools/views/Profiler/NoCommitData.js index 314f397016fa8..8396232a1de50 100644 --- a/packages/react-devtools-shared/src/devtools/views/Profiler/NoCommitData.js +++ b/packages/react-devtools-shared/src/devtools/views/Profiler/NoCommitData.js @@ -7,7 +7,7 @@ * @flow */ -import React from 'react'; +import * as React from 'react'; import styles from './NoCommitData.css'; diff --git a/packages/react-devtools-shared/src/devtools/views/Profiler/NoInteractions.js b/packages/react-devtools-shared/src/devtools/views/Profiler/NoInteractions.js index 993d7136ac036..932c906cc7c05 100644 --- a/packages/react-devtools-shared/src/devtools/views/Profiler/NoInteractions.js +++ b/packages/react-devtools-shared/src/devtools/views/Profiler/NoInteractions.js @@ -7,7 +7,7 @@ * @flow */ -import React from 'react'; +import * as React from 'react'; import styles from './NoInteractions.css'; diff --git a/packages/react-devtools-shared/src/devtools/views/Profiler/Profiler.js b/packages/react-devtools-shared/src/devtools/views/Profiler/Profiler.js index 868f4add55e8c..8be66852593e0 100644 --- a/packages/react-devtools-shared/src/devtools/views/Profiler/Profiler.js +++ b/packages/react-devtools-shared/src/devtools/views/Profiler/Profiler.js @@ -7,7 +7,8 @@ * @flow */ -import React, {Fragment, useContext} from 'react'; +import * as React from 'react'; +import {Fragment, useContext} from 'react'; import {ModalDialog} from '../ModalDialog'; import {ProfilerContext} from './ProfilerContext'; import TabBar from '../TabBar'; diff --git a/packages/react-devtools-shared/src/devtools/views/Profiler/ProfilerContext.js b/packages/react-devtools-shared/src/devtools/views/Profiler/ProfilerContext.js index 4c3bc8834fa93..71b82267b8372 100644 --- a/packages/react-devtools-shared/src/devtools/views/Profiler/ProfilerContext.js +++ b/packages/react-devtools-shared/src/devtools/views/Profiler/ProfilerContext.js @@ -7,13 +7,8 @@ * @flow */ -import React, { - createContext, - useCallback, - useContext, - useMemo, - useState, -} from 'react'; +import * as React from 'react'; +import {createContext, useCallback, useContext, useMemo, useState} from 'react'; import {unstable_batchedUpdates as batchedUpdates} from 'react-dom'; import {useLocalStorage, useSubscription} from '../hooks'; import { diff --git a/packages/react-devtools-shared/src/devtools/views/Profiler/ProfilingImportExportButtons.js b/packages/react-devtools-shared/src/devtools/views/Profiler/ProfilingImportExportButtons.js index b23d797aa3274..8d7112b31e226 100644 --- a/packages/react-devtools-shared/src/devtools/views/Profiler/ProfilingImportExportButtons.js +++ b/packages/react-devtools-shared/src/devtools/views/Profiler/ProfilingImportExportButtons.js @@ -7,7 +7,8 @@ * @flow */ -import React, {Fragment, useContext, useCallback, useRef} from 'react'; +import * as React from 'react'; +import {Fragment, useContext, useCallback, useRef} from 'react'; import {ProfilerContext} from './ProfilerContext'; import {ModalDialogContext} from '../ModalDialog'; import Button from '../Button'; diff --git a/packages/react-devtools-shared/src/devtools/views/Profiler/RecordToggle.js b/packages/react-devtools-shared/src/devtools/views/Profiler/RecordToggle.js index db03e74aa85ee..8efe9eae8e4a9 100644 --- a/packages/react-devtools-shared/src/devtools/views/Profiler/RecordToggle.js +++ b/packages/react-devtools-shared/src/devtools/views/Profiler/RecordToggle.js @@ -7,7 +7,8 @@ * @flow */ -import React, {useContext} from 'react'; +import * as React from 'react'; +import {useContext} from 'react'; import Button from '../Button'; import ButtonIcon from '../ButtonIcon'; import {ProfilerContext} from './ProfilerContext'; diff --git a/packages/react-devtools-shared/src/devtools/views/Profiler/ReloadAndProfileButton.js b/packages/react-devtools-shared/src/devtools/views/Profiler/ReloadAndProfileButton.js index d3772ead4d167..a54804bdce7e1 100644 --- a/packages/react-devtools-shared/src/devtools/views/Profiler/ReloadAndProfileButton.js +++ b/packages/react-devtools-shared/src/devtools/views/Profiler/ReloadAndProfileButton.js @@ -7,7 +7,8 @@ * @flow */ -import React, {useCallback, useContext, useMemo} from 'react'; +import * as React from 'react'; +import {useCallback, useContext, useMemo} from 'react'; import Button from '../Button'; import ButtonIcon from '../ButtonIcon'; import {BridgeContext, StoreContext} from '../context'; diff --git a/packages/react-devtools-shared/src/devtools/views/Profiler/RootSelector.js b/packages/react-devtools-shared/src/devtools/views/Profiler/RootSelector.js index b911faff35b9a..4506847e74504 100644 --- a/packages/react-devtools-shared/src/devtools/views/Profiler/RootSelector.js +++ b/packages/react-devtools-shared/src/devtools/views/Profiler/RootSelector.js @@ -7,7 +7,8 @@ * @flow */ -import React, {Fragment, useCallback, useContext} from 'react'; +import * as React from 'react'; +import {Fragment, useCallback, useContext} from 'react'; import {ProfilerContext} from './ProfilerContext'; import styles from './RootSelector.css'; diff --git a/packages/react-devtools-shared/src/devtools/views/Profiler/SidebarCommitInfo.js b/packages/react-devtools-shared/src/devtools/views/Profiler/SidebarCommitInfo.js index 735065f2809cc..6cff0e209fa9a 100644 --- a/packages/react-devtools-shared/src/devtools/views/Profiler/SidebarCommitInfo.js +++ b/packages/react-devtools-shared/src/devtools/views/Profiler/SidebarCommitInfo.js @@ -7,7 +7,8 @@ * @flow */ -import React, {Fragment, useContext} from 'react'; +import * as React from 'react'; +import {Fragment, useContext} from 'react'; import {ProfilerContext} from './ProfilerContext'; import {formatDuration, formatTime} from './utils'; import {StoreContext} from '../context'; diff --git a/packages/react-devtools-shared/src/devtools/views/Profiler/SidebarInteractions.js b/packages/react-devtools-shared/src/devtools/views/Profiler/SidebarInteractions.js index 21787d5fb1f50..186ec24250eac 100644 --- a/packages/react-devtools-shared/src/devtools/views/Profiler/SidebarInteractions.js +++ b/packages/react-devtools-shared/src/devtools/views/Profiler/SidebarInteractions.js @@ -7,7 +7,8 @@ * @flow */ -import React, {Fragment, useContext} from 'react'; +import * as React from 'react'; +import {Fragment, useContext} from 'react'; import {ProfilerContext} from './ProfilerContext'; import {formatDuration, formatTime} from './utils'; import {StoreContext} from '../context'; diff --git a/packages/react-devtools-shared/src/devtools/views/Profiler/SidebarSelectedFiberInfo.css b/packages/react-devtools-shared/src/devtools/views/Profiler/SidebarSelectedFiberInfo.css index 2ea9526d9653a..de19c10391653 100644 --- a/packages/react-devtools-shared/src/devtools/views/Profiler/SidebarSelectedFiberInfo.css +++ b/packages/react-devtools-shared/src/devtools/views/Profiler/SidebarSelectedFiberInfo.css @@ -42,6 +42,7 @@ padding: 0.25rem 0.5rem; color: var(--color-text); } + .Commit:focus, .Commit:hover { outline: none; @@ -52,25 +53,7 @@ background-color: var(--color-background-selected); color: var(--color-text-selected); } + .CurrentCommit:focus { outline: none; } - -.WhatChangedItem { - margin-top: 0.25rem; -} - -.WhatChangedKey { - font-family: var(--font-family-monospace); - font-size: var(--font-size-monospace-small); - line-height: 1; -} -.WhatChangedKey:first-of-type::before { - content: ' ('; -} -.WhatChangedKey::after { - content: ', '; -} -.WhatChangedKey:last-of-type::after { - content: ')'; -} diff --git a/packages/react-devtools-shared/src/devtools/views/Profiler/SidebarSelectedFiberInfo.js b/packages/react-devtools-shared/src/devtools/views/Profiler/SidebarSelectedFiberInfo.js index 595d0e0e17439..79244eb90387a 100644 --- a/packages/react-devtools-shared/src/devtools/views/Profiler/SidebarSelectedFiberInfo.js +++ b/packages/react-devtools-shared/src/devtools/views/Profiler/SidebarSelectedFiberInfo.js @@ -7,8 +7,9 @@ * @flow */ -import React, {Fragment, useContext} from 'react'; -import ProfilerStore from 'react-devtools-shared/src/devtools/ProfilerStore'; +import * as React from 'react'; +import {Fragment, useContext} from 'react'; +import WhatChanged from './WhatChanged'; import {ProfilerContext} from './ProfilerContext'; import {formatDuration, formatTime} from './utils'; import {StoreContext} from '../context'; @@ -75,12 +76,7 @@ export default function SidebarSelectedFiberInfo(_: Props) {
- + {listItems.length > 0 && ( : {listItems} @@ -93,129 +89,3 @@ export default function SidebarSelectedFiberInfo(_: Props) { ); } - -type WhatChangedProps = {| - commitIndex: number | null, - fiberID: number, - profilerStore: ProfilerStore, - rootID: number, -|}; - -function WhatChanged({ - commitIndex, - fiberID, - profilerStore, - rootID, -}: WhatChangedProps) { - // TRICKY - // Handle edge case where no commit is selected because of a min-duration filter update. - // If the commit index is null, suspending for data below would throw an error. - // TODO (ProfilerContext) This check should not be necessary. - if (commitIndex === null) { - return null; - } - - const {changeDescriptions} = profilerStore.getCommitData( - ((rootID: any): number), - commitIndex, - ); - if (changeDescriptions === null) { - return null; - } - - const changeDescription = changeDescriptions.get(fiberID); - if (changeDescription == null) { - return null; - } - - if (changeDescription.isFirstMount) { - return ( -
- -
- This is the first time the component rendered. -
-
- ); - } - - const changes = []; - - if (changeDescription.context === true) { - changes.push( -
- • Context changed -
, - ); - } else if ( - typeof changeDescription.context === 'object' && - changeDescription.context !== null && - changeDescription.context.length !== 0 - ) { - changes.push( -
- • Context changed: - {changeDescription.context.map(key => ( - - {key} - - ))} -
, - ); - } - - if (changeDescription.didHooksChange) { - changes.push( -
- • Hooks changed -
, - ); - } - - if ( - changeDescription.props !== null && - changeDescription.props.length !== 0 - ) { - changes.push( -
- • Props changed: - {changeDescription.props.map(key => ( - - {key} - - ))} -
, - ); - } - - if ( - changeDescription.state !== null && - changeDescription.state.length !== 0 - ) { - changes.push( -
- • State changed: - {changeDescription.state.map(key => ( - - {key} - - ))} -
, - ); - } - - if (changes.length === 0) { - changes.push( -
- The parent component rendered. -
, - ); - } - - return ( -
- - {changes} -
- ); -} diff --git a/packages/react-devtools-shared/src/devtools/views/Profiler/SnapshotCommitList.js b/packages/react-devtools-shared/src/devtools/views/Profiler/SnapshotCommitList.js index 1723b9d5c7fe1..54b5370991e93 100644 --- a/packages/react-devtools-shared/src/devtools/views/Profiler/SnapshotCommitList.js +++ b/packages/react-devtools-shared/src/devtools/views/Profiler/SnapshotCommitList.js @@ -7,7 +7,8 @@ * @flow */ -import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; +import * as React from 'react'; +import {useCallback, useEffect, useMemo, useRef, useState} from 'react'; import AutoSizer from 'react-virtualized-auto-sizer'; import {FixedSizeList} from 'react-window'; import SnapshotCommitListItem from './SnapshotCommitListItem'; diff --git a/packages/react-devtools-shared/src/devtools/views/Profiler/SnapshotCommitListItem.js b/packages/react-devtools-shared/src/devtools/views/Profiler/SnapshotCommitListItem.js index feb69c8f7c84a..e49a0c153448e 100644 --- a/packages/react-devtools-shared/src/devtools/views/Profiler/SnapshotCommitListItem.js +++ b/packages/react-devtools-shared/src/devtools/views/Profiler/SnapshotCommitListItem.js @@ -7,7 +7,8 @@ * @flow */ -import React, {memo, useCallback} from 'react'; +import * as React from 'react'; +import {memo, useCallback} from 'react'; import {areEqual} from 'react-window'; import {getGradientColor, formatDuration, formatTime} from './utils'; diff --git a/packages/react-devtools-shared/src/devtools/views/Profiler/SnapshotSelector.js b/packages/react-devtools-shared/src/devtools/views/Profiler/SnapshotSelector.js index 41d5e7a77ed03..c67b895edf46e 100644 --- a/packages/react-devtools-shared/src/devtools/views/Profiler/SnapshotSelector.js +++ b/packages/react-devtools-shared/src/devtools/views/Profiler/SnapshotSelector.js @@ -7,7 +7,8 @@ * @flow */ -import React, {Fragment, useCallback, useContext, useMemo} from 'react'; +import * as React from 'react'; +import {Fragment, useCallback, useContext, useMemo} from 'react'; import Button from '../Button'; import ButtonIcon from '../ButtonIcon'; import {ProfilerContext} from './ProfilerContext'; diff --git a/packages/react-devtools-shared/src/devtools/views/Profiler/Tooltip.css b/packages/react-devtools-shared/src/devtools/views/Profiler/Tooltip.css new file mode 100644 index 0000000000000..2d2bbaf34ea38 --- /dev/null +++ b/packages/react-devtools-shared/src/devtools/views/Profiler/Tooltip.css @@ -0,0 +1,24 @@ +.Tooltip { + position: absolute; + pointer-events: none; + border: none; + border-radius: 0.25rem; + padding: 0.25rem 0.5rem; + font-family: var(--font-family-sans); + font-size: 12px; + background-color: var(--color-tooltip-background); + color: var(--color-tooltip-text); + opacity: 1; + /* Make sure this is above the DevTools, which are above the Overlay */ + z-index: 10000002; +} + +.Tooltip.hidden { + opacity: 0; +} + + +.Container { + width: -moz-max-content; + width: -webkit-max-content; +} diff --git a/packages/react-devtools-shared/src/devtools/views/Profiler/Tooltip.js b/packages/react-devtools-shared/src/devtools/views/Profiler/Tooltip.js new file mode 100644 index 0000000000000..041b55134c9c2 --- /dev/null +++ b/packages/react-devtools-shared/src/devtools/views/Profiler/Tooltip.js @@ -0,0 +1,103 @@ +/** @flow */ + +import * as React from 'react'; +import {useRef} from 'react'; + +import styles from './Tooltip.css'; + +const initialTooltipState = {height: 0, mouseX: 0, mouseY: 0, width: 0}; + +export default function Tooltip({children, label}: any) { + const containerRef = useRef(null); + const tooltipRef = useRef(null); + + // update the position of the tooltip based on current mouse position + const updateTooltipPosition = (event: SyntheticMouseEvent<*>) => { + const element = tooltipRef.current; + if (element != null) { + // first find the mouse position + const mousePosition = getMousePosition(containerRef.current, event); + // use the mouse position to find the position of tooltip + const {left, top} = getTooltipPosition(element, mousePosition); + // update tooltip position + element.style.left = left; + element.style.top = top; + } + }; + + const onMouseMove = (event: SyntheticMouseEvent<*>) => { + updateTooltipPosition(event); + }; + + const tooltipClassName = label === null ? styles.hidden : ''; + + return ( +
+
+ {label} +
+ {children} +
+ ); +} + +const TOOLTIP_OFFSET = 5; + +// Method used to find the position of the tooltip based on current mouse position +function getTooltipPosition(element, mousePosition) { + const {height, mouseX, mouseY, width} = mousePosition; + let top = 0; + let left = 0; + + if (mouseY + TOOLTIP_OFFSET + element.offsetHeight >= height) { + if (mouseY - TOOLTIP_OFFSET - element.offsetHeight > 0) { + top = `${mouseY - element.offsetHeight - TOOLTIP_OFFSET}px`; + } else { + top = '0px'; + } + } else { + top = `${mouseY + TOOLTIP_OFFSET}px`; + } + + if (mouseX + TOOLTIP_OFFSET + element.offsetWidth >= width) { + if (mouseX - TOOLTIP_OFFSET - element.offsetWidth > 0) { + left = `${mouseX - element.offsetWidth - TOOLTIP_OFFSET}px`; + } else { + left = '0px'; + } + } else { + left = `${mouseX + TOOLTIP_OFFSET * 2}px`; + } + + return {left, top}; +} + +// method used to find the current mouse position inside the container +function getMousePosition( + relativeContainer, + mouseEvent: SyntheticMouseEvent<*>, +) { + if (relativeContainer !== null) { + // Positon within the nearest position:relative container. + let targetContainer = relativeContainer; + while (targetContainer.parentElement != null) { + if (targetContainer.style.position === 'relative') { + break; + } else { + targetContainer = targetContainer.parentElement; + } + } + + const {height, left, top, width} = targetContainer.getBoundingClientRect(); + + const mouseX = mouseEvent.clientX - left; + const mouseY = mouseEvent.clientY - top; + + return {height, mouseX, mouseY, width}; + } else { + return initialTooltipState; + } +} diff --git a/packages/react-devtools-shared/src/devtools/views/Profiler/WhatChanged.css b/packages/react-devtools-shared/src/devtools/views/Profiler/WhatChanged.css new file mode 100644 index 0000000000000..246dbfe9bac17 --- /dev/null +++ b/packages/react-devtools-shared/src/devtools/views/Profiler/WhatChanged.css @@ -0,0 +1,29 @@ +.Component { + margin-bottom: 0.5rem; +} + +.Item { + margin-top: 0.25rem; +} + +.Key { + font-family: var(--font-family-monospace); + font-size: var(--font-size-monospace-small); + line-height: 1; +} + +.Key:first-of-type::before { + content: ' ('; +} + +.Key::after { + content: ', '; +} + +.Key:last-of-type::after { + content: ')'; +} + +.Label { + font-weight: bold; +} diff --git a/packages/react-devtools-shared/src/devtools/views/Profiler/WhatChanged.js b/packages/react-devtools-shared/src/devtools/views/Profiler/WhatChanged.js new file mode 100644 index 0000000000000..00bdedddc9c39 --- /dev/null +++ b/packages/react-devtools-shared/src/devtools/views/Profiler/WhatChanged.js @@ -0,0 +1,137 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +import * as React from 'react'; +import {useContext} from 'react'; +import {ProfilerContext} from '../Profiler/ProfilerContext'; +import {StoreContext} from '../context'; + +import styles from './WhatChanged.css'; + +type Props = {| + fiberID: number, +|}; + +export default function WhatChanged({fiberID}: Props) { + const {profilerStore} = useContext(StoreContext); + const {rootID, selectedCommitIndex} = useContext(ProfilerContext); + + // TRICKY + // Handle edge case where no commit is selected because of a min-duration filter update. + // If the commit index is null, suspending for data below would throw an error. + // TODO (ProfilerContext) This check should not be necessary. + if (selectedCommitIndex === null) { + return null; + } + + const {changeDescriptions} = profilerStore.getCommitData( + ((rootID: any): number), + selectedCommitIndex, + ); + + if (changeDescriptions === null) { + return null; + } + + const changeDescription = changeDescriptions.get(fiberID); + if (changeDescription == null) { + return null; + } + + if (changeDescription.isFirstMount) { + return ( +
+ +
+ This is the first time the component rendered. +
+
+ ); + } + + const changes = []; + + if (changeDescription.context === true) { + changes.push( +
+ • Context changed +
, + ); + } else if ( + typeof changeDescription.context === 'object' && + changeDescription.context !== null && + changeDescription.context.length !== 0 + ) { + changes.push( +
+ • Context changed: + {changeDescription.context.map(key => ( + + {key} + + ))} +
, + ); + } + + if (changeDescription.didHooksChange) { + changes.push( +
+ • Hooks changed +
, + ); + } + + if ( + changeDescription.props !== null && + changeDescription.props.length !== 0 + ) { + changes.push( +
+ • Props changed: + {changeDescription.props.map(key => ( + + {key} + + ))} +
, + ); + } + + if ( + changeDescription.state !== null && + changeDescription.state.length !== 0 + ) { + changes.push( +
+ • State changed: + {changeDescription.state.map(key => ( + + {key} + + ))} +
, + ); + } + + if (changes.length === 0) { + changes.push( +
+ The parent component rendered. +
, + ); + } + + return ( +
+ + {changes} +
+ ); +} diff --git a/packages/react-devtools-shared/src/devtools/views/ReactLogo.js b/packages/react-devtools-shared/src/devtools/views/ReactLogo.js index 5ae9e09e9d41c..8f9cfc15c24a7 100644 --- a/packages/react-devtools-shared/src/devtools/views/ReactLogo.js +++ b/packages/react-devtools-shared/src/devtools/views/ReactLogo.js @@ -7,7 +7,7 @@ * @flow */ -import React from 'react'; +import * as React from 'react'; import styles from './ReactLogo.css'; diff --git a/packages/react-devtools-shared/src/devtools/views/Settings/ComponentsSettings.js b/packages/react-devtools-shared/src/devtools/views/Settings/ComponentsSettings.js index 8244a4b83d622..6e25adaa5d5ba 100644 --- a/packages/react-devtools-shared/src/devtools/views/Settings/ComponentsSettings.js +++ b/packages/react-devtools-shared/src/devtools/views/Settings/ComponentsSettings.js @@ -7,7 +7,8 @@ * @flow */ -import React, { +import * as React from 'react'; +import { useCallback, useContext, useEffect, diff --git a/packages/react-devtools-shared/src/devtools/views/Settings/GeneralSettings.js b/packages/react-devtools-shared/src/devtools/views/Settings/GeneralSettings.js index 4e7ce6357c638..6595c04d71d96 100644 --- a/packages/react-devtools-shared/src/devtools/views/Settings/GeneralSettings.js +++ b/packages/react-devtools-shared/src/devtools/views/Settings/GeneralSettings.js @@ -7,7 +7,8 @@ * @flow */ -import React, {useContext} from 'react'; +import * as React from 'react'; +import {useContext} from 'react'; import {SettingsContext} from './SettingsContext'; import {StoreContext} from '../context'; import {CHANGE_LOG_URL} from 'react-devtools-shared/src/constants'; diff --git a/packages/react-devtools-shared/src/devtools/views/Settings/ProfilerSettings.js b/packages/react-devtools-shared/src/devtools/views/Settings/ProfilerSettings.js index 7265154229442..7a2c8ede453d7 100644 --- a/packages/react-devtools-shared/src/devtools/views/Settings/ProfilerSettings.js +++ b/packages/react-devtools-shared/src/devtools/views/Settings/ProfilerSettings.js @@ -7,7 +7,8 @@ * @flow */ -import React, {useCallback, useContext, useMemo, useRef} from 'react'; +import * as React from 'react'; +import {useCallback, useContext, useMemo, useRef} from 'react'; import {useSubscription} from '../hooks'; import {StoreContext} from '../context'; import {ProfilerContext} from 'react-devtools-shared/src/devtools/views/Profiler/ProfilerContext'; diff --git a/packages/react-devtools-shared/src/devtools/views/Settings/SettingsContext.js b/packages/react-devtools-shared/src/devtools/views/Settings/SettingsContext.js index ce6dc6022f261..3bafda92f74fd 100644 --- a/packages/react-devtools-shared/src/devtools/views/Settings/SettingsContext.js +++ b/packages/react-devtools-shared/src/devtools/views/Settings/SettingsContext.js @@ -7,7 +7,8 @@ * @flow */ -import React, { +import * as React from 'react'; +import { createContext, useContext, useEffect, diff --git a/packages/react-devtools-shared/src/devtools/views/Settings/SettingsModal.js b/packages/react-devtools-shared/src/devtools/views/Settings/SettingsModal.js index e76dae6518203..adad478c6842b 100644 --- a/packages/react-devtools-shared/src/devtools/views/Settings/SettingsModal.js +++ b/packages/react-devtools-shared/src/devtools/views/Settings/SettingsModal.js @@ -7,13 +7,8 @@ * @flow */ -import React, { - useCallback, - useContext, - useEffect, - useMemo, - useRef, -} from 'react'; +import * as React from 'react'; +import {useCallback, useContext, useEffect, useMemo, useRef} from 'react'; import {SettingsModalContext} from './SettingsModalContext'; import Button from '../Button'; import ButtonIcon from '../ButtonIcon'; diff --git a/packages/react-devtools-shared/src/devtools/views/Settings/SettingsModalContext.js b/packages/react-devtools-shared/src/devtools/views/Settings/SettingsModalContext.js index 694e859808730..d3b500cee872b 100644 --- a/packages/react-devtools-shared/src/devtools/views/Settings/SettingsModalContext.js +++ b/packages/react-devtools-shared/src/devtools/views/Settings/SettingsModalContext.js @@ -7,7 +7,8 @@ * @flow */ -import React, {createContext, useMemo, useState} from 'react'; +import * as React from 'react'; +import {createContext, useMemo, useState} from 'react'; export type DisplayDensity = 'comfortable' | 'compact'; export type Theme = 'auto' | 'light' | 'dark'; diff --git a/packages/react-devtools-shared/src/devtools/views/Settings/SettingsModalContextToggle.js b/packages/react-devtools-shared/src/devtools/views/Settings/SettingsModalContextToggle.js index 465f835a05aa3..c20124d90f6f3 100644 --- a/packages/react-devtools-shared/src/devtools/views/Settings/SettingsModalContextToggle.js +++ b/packages/react-devtools-shared/src/devtools/views/Settings/SettingsModalContextToggle.js @@ -7,7 +7,8 @@ * @flow */ -import React, {useCallback, useContext, useMemo} from 'react'; +import * as React from 'react'; +import {useCallback, useContext, useMemo} from 'react'; import {SettingsModalContext} from './SettingsModalContext'; import Button from '../Button'; import ButtonIcon from '../ButtonIcon'; diff --git a/packages/react-devtools-shared/src/devtools/views/TabBar.js b/packages/react-devtools-shared/src/devtools/views/TabBar.js index 7e6b9b9e9866f..18664dae86b70 100644 --- a/packages/react-devtools-shared/src/devtools/views/TabBar.js +++ b/packages/react-devtools-shared/src/devtools/views/TabBar.js @@ -7,7 +7,8 @@ * @flow */ -import React, {Fragment, useCallback} from 'react'; +import * as React from 'react'; +import {Fragment, useCallback} from 'react'; import Tooltip from '@reach/tooltip'; import Icon from './Icon'; diff --git a/packages/react-devtools-shared/src/devtools/views/Toggle.js b/packages/react-devtools-shared/src/devtools/views/Toggle.js index 1eaaa73738905..c5961a9bbfc5c 100644 --- a/packages/react-devtools-shared/src/devtools/views/Toggle.js +++ b/packages/react-devtools-shared/src/devtools/views/Toggle.js @@ -7,7 +7,8 @@ * @flow */ -import React, {useCallback} from 'react'; +import * as React from 'react'; +import {useCallback} from 'react'; import Tooltip from '@reach/tooltip'; import styles from './Toggle.css'; diff --git a/packages/react-devtools-shared/src/devtools/views/UnsupportedVersionDialog.js b/packages/react-devtools-shared/src/devtools/views/UnsupportedVersionDialog.js index 1f35eee3c85e4..e28398d7f6420 100644 --- a/packages/react-devtools-shared/src/devtools/views/UnsupportedVersionDialog.js +++ b/packages/react-devtools-shared/src/devtools/views/UnsupportedVersionDialog.js @@ -7,7 +7,8 @@ * @flow */ -import React, {Fragment, useContext, useEffect, useState} from 'react'; +import * as React from 'react'; +import {Fragment, useContext, useEffect, useState} from 'react'; import {unstable_batchedUpdates as batchedUpdates} from 'react-dom'; import {ModalDialogContext} from './ModalDialog'; import {StoreContext} from './context'; diff --git a/packages/react-devtools-shared/src/devtools/views/WarnIfLegacyBackendDetected.js b/packages/react-devtools-shared/src/devtools/views/WarnIfLegacyBackendDetected.js index cb4933f8dbda2..12d88c0562205 100644 --- a/packages/react-devtools-shared/src/devtools/views/WarnIfLegacyBackendDetected.js +++ b/packages/react-devtools-shared/src/devtools/views/WarnIfLegacyBackendDetected.js @@ -7,7 +7,8 @@ * @flow */ -import React, {Fragment, useContext, useEffect} from 'react'; +import * as React from 'react'; +import {Fragment, useContext, useEffect} from 'react'; import {BridgeContext} from './context'; import {ModalDialogContext} from './ModalDialog'; diff --git a/packages/react-devtools-shared/src/devtools/views/portaledContent.js b/packages/react-devtools-shared/src/devtools/views/portaledContent.js index cf312787f5ea7..be51534f1b676 100644 --- a/packages/react-devtools-shared/src/devtools/views/portaledContent.js +++ b/packages/react-devtools-shared/src/devtools/views/portaledContent.js @@ -7,7 +7,7 @@ * @flow */ -import React from 'react'; +import * as React from 'react'; import {createPortal} from 'react-dom'; import ErrorBoundary from './ErrorBoundary'; diff --git a/packages/react-devtools-shared/src/node_modules/react-window/src/createGridComponent.js b/packages/react-devtools-shared/src/node_modules/react-window/src/createGridComponent.js index 24f07403b0d8b..12d1a4cc9ebd6 100644 --- a/packages/react-devtools-shared/src/node_modules/react-window/src/createGridComponent.js +++ b/packages/react-devtools-shared/src/node_modules/react-window/src/createGridComponent.js @@ -1,7 +1,8 @@ // @flow import memoizeOne from 'memoize-one'; -import React, { createElement, PureComponent } from 'react'; +import * as React from 'react'; +import { createElement, PureComponent } from 'react'; import { cancelTimeout, requestTimeout } from './timer'; import { getScrollbarSize, getRTLOffsetType } from './domHelpers'; diff --git a/packages/react-devtools-shared/src/node_modules/react-window/src/createListComponent.js b/packages/react-devtools-shared/src/node_modules/react-window/src/createListComponent.js index 5b5f1a15c7c1b..bf9c3e902a96f 100644 --- a/packages/react-devtools-shared/src/node_modules/react-window/src/createListComponent.js +++ b/packages/react-devtools-shared/src/node_modules/react-window/src/createListComponent.js @@ -1,7 +1,8 @@ // @flow import memoizeOne from 'memoize-one'; -import React, { createElement, PureComponent } from 'react'; +import * as React from 'react'; +import { createElement, PureComponent } from 'react'; import { cancelTimeout, requestTimeout } from './timer'; import { getRTLOffsetType } from './domHelpers'; diff --git a/packages/react-devtools-shell/src/app/DeeplyNestedComponents/index.js b/packages/react-devtools-shell/src/app/DeeplyNestedComponents/index.js index bc6b05fa9d774..613914e8abfd1 100644 --- a/packages/react-devtools-shell/src/app/DeeplyNestedComponents/index.js +++ b/packages/react-devtools-shell/src/app/DeeplyNestedComponents/index.js @@ -7,7 +7,8 @@ * @flow */ -import React, {Fragment} from 'react'; +import * as React from 'react'; +import {Fragment} from 'react'; function wrapWithHoc(Component, index) { function HOC() { diff --git a/packages/react-devtools-shell/src/app/EditableProps/index.js b/packages/react-devtools-shell/src/app/EditableProps/index.js index 220a58c17be32..306ac73b91a7c 100644 --- a/packages/react-devtools-shell/src/app/EditableProps/index.js +++ b/packages/react-devtools-shell/src/app/EditableProps/index.js @@ -7,7 +7,8 @@ * @flow */ -import React, { +import * as React from 'react'; +import { createContext, Component, forwardRef, diff --git a/packages/react-devtools-shell/src/app/ElementTypes/index.js b/packages/react-devtools-shell/src/app/ElementTypes/index.js index 1b7bb0d32fc60..3b2c97dae5036 100644 --- a/packages/react-devtools-shell/src/app/ElementTypes/index.js +++ b/packages/react-devtools-shell/src/app/ElementTypes/index.js @@ -7,7 +7,8 @@ * @flow */ -import React, { +import * as React from 'react'; +import { createContext, forwardRef, lazy, diff --git a/packages/react-devtools-shell/src/app/Hydration/index.js b/packages/react-devtools-shell/src/app/Hydration/index.js index d2be0e6901737..1cf78ecddc56b 100644 --- a/packages/react-devtools-shell/src/app/Hydration/index.js +++ b/packages/react-devtools-shell/src/app/Hydration/index.js @@ -7,7 +7,8 @@ * @flow */ -import React, {Fragment, useDebugValue, useState} from 'react'; +import * as React from 'react'; +import {Fragment, useDebugValue, useState} from 'react'; const div = document.createElement('div'); const exmapleFunction = () => {}; diff --git a/packages/react-devtools-shell/src/app/Iframe/index.js b/packages/react-devtools-shell/src/app/Iframe/index.js index 394b0d680f9c4..d22c33f2d9efa 100644 --- a/packages/react-devtools-shell/src/app/Iframe/index.js +++ b/packages/react-devtools-shell/src/app/Iframe/index.js @@ -1,7 +1,8 @@ /** @flow */ -import React, {Fragment} from 'react'; -import ReactDOM from 'react-dom'; +import * as React from 'react'; +import {Fragment} from 'react'; +import * as ReactDOM from 'react-dom'; export default function Iframe() { return ( diff --git a/packages/react-devtools-shell/src/app/InspectableElements/CircularReferences.js b/packages/react-devtools-shell/src/app/InspectableElements/CircularReferences.js index f2320c490a160..ffce696fa8ef3 100644 --- a/packages/react-devtools-shell/src/app/InspectableElements/CircularReferences.js +++ b/packages/react-devtools-shell/src/app/InspectableElements/CircularReferences.js @@ -7,7 +7,7 @@ * @flow */ -import React from 'react'; +import * as React from 'react'; const arrayOne = []; const arrayTwo = []; diff --git a/packages/react-devtools-shell/src/app/InspectableElements/Contexts.js b/packages/react-devtools-shell/src/app/InspectableElements/Contexts.js index 776e043feca2d..030e995e356c4 100644 --- a/packages/react-devtools-shell/src/app/InspectableElements/Contexts.js +++ b/packages/react-devtools-shell/src/app/InspectableElements/Contexts.js @@ -7,7 +7,8 @@ * @flow */ -import React, {createContext, Component, Fragment, useContext} from 'react'; +import * as React from 'react'; +import {createContext, Component, Fragment, useContext} from 'react'; import PropTypes from 'prop-types'; function someNamedFunction() {} diff --git a/packages/react-devtools-shell/src/app/InspectableElements/CustomHooks.js b/packages/react-devtools-shell/src/app/InspectableElements/CustomHooks.js index e363c35d58bcf..cf23fbd2159c5 100644 --- a/packages/react-devtools-shell/src/app/InspectableElements/CustomHooks.js +++ b/packages/react-devtools-shell/src/app/InspectableElements/CustomHooks.js @@ -7,7 +7,8 @@ * @flow */ -import React, { +import * as React from 'react'; +import { forwardRef, Fragment, memo, diff --git a/packages/react-devtools-shell/src/app/InspectableElements/CustomObject.js b/packages/react-devtools-shell/src/app/InspectableElements/CustomObject.js index be8e53b5e78f9..c4557e5879a1f 100644 --- a/packages/react-devtools-shell/src/app/InspectableElements/CustomObject.js +++ b/packages/react-devtools-shell/src/app/InspectableElements/CustomObject.js @@ -7,7 +7,7 @@ * @flow */ -import React from 'react'; +import * as React from 'react'; class Custom { _number = 42; diff --git a/packages/react-devtools-shell/src/app/InspectableElements/EdgeCaseObjects.js b/packages/react-devtools-shell/src/app/InspectableElements/EdgeCaseObjects.js index 5303b9a281d14..b747f855dae48 100644 --- a/packages/react-devtools-shell/src/app/InspectableElements/EdgeCaseObjects.js +++ b/packages/react-devtools-shell/src/app/InspectableElements/EdgeCaseObjects.js @@ -7,7 +7,7 @@ * @flow */ -import React from 'react'; +import * as React from 'react'; const objectWithModifiedHasOwnProperty = { foo: 'abc', diff --git a/packages/react-devtools-shell/src/app/InspectableElements/InspectableElements.js b/packages/react-devtools-shell/src/app/InspectableElements/InspectableElements.js index 62cf39fe9767a..49fc1c4f2edd6 100644 --- a/packages/react-devtools-shell/src/app/InspectableElements/InspectableElements.js +++ b/packages/react-devtools-shell/src/app/InspectableElements/InspectableElements.js @@ -7,7 +7,8 @@ * @flow */ -import React, {Fragment} from 'react'; +import * as React from 'react'; +import {Fragment} from 'react'; import UnserializableProps from './UnserializableProps'; import CircularReferences from './CircularReferences'; import Contexts from './Contexts'; diff --git a/packages/react-devtools-shell/src/app/InspectableElements/NestedProps.js b/packages/react-devtools-shell/src/app/InspectableElements/NestedProps.js index 24076e85951d2..16ccad8d32e73 100644 --- a/packages/react-devtools-shell/src/app/InspectableElements/NestedProps.js +++ b/packages/react-devtools-shell/src/app/InspectableElements/NestedProps.js @@ -7,7 +7,7 @@ * @flow */ -import React from 'react'; +import * as React from 'react'; const object = { string: 'abc', diff --git a/packages/react-devtools-shell/src/app/InspectableElements/SimpleValues.js b/packages/react-devtools-shell/src/app/InspectableElements/SimpleValues.js index 1a748d926d0fc..6623e53d03c3e 100644 --- a/packages/react-devtools-shell/src/app/InspectableElements/SimpleValues.js +++ b/packages/react-devtools-shell/src/app/InspectableElements/SimpleValues.js @@ -7,7 +7,8 @@ * @flow */ -import React, {Component} from 'react'; +import * as React from 'react'; +import {Component} from 'react'; function noop() {} diff --git a/packages/react-devtools-shell/src/app/InspectableElements/UnserializableProps.js b/packages/react-devtools-shell/src/app/InspectableElements/UnserializableProps.js index 982ea6528425e..f0f08b7d173c1 100644 --- a/packages/react-devtools-shell/src/app/InspectableElements/UnserializableProps.js +++ b/packages/react-devtools-shell/src/app/InspectableElements/UnserializableProps.js @@ -7,7 +7,7 @@ * @flow */ -import React from 'react'; +import * as React from 'react'; import Immutable from 'immutable'; const set = new Set(['abc', 123]); diff --git a/packages/react-devtools-shell/src/app/InteractionTracing/index.js b/packages/react-devtools-shell/src/app/InteractionTracing/index.js index 9d713d8fe5bcd..fd9bb891e0e67 100644 --- a/packages/react-devtools-shell/src/app/InteractionTracing/index.js +++ b/packages/react-devtools-shell/src/app/InteractionTracing/index.js @@ -7,7 +7,8 @@ * @flow */ -import React, {Fragment, useCallback, useEffect, useState} from 'react'; +import * as React from 'react'; +import {Fragment, useCallback, useEffect, useState} from 'react'; import {unstable_batchedUpdates as batchedUpdates} from 'react-dom'; import { unstable_trace as trace, diff --git a/packages/react-devtools-shell/src/app/PriorityLevels/index.js b/packages/react-devtools-shell/src/app/PriorityLevels/index.js index c19e02868f9b5..32858eb08a04f 100644 --- a/packages/react-devtools-shell/src/app/PriorityLevels/index.js +++ b/packages/react-devtools-shell/src/app/PriorityLevels/index.js @@ -7,7 +7,8 @@ * @flow */ -import React, {Fragment, useCallback, useState} from 'react'; +import * as React from 'react'; +import {Fragment, useCallback, useState} from 'react'; import { unstable_IdlePriority as IdlePriority, unstable_LowPriority as LowPriority, diff --git a/packages/react-devtools-shell/src/app/ReactNativeWeb/index.js b/packages/react-devtools-shell/src/app/ReactNativeWeb/index.js index 2f0ca88d3242c..89a0badca15f2 100644 --- a/packages/react-devtools-shell/src/app/ReactNativeWeb/index.js +++ b/packages/react-devtools-shell/src/app/ReactNativeWeb/index.js @@ -7,7 +7,8 @@ * @flow */ -import React, {Fragment, useState} from 'react'; +import * as React from 'react'; +import {Fragment, useState} from 'react'; // $FlowFixMe import {Button, Text, View} from 'react-native-web'; diff --git a/packages/react-devtools-shell/src/app/SuspenseTree/index.js b/packages/react-devtools-shell/src/app/SuspenseTree/index.js index 4763b51a42960..73701922d2250 100644 --- a/packages/react-devtools-shell/src/app/SuspenseTree/index.js +++ b/packages/react-devtools-shell/src/app/SuspenseTree/index.js @@ -7,7 +7,8 @@ * @flow */ -import React, {Fragment, Suspense, SuspenseList, useState} from 'react'; +import * as React from 'react'; +import {Fragment, Suspense, SuspenseList, useState} from 'react'; function SuspenseTree() { return ( diff --git a/packages/react-devtools-shell/src/app/ToDoList/List.js b/packages/react-devtools-shell/src/app/ToDoList/List.js index e3fee20080797..47dbb3f6abb03 100644 --- a/packages/react-devtools-shell/src/app/ToDoList/List.js +++ b/packages/react-devtools-shell/src/app/ToDoList/List.js @@ -7,7 +7,8 @@ * @flow */ -import React, {Fragment, useCallback, useState} from 'react'; +import * as React from 'react'; +import {Fragment, useCallback, useState} from 'react'; import ListItem from './ListItem'; import styles from './List.css'; diff --git a/packages/react-devtools-shell/src/app/ToDoList/ListItem.js b/packages/react-devtools-shell/src/app/ToDoList/ListItem.js index 9ef1afffdf44c..5d816f652c830 100644 --- a/packages/react-devtools-shell/src/app/ToDoList/ListItem.js +++ b/packages/react-devtools-shell/src/app/ToDoList/ListItem.js @@ -7,7 +7,8 @@ * @flow */ -import React, {memo, useCallback} from 'react'; +import * as React from 'react'; +import {memo, useCallback} from 'react'; import styles from './ListItem.css'; import type {Item} from './List'; diff --git a/packages/react-devtools-shell/src/app/Toggle/index.js b/packages/react-devtools-shell/src/app/Toggle/index.js index 9740c797de88b..98b816d8837dd 100644 --- a/packages/react-devtools-shell/src/app/Toggle/index.js +++ b/packages/react-devtools-shell/src/app/Toggle/index.js @@ -1,4 +1,5 @@ -import React, {useState} from 'react'; +import * as React from 'react'; +import {useState} from 'react'; export default function Toggle() { const [show, setShow] = useState(false); diff --git a/packages/react-dom/index.classic.fb.js b/packages/react-dom/index.classic.fb.js new file mode 100644 index 0000000000000..96ce6a6cc35c8 --- /dev/null +++ b/packages/react-dom/index.classic.fb.js @@ -0,0 +1,46 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +import {addUserTimingListener} from 'shared/ReactFeatureFlags'; +import {isEnabled} from './src/events/ReactDOMEventListener'; +import {getClosestInstanceFromNode} from './src/client/ReactDOMComponentTree'; + +import {__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED} from './src/client/ReactDOM'; + +// For classic WWW builds, include a few internals that are already in use. +Object.assign((__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED: any), { + ReactBrowserEventEmitter: { + isEnabled, + }, + ReactDOMComponentTree: { + getClosestInstanceFromNode, + }, + // Perf experiment + addUserTimingListener, +}); + +export { + createPortal, + unstable_batchedUpdates, + flushSync, + __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED, + version, + findDOMNode, + hydrate, + render, + unmountComponentAtNode, + createRoot, + createBlockingRoot, + unstable_discreteUpdates, + unstable_flushDiscreteUpdates, + unstable_flushControlled, + unstable_scheduleHydration, + unstable_renderSubtreeIntoContainer, + unstable_createPortal, +} from './src/client/ReactDOM'; diff --git a/packages/react-dom/index.experimental.js b/packages/react-dom/index.experimental.js new file mode 100644 index 0000000000000..b771d0105b8e5 --- /dev/null +++ b/packages/react-dom/index.experimental.js @@ -0,0 +1,34 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +export { + createPortal, + unstable_batchedUpdates, + flushSync, + __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED, + version, + // Disabled behind disableLegacyReactDOMAPIs + findDOMNode, + hydrate, + render, + unmountComponentAtNode, + // exposeConcurrentModeAPIs + createRoot, + createBlockingRoot, + unstable_discreteUpdates, + unstable_flushDiscreteUpdates, + unstable_flushControlled, + unstable_scheduleHydration, + // Disabled behind disableUnstableRenderSubtreeIntoContainer + unstable_renderSubtreeIntoContainer, + // Disabled behind disableUnstableCreatePortal + // Temporary alias since we already shipped React 16 RC with it. + // TODO: remove in React 17. + unstable_createPortal, +} from './src/client/ReactDOM'; diff --git a/packages/react-dom/index.fb.js b/packages/react-dom/index.fb.js deleted file mode 100644 index ea901748d6949..0000000000000 --- a/packages/react-dom/index.fb.js +++ /dev/null @@ -1,14 +0,0 @@ -/** - * Copyright (c) Facebook, Inc. and its affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */ - -'use strict'; - -const ReactDOMFB = require('./src/client/ReactDOMFB'); - -// TODO: decide on the top-level export form. -// This is hacky but makes it work with both Rollup and Jest. -module.exports = ReactDOMFB.default || ReactDOMFB; diff --git a/packages/react-dom/index.js b/packages/react-dom/index.js index 2a016ba16e9db..3714de11244ff 100644 --- a/packages/react-dom/index.js +++ b/packages/react-dom/index.js @@ -7,10 +7,4 @@ * @flow */ -'use strict'; - -const ReactDOM = require('./src/client/ReactDOM'); - -// TODO: decide on the top-level export form. -// This is hacky but makes it work with both Rollup and Jest. -module.exports = ReactDOM.default || ReactDOM; +export * from './src/client/ReactDOM'; diff --git a/packages/react-dom/index.modern.fb.js b/packages/react-dom/index.modern.fb.js new file mode 100644 index 0000000000000..bcb213ad4ee35 --- /dev/null +++ b/packages/react-dom/index.modern.fb.js @@ -0,0 +1,22 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +export { + createPortal, + unstable_batchedUpdates, + flushSync, + __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED, + version, + createRoot, + createBlockingRoot, + unstable_discreteUpdates, + unstable_flushDiscreteUpdates, + unstable_flushControlled, + unstable_scheduleHydration, +} from './src/client/ReactDOM'; diff --git a/packages/react-dom/index.stable.js b/packages/react-dom/index.stable.js new file mode 100644 index 0000000000000..00d52806bab59 --- /dev/null +++ b/packages/react-dom/index.stable.js @@ -0,0 +1,24 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +export { + createPortal, + unstable_batchedUpdates, + flushSync, + __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED, + version, + findDOMNode, + hydrate, + render, + unmountComponentAtNode, + unstable_renderSubtreeIntoContainer, + // Temporary alias since we already shipped React 16 RC with it. + // TODO: remove in React 17. + unstable_createPortal, +} from './src/client/ReactDOM'; diff --git a/packages/react-dom/package.json b/packages/react-dom/package.json index 7e6cf5e5c0ee8..fb5f49740caf4 100644 --- a/packages/react-dom/package.json +++ b/packages/react-dom/package.json @@ -1,6 +1,6 @@ { "name": "react-dom", - "version": "16.12.0", + "version": "16.13.0", "description": "React package for working with the DOM.", "main": "index.js", "repository": { @@ -19,8 +19,7 @@ "dependencies": { "loose-envify": "^1.1.0", "object-assign": "^4.1.1", - "prop-types": "^15.6.2", - "scheduler": "^0.18.0" + "scheduler": "^0.19.0" }, "peerDependencies": { "react": "^16.0.0" diff --git a/packages/react-dom/server.browser.js b/packages/react-dom/server.browser.js index df45b9fa9fd63..c57063649a9f0 100644 --- a/packages/react-dom/server.browser.js +++ b/packages/react-dom/server.browser.js @@ -7,10 +7,10 @@ * @flow */ -'use strict'; - -const ReactDOMServer = require('./src/server/ReactDOMServerBrowser'); - -// TODO: decide on the top-level export form. -// This is hacky but makes it work with both Rollup and Jest -module.exports = ReactDOMServer.default || ReactDOMServer; +export { + renderToString, + renderToStaticMarkup, + renderToNodeStream, + renderToStaticNodeStream, + version, +} from './src/server/ReactDOMServerBrowser'; diff --git a/packages/react-dom/server.js b/packages/react-dom/server.js index 03006336ba4fe..6010f4e3d5b22 100644 --- a/packages/react-dom/server.js +++ b/packages/react-dom/server.js @@ -7,6 +7,4 @@ * @flow */ -'use strict'; - -module.exports = require('./server.node'); +export * from './server.node'; diff --git a/packages/react-dom/server.node.js b/packages/react-dom/server.node.js index 66e1ea1f0f9ad..e610a8818f961 100644 --- a/packages/react-dom/server.node.js +++ b/packages/react-dom/server.node.js @@ -7,10 +7,11 @@ * @flow */ -'use strict'; - -const ReactDOMServer = require('./src/server/ReactDOMServerNode'); - -// TODO: decide on the top-level export form. -// This is hacky but makes it work with both Rollup and Jest -module.exports = ReactDOMServer.default || ReactDOMServer; +// For some reason Flow doesn't like export * in this file. I don't know why. +export { + renderToString, + renderToStaticMarkup, + renderToNodeStream, + renderToStaticNodeStream, + version, +} from './src/server/ReactDOMServerNode'; diff --git a/packages/react-dom/src/__tests__/EventPluginHub-test.js b/packages/react-dom/src/__tests__/InvalidEventListeners-test.js similarity index 96% rename from packages/react-dom/src/__tests__/EventPluginHub-test.js rename to packages/react-dom/src/__tests__/InvalidEventListeners-test.js index df7859fa6b175..dc5f0e694fe1e 100644 --- a/packages/react-dom/src/__tests__/EventPluginHub-test.js +++ b/packages/react-dom/src/__tests__/InvalidEventListeners-test.js @@ -11,7 +11,7 @@ jest.mock('../events/isEventSupported'); -describe('EventPluginHub', () => { +describe('InvalidEventListeners', () => { let React; let ReactTestUtils; diff --git a/packages/react-dom/src/__tests__/ReactBrowserEventEmitter-test.internal.js b/packages/react-dom/src/__tests__/ReactBrowserEventEmitter-test.internal.js index 57348fa556da1..64099b4b8fed4 100644 --- a/packages/react-dom/src/__tests__/ReactBrowserEventEmitter-test.internal.js +++ b/packages/react-dom/src/__tests__/ReactBrowserEventEmitter-test.internal.js @@ -9,12 +9,13 @@ 'use strict'; -let EventPluginHub; +let EventPluginGetListener; let EventPluginRegistry; let React; let ReactDOM; let ReactDOMComponentTree; -let ReactBrowserEventEmitter; +let listenToEvent; +let ReactDOMEventListener; let ReactTestUtils; let idCallOrder; @@ -52,18 +53,21 @@ function registerSimpleTestHandler() { return getListener(CHILD, ON_CLICK_KEY); } +// We should probably remove this file at some point, it's just full of +// internal API usage. describe('ReactBrowserEventEmitter', () => { beforeEach(() => { jest.resetModules(); LISTENER.mockClear(); - // TODO: can we express this test with only public API? - EventPluginHub = require('legacy-events/EventPluginHub'); + EventPluginGetListener = require('legacy-events/getListener').default; EventPluginRegistry = require('legacy-events/EventPluginRegistry'); React = require('react'); ReactDOM = require('react-dom'); ReactDOMComponentTree = require('../client/ReactDOMComponentTree'); - ReactBrowserEventEmitter = require('../events/ReactBrowserEventEmitter'); + listenToEvent = require('../events/DOMLegacyEventPluginSystem') + .legacyListenToEvent; + ReactDOMEventListener = require('../events/ReactDOMEventListener'); ReactTestUtils = require('react-dom/test-utils'); container = document.createElement('div'); @@ -100,7 +104,7 @@ describe('ReactBrowserEventEmitter', () => { getListener = function(node, eventName) { const inst = ReactDOMComponentTree.getInstanceFromNode(node); - return EventPluginHub.getListener(inst, eventName); + return EventPluginGetListener(inst, eventName); }; putListener = function(node, eventName, listener) { switch (node) { @@ -177,12 +181,12 @@ describe('ReactBrowserEventEmitter', () => { expect(LISTENER).toHaveBeenCalledTimes(1); }); - it('should not invoke handlers if ReactBrowserEventEmitter is disabled', () => { + it('should not invoke handlers if ReactDOMEventListener is disabled', () => { registerSimpleTestHandler(); - ReactBrowserEventEmitter.setEnabled(false); + ReactDOMEventListener.setEnabled(false); CHILD.click(); expect(LISTENER).toHaveBeenCalledTimes(0); - ReactBrowserEventEmitter.setEnabled(true); + ReactDOMEventListener.setEnabled(true); CHILD.click(); expect(LISTENER).toHaveBeenCalledTimes(1); }); @@ -346,15 +350,15 @@ describe('ReactBrowserEventEmitter', () => { it('should listen to events only once', () => { spyOnDevAndProd(EventTarget.prototype, 'addEventListener'); - ReactBrowserEventEmitter.listenTo(ON_CLICK_KEY, document); - ReactBrowserEventEmitter.listenTo(ON_CLICK_KEY, document); + listenToEvent(ON_CLICK_KEY, document); + listenToEvent(ON_CLICK_KEY, document); expect(EventTarget.prototype.addEventListener).toHaveBeenCalledTimes(1); }); it('should work with event plugins without dependencies', () => { spyOnDevAndProd(EventTarget.prototype, 'addEventListener'); - ReactBrowserEventEmitter.listenTo(ON_CLICK_KEY, document); + listenToEvent(ON_CLICK_KEY, document); expect(EventTarget.prototype.addEventListener.calls.argsFor(0)[0]).toBe( 'click', @@ -364,7 +368,7 @@ describe('ReactBrowserEventEmitter', () => { it('should work with event plugins with dependencies', () => { spyOnDevAndProd(EventTarget.prototype, 'addEventListener'); - ReactBrowserEventEmitter.listenTo(ON_CHANGE_KEY, document); + listenToEvent(ON_CHANGE_KEY, document); const setEventListeners = []; const listenCalls = EventTarget.prototype.addEventListener.calls.allArgs(); diff --git a/packages/react-dom/src/__tests__/ReactCompositeComponent-test.js b/packages/react-dom/src/__tests__/ReactCompositeComponent-test.js index 908d418cac7ef..49e107f877ef5 100644 --- a/packages/react-dom/src/__tests__/ReactCompositeComponent-test.js +++ b/packages/react-dom/src/__tests__/ReactCompositeComponent-test.js @@ -493,43 +493,6 @@ describe('ReactCompositeComponent', () => { ReactDOM.render(, container); }); - it('should warn about `setState` in getChildContext', () => { - const container = document.createElement('div'); - - let renderPasses = 0; - - class Component extends React.Component { - state = {value: 0}; - - getChildContext() { - if (this.state.value === 0) { - this.setState({value: 1}); - } - } - - render() { - renderPasses++; - return
; - } - } - Component.childContextTypes = {}; - - let instance; - - expect(() => { - instance = ReactDOM.render(, container); - }).toErrorDev( - 'Warning: setState(...): Cannot call setState() inside getChildContext()', - ); - - expect(renderPasses).toBe(2); - expect(instance.state.value).toBe(1); - - // Test deduplication; (no additional warnings are expected). - ReactDOM.unmountComponentAtNode(container); - ReactDOM.render(, container); - }); - it('should cleanup even if render() fatals', () => { class BadComponent extends React.Component { render() { diff --git a/packages/react-dom/src/__tests__/ReactDOMComponent-test.js b/packages/react-dom/src/__tests__/ReactDOMComponent-test.js index 7a0e286399528..1b9e644848894 100644 --- a/packages/react-dom/src/__tests__/ReactDOMComponent-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMComponent-test.js @@ -1242,53 +1242,6 @@ describe('ReactDOMComponent', () => { ); }); - it('should emit a warning once for a named custom component using shady DOM', () => { - const defaultCreateElement = document.createElement.bind(document); - - try { - document.createElement = element => { - const container = defaultCreateElement(element); - container.shadyRoot = {}; - return container; - }; - class ShadyComponent extends React.Component { - render() { - return ; - } - } - const node = document.createElement('div'); - expect(() => ReactDOM.render(, node)).toErrorDev( - 'ShadyComponent is using shady DOM. Using shady DOM with React can ' + - 'cause things to break subtly.', - ); - mountComponent({is: 'custom-shady-div2'}); - } finally { - document.createElement = defaultCreateElement; - } - }); - - it('should emit a warning once for an unnamed custom component using shady DOM', () => { - const defaultCreateElement = document.createElement.bind(document); - - try { - document.createElement = element => { - const container = defaultCreateElement(element); - container.shadyRoot = {}; - return container; - }; - - expect(() => mountComponent({is: 'custom-shady-div'})).toErrorDev( - 'A component is using shady DOM. Using shady DOM with React can ' + - 'cause things to break subtly.', - ); - - // No additional warnings are expected - mountComponent({is: 'custom-shady-div2'}); - } finally { - document.createElement = defaultCreateElement; - } - }); - it('should treat menuitem as a void element but still create the closing tag', () => { // menuitem is not implemented in jsdom, so this triggers the unknown warning error const container = document.createElement('div'); diff --git a/packages/react-dom/src/__tests__/ReactDOMFiber-test.js b/packages/react-dom/src/__tests__/ReactDOMFiber-test.js index efc605a9edb3a..ef872dfeb47e0 100644 --- a/packages/react-dom/src/__tests__/ReactDOMFiber-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMFiber-test.js @@ -13,8 +13,6 @@ const React = require('react'); const ReactDOM = require('react-dom'); const PropTypes = require('prop-types'); -const ReactFeatureFlags = require('shared/ReactFeatureFlags'); - describe('ReactDOMFiber', () => { let container; @@ -249,7 +247,7 @@ describe('ReactDOMFiber', () => { }); // TODO: remove in React 17 - if (!ReactFeatureFlags.disableUnstableCreatePortal) { + if (!__EXPERIMENTAL__) { it('should support unstable_createPortal alias', () => { const portalContainer = document.createElement('div'); diff --git a/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js b/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js index 7a25979987b4f..f6f7c4979bc91 100644 --- a/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js +++ b/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js @@ -86,8 +86,6 @@ describe('ReactDOMServerPartialHydration', () => { Scheduler = require('scheduler'); Suspense = React.Suspense; SuspenseList = React.SuspenseList; - - useHover = require('react-interactions/events/hover').useHover; }); if (!__EXPERIMENTAL__) { @@ -2368,120 +2366,124 @@ describe('ReactDOMServerPartialHydration', () => { document.body.removeChild(container); }); - it('blocks only on the last continuous event (Responder system)', async () => { - let suspend1 = false; - let resolve1; - let promise1 = new Promise(resolvePromise => (resolve1 = resolvePromise)); - let suspend2 = false; - let resolve2; - let promise2 = new Promise(resolvePromise => (resolve2 = resolvePromise)); - - function First({text}) { - if (suspend1) { - throw promise1; - } else { - return 'Hello'; + if (__EXPERIMENTAL__) { + it('blocks only on the last continuous event (Responder system)', async () => { + useHover = require('react-interactions/events/hover').useHover; + + let suspend1 = false; + let resolve1; + let promise1 = new Promise(resolvePromise => (resolve1 = resolvePromise)); + let suspend2 = false; + let resolve2; + let promise2 = new Promise(resolvePromise => (resolve2 = resolvePromise)); + + function First({text}) { + if (suspend1) { + throw promise1; + } else { + return 'Hello'; + } } - } - function Second({text}) { - if (suspend2) { - throw promise2; - } else { - return 'World'; + function Second({text}) { + if (suspend2) { + throw promise2; + } else { + return 'World'; + } } - } - - let ops = []; - function App() { - const listener1 = useHover({ - onHoverStart() { - ops.push('Hover Start First'); - }, - onHoverEnd() { - ops.push('Hover End First'); - }, - }); - const listener2 = useHover({ - onHoverStart() { - ops.push('Hover Start Second'); - }, - onHoverEnd() { - ops.push('Hover End Second'); - }, - }); - return ( -
- - - {/* We suspend after to test what happens when we eager + let ops = []; + + function App() { + const listener1 = useHover({ + onHoverStart() { + ops.push('Hover Start First'); + }, + onHoverEnd() { + ops.push('Hover End First'); + }, + }); + const listener2 = useHover({ + onHoverStart() { + ops.push('Hover Start Second'); + }, + onHoverEnd() { + ops.push('Hover End Second'); + }, + }); + return ( +
+ + + {/* We suspend after to test what happens when we eager attach the listener. */} - - - - - - - -
- ); - } + +
+ + + + + +
+ ); + } - let finalHTML = ReactDOMServer.renderToString(); - let container = document.createElement('div'); - container.innerHTML = finalHTML; + let finalHTML = ReactDOMServer.renderToString(); + let container = document.createElement('div'); + container.innerHTML = finalHTML; - // We need this to be in the document since we'll dispatch events on it. - document.body.appendChild(container); + // We need this to be in the document since we'll dispatch events on it. + document.body.appendChild(container); - let appDiv = container.getElementsByTagName('div')[0]; - let firstSpan = appDiv.getElementsByTagName('span')[0]; - let secondSpan = appDiv.getElementsByTagName('span')[1]; - expect(firstSpan.textContent).toBe(''); - expect(secondSpan.textContent).toBe('World'); + let appDiv = container.getElementsByTagName('div')[0]; + let firstSpan = appDiv.getElementsByTagName('span')[0]; + let secondSpan = appDiv.getElementsByTagName('span')[1]; + expect(firstSpan.textContent).toBe(''); + expect(secondSpan.textContent).toBe('World'); - // On the client we don't have all data yet but we want to start - // hydrating anyway. - suspend1 = true; - suspend2 = true; - let root = ReactDOM.createRoot(container, {hydrate: true}); - root.render(); + // On the client we don't have all data yet but we want to start + // hydrating anyway. + suspend1 = true; + suspend2 = true; + let root = ReactDOM.createRoot(container, {hydrate: true}); + root.render(); - Scheduler.unstable_flushAll(); - jest.runAllTimers(); + Scheduler.unstable_flushAll(); + jest.runAllTimers(); - dispatchMouseEvent(appDiv, null); - dispatchMouseEvent(firstSpan, appDiv); - dispatchMouseEvent(secondSpan, firstSpan); + dispatchMouseEvent(appDiv, null); + dispatchMouseEvent(firstSpan, appDiv); + dispatchMouseEvent(secondSpan, firstSpan); - // Neither target is yet hydrated. - expect(ops).toEqual([]); + // Neither target is yet hydrated. + expect(ops).toEqual([]); - // Resolving the second promise so that rendering can complete. - suspend2 = false; - resolve2(); - await promise2; + // Resolving the second promise so that rendering can complete. + suspend2 = false; + resolve2(); + await promise2; - Scheduler.unstable_flushAll(); - jest.runAllTimers(); + Scheduler.unstable_flushAll(); + jest.runAllTimers(); - // We've unblocked the current hover target so we should be - // able to replay it now. - expect(ops).toEqual(['Hover Start Second']); + // We've unblocked the current hover target so we should be + // able to replay it now. + expect(ops).toEqual(['Hover Start Second']); - // Resolving the first promise has no effect now. - suspend1 = false; - resolve1(); - await promise1; + // Resolving the first promise has no effect now. + suspend1 = false; + resolve1(); + await promise1; - Scheduler.unstable_flushAll(); - jest.runAllTimers(); + Scheduler.unstable_flushAll(); + jest.runAllTimers(); - expect(ops).toEqual(['Hover Start Second']); + expect(ops).toEqual(['Hover Start Second']); - document.body.removeChild(container); - }); + document.body.removeChild(container); + }); + } it('finishes normal pri work before continuing to hydrate a retry', async () => { let suspend = false; diff --git a/packages/react-dom/src/__tests__/ReactDOMServerSelectiveHydration-test.internal.js b/packages/react-dom/src/__tests__/ReactDOMServerSelectiveHydration-test.internal.js index 02d1dff1a9ec6..3fb9169c1dc6e 100644 --- a/packages/react-dom/src/__tests__/ReactDOMServerSelectiveHydration-test.internal.js +++ b/packages/react-dom/src/__tests__/ReactDOMServerSelectiveHydration-test.internal.js @@ -17,7 +17,6 @@ let ReactDOMServer; let ReactTestUtils; let Scheduler; let Suspense; -let usePress; function dispatchMouseHoverEvent(to, from) { if (!to) { @@ -106,7 +105,6 @@ describe('ReactDOMServerSelectiveHydration', () => { ReactTestUtils = require('react-dom/test-utils'); Scheduler = require('scheduler'); Suspense = React.Suspense; - usePress = require('react-interactions/events/press').usePress; }); if (!__EXPERIMENTAL__) { @@ -352,240 +350,246 @@ describe('ReactDOMServerSelectiveHydration', () => { document.body.removeChild(container); }); - it('hydrates the target boundary synchronously during a click (flare)', async () => { - function Child({text}) { - Scheduler.unstable_yieldValue(text); - const listener = usePress({ - onPress() { - Scheduler.unstable_yieldValue('Clicked ' + text); - }, - }); + if (__EXPERIMENTAL__) { + it('hydrates the target boundary synchronously during a click (flare)', async () => { + let usePress = require('react-interactions/events/press').usePress; - return {text}; - } + function Child({text}) { + Scheduler.unstable_yieldValue(text); + const listener = usePress({ + onPress() { + Scheduler.unstable_yieldValue('Clicked ' + text); + }, + }); - function App() { - Scheduler.unstable_yieldValue('App'); - return ( -
- - - - - - -
- ); - } + return {text}; + } - let finalHTML = ReactDOMServer.renderToString(); + function App() { + Scheduler.unstable_yieldValue('App'); + return ( +
+ + + + + + +
+ ); + } - expect(Scheduler).toHaveYielded(['App', 'A', 'B']); + let finalHTML = ReactDOMServer.renderToString(); - let container = document.createElement('div'); - // We need this to be in the document since we'll dispatch events on it. - document.body.appendChild(container); + expect(Scheduler).toHaveYielded(['App', 'A', 'B']); - container.innerHTML = finalHTML; + let container = document.createElement('div'); + // We need this to be in the document since we'll dispatch events on it. + document.body.appendChild(container); - let root = ReactDOM.createRoot(container, {hydrate: true}); - root.render(); + container.innerHTML = finalHTML; - // Nothing has been hydrated so far. - expect(Scheduler).toHaveYielded([]); + let root = ReactDOM.createRoot(container, {hydrate: true}); + root.render(); - let span = container.getElementsByTagName('span')[1]; + // Nothing has been hydrated so far. + expect(Scheduler).toHaveYielded([]); - let target = createEventTarget(span); + let span = container.getElementsByTagName('span')[1]; - // This should synchronously hydrate the root App and the second suspense - // boundary. - let preventDefault = jest.fn(); - target.virtualclick({preventDefault}); + let target = createEventTarget(span); - // The event should have been canceled because we called preventDefault. - expect(preventDefault).toHaveBeenCalled(); + // This should synchronously hydrate the root App and the second suspense + // boundary. + let preventDefault = jest.fn(); + target.virtualclick({preventDefault}); - // We rendered App, B and then invoked the event without rendering A. - expect(Scheduler).toHaveYielded(['App', 'B', 'Clicked B']); + // The event should have been canceled because we called preventDefault. + expect(preventDefault).toHaveBeenCalled(); - // After continuing the scheduler, we finally hydrate A. - expect(Scheduler).toFlushAndYield(['A']); + // We rendered App, B and then invoked the event without rendering A. + expect(Scheduler).toHaveYielded(['App', 'B', 'Clicked B']); - document.body.removeChild(container); - }); + // After continuing the scheduler, we finally hydrate A. + expect(Scheduler).toFlushAndYield(['A']); - it('hydrates at higher pri if sync did not work first time (flare)', async () => { - let suspend = false; - let resolve; - let promise = new Promise(resolvePromise => (resolve = resolvePromise)); + document.body.removeChild(container); + }); - function Child({text}) { - if ((text === 'A' || text === 'D') && suspend) { - throw promise; + it('hydrates at higher pri if sync did not work first time (flare)', async () => { + let usePress = require('react-interactions/events/press').usePress; + let suspend = false; + let resolve; + let promise = new Promise(resolvePromise => (resolve = resolvePromise)); + + function Child({text}) { + if ((text === 'A' || text === 'D') && suspend) { + throw promise; + } + Scheduler.unstable_yieldValue(text); + + const listener = usePress({ + onPress() { + Scheduler.unstable_yieldValue('Clicked ' + text); + }, + }); + return {text}; } - Scheduler.unstable_yieldValue(text); - const listener = usePress({ - onPress() { - Scheduler.unstable_yieldValue('Clicked ' + text); - }, - }); - return {text}; - } + function App() { + Scheduler.unstable_yieldValue('App'); + return ( +
+ + + + + + + + + + + + +
+ ); + } - function App() { - Scheduler.unstable_yieldValue('App'); - return ( -
- - - - - - - - - - - - -
- ); - } + let finalHTML = ReactDOMServer.renderToString(); - let finalHTML = ReactDOMServer.renderToString(); + expect(Scheduler).toHaveYielded(['App', 'A', 'B', 'C', 'D']); - expect(Scheduler).toHaveYielded(['App', 'A', 'B', 'C', 'D']); + let container = document.createElement('div'); + // We need this to be in the document since we'll dispatch events on it. + document.body.appendChild(container); - let container = document.createElement('div'); - // We need this to be in the document since we'll dispatch events on it. - document.body.appendChild(container); + container.innerHTML = finalHTML; - container.innerHTML = finalHTML; + let spanD = container.getElementsByTagName('span')[3]; - let spanD = container.getElementsByTagName('span')[3]; + suspend = true; - suspend = true; + // A and D will be suspended. We'll click on D which should take + // priority, after we unsuspend. + let root = ReactDOM.createRoot(container, {hydrate: true}); + root.render(); - // A and D will be suspended. We'll click on D which should take - // priority, after we unsuspend. - let root = ReactDOM.createRoot(container, {hydrate: true}); - root.render(); + // Nothing has been hydrated so far. + expect(Scheduler).toHaveYielded([]); - // Nothing has been hydrated so far. - expect(Scheduler).toHaveYielded([]); + // This click target cannot be hydrated yet because it's suspended. + let result = dispatchClickEvent(spanD); - // This click target cannot be hydrated yet because it's suspended. - let result = dispatchClickEvent(spanD); + expect(Scheduler).toHaveYielded(['App']); - expect(Scheduler).toHaveYielded(['App']); + expect(result).toBe(true); - expect(result).toBe(true); + // Continuing rendering will render B next. + expect(Scheduler).toFlushAndYield(['B', 'C']); - // Continuing rendering will render B next. - expect(Scheduler).toFlushAndYield(['B', 'C']); + suspend = false; + resolve(); + await promise; - suspend = false; - resolve(); - await promise; + // After the click, we should prioritize D and the Click first, + // and only after that render A and C. + expect(Scheduler).toFlushAndYield(['D', 'Clicked D', 'A']); - // After the click, we should prioritize D and the Click first, - // and only after that render A and C. - expect(Scheduler).toFlushAndYield(['D', 'Clicked D', 'A']); + document.body.removeChild(container); + }); - document.body.removeChild(container); - }); + it('hydrates at higher pri for secondary discrete events (flare)', async () => { + let usePress = require('react-interactions/events/press').usePress; + let suspend = false; + let resolve; + let promise = new Promise(resolvePromise => (resolve = resolvePromise)); - it('hydrates at higher pri for secondary discrete events (flare)', async () => { - let suspend = false; - let resolve; - let promise = new Promise(resolvePromise => (resolve = resolvePromise)); + function Child({text}) { + if ((text === 'A' || text === 'D') && suspend) { + throw promise; + } + Scheduler.unstable_yieldValue(text); - function Child({text}) { - if ((text === 'A' || text === 'D') && suspend) { - throw promise; + const listener = usePress({ + onPress() { + Scheduler.unstable_yieldValue('Clicked ' + text); + }, + }); + return {text}; } - Scheduler.unstable_yieldValue(text); - const listener = usePress({ - onPress() { - Scheduler.unstable_yieldValue('Clicked ' + text); - }, - }); - return {text}; - } + function App() { + Scheduler.unstable_yieldValue('App'); + return ( +
+ + + + + + + + + + + + +
+ ); + } - function App() { - Scheduler.unstable_yieldValue('App'); - return ( -
- - - - - - - - - - - - -
- ); - } + let finalHTML = ReactDOMServer.renderToString(); - let finalHTML = ReactDOMServer.renderToString(); + expect(Scheduler).toHaveYielded(['App', 'A', 'B', 'C', 'D']); - expect(Scheduler).toHaveYielded(['App', 'A', 'B', 'C', 'D']); + let container = document.createElement('div'); + // We need this to be in the document since we'll dispatch events on it. + document.body.appendChild(container); - let container = document.createElement('div'); - // We need this to be in the document since we'll dispatch events on it. - document.body.appendChild(container); + container.innerHTML = finalHTML; - container.innerHTML = finalHTML; + let spanA = container.getElementsByTagName('span')[0]; + let spanC = container.getElementsByTagName('span')[2]; + let spanD = container.getElementsByTagName('span')[3]; - let spanA = container.getElementsByTagName('span')[0]; - let spanC = container.getElementsByTagName('span')[2]; - let spanD = container.getElementsByTagName('span')[3]; + suspend = true; - suspend = true; + // A and D will be suspended. We'll click on D which should take + // priority, after we unsuspend. + let root = ReactDOM.createRoot(container, {hydrate: true}); + root.render(); - // A and D will be suspended. We'll click on D which should take - // priority, after we unsuspend. - let root = ReactDOM.createRoot(container, {hydrate: true}); - root.render(); + // Nothing has been hydrated so far. + expect(Scheduler).toHaveYielded([]); - // Nothing has been hydrated so far. - expect(Scheduler).toHaveYielded([]); + // This click target cannot be hydrated yet because the first is Suspended. + dispatchClickEvent(spanA); + dispatchClickEvent(spanC); + dispatchClickEvent(spanD); - // This click target cannot be hydrated yet because the first is Suspended. - dispatchClickEvent(spanA); - dispatchClickEvent(spanC); - dispatchClickEvent(spanD); + expect(Scheduler).toHaveYielded(['App']); - expect(Scheduler).toHaveYielded(['App']); + suspend = false; + resolve(); + await promise; - suspend = false; - resolve(); - await promise; - - // We should prioritize hydrating A, C and D first since we clicked in - // them. Only after they're done will we hydrate B. - expect(Scheduler).toFlushAndYield([ - 'A', - 'Clicked A', - 'C', - 'Clicked C', - 'D', - 'Clicked D', - // B should render last since it wasn't clicked. - 'B', - ]); + // We should prioritize hydrating A, C and D first since we clicked in + // them. Only after they're done will we hydrate B. + expect(Scheduler).toFlushAndYield([ + 'A', + 'Clicked A', + 'C', + 'Clicked C', + 'D', + 'Clicked D', + // B should render last since it wasn't clicked. + 'B', + ]); - document.body.removeChild(container); - }); + document.body.removeChild(container); + }); + } it('hydrates the hovered targets as higher priority for continuous events', async () => { let suspend = false; diff --git a/packages/react-dom/src/__tests__/ReactFunctionComponent-test.js b/packages/react-dom/src/__tests__/ReactFunctionComponent-test.js index 2b33321358ffd..c80a05c2113be 100644 --- a/packages/react-dom/src/__tests__/ReactFunctionComponent-test.js +++ b/packages/react-dom/src/__tests__/ReactFunctionComponent-test.js @@ -159,7 +159,7 @@ describe('ReactFunctionComponent', () => { ReactTestUtils.renderIntoDocument(); }).toThrowError( __DEV__ - ? 'Function components cannot have refs.' + ? 'Function components cannot have string refs. We recommend using useRef() instead.' : // It happens because we don't save _owner in production for // function components. 'Element ref was specified as a string (me) but no owner was set. This could happen for one of' + diff --git a/packages/react-dom/src/__tests__/ReactTestUtils-test.js b/packages/react-dom/src/__tests__/ReactTestUtils-test.js index 540f23edd17c0..b0ac326e9573e 100644 --- a/packages/react-dom/src/__tests__/ReactTestUtils-test.js +++ b/packages/react-dom/src/__tests__/ReactTestUtils-test.js @@ -9,11 +9,11 @@ 'use strict'; -let createRenderer; -let React; -let ReactDOM; -let ReactDOMServer; -let ReactTestUtils; +import ReactShallowRenderer from 'react-test-renderer/shallow'; +import * as React from 'react'; +import * as ReactDOM from 'react-dom'; +import * as ReactDOMServer from 'react-dom/server'; +import * as ReactTestUtils from 'react-dom/test-utils'; function getTestDocument(markup) { const doc = document.implementation.createHTMLDocument(''); @@ -27,14 +27,6 @@ function getTestDocument(markup) { } describe('ReactTestUtils', () => { - beforeEach(() => { - createRenderer = require('react-test-renderer/shallow').createRenderer; - React = require('react'); - ReactDOM = require('react-dom'); - ReactDOMServer = require('react-dom/server'); - ReactTestUtils = require('react-dom/test-utils'); - }); - it('Simulate should have locally attached media events', () => { expect(Object.keys(ReactTestUtils.Simulate).sort()).toMatchSnapshot(); }); @@ -403,7 +395,7 @@ describe('ReactTestUtils', () => { } const handler = jest.fn().mockName('spy'); - const shallowRenderer = createRenderer(); + const shallowRenderer = ReactShallowRenderer.createRenderer(); const result = shallowRenderer.render( , ); diff --git a/packages/react-dom/src/__tests__/renderSubtreeIntoContainer-test.js b/packages/react-dom/src/__tests__/renderSubtreeIntoContainer-test.js index 2a540f9929d14..678f96de9f080 100644 --- a/packages/react-dom/src/__tests__/renderSubtreeIntoContainer-test.js +++ b/packages/react-dom/src/__tests__/renderSubtreeIntoContainer-test.js @@ -19,7 +19,7 @@ const renderSubtreeIntoContainer = require('react-dom') const ReactFeatureFlags = require('shared/ReactFeatureFlags'); // Once this flag is always true, we should delete this test file -if (ReactFeatureFlags.disableUnstableRenderSubtreeIntoContainer) { +if (__EXPERIMENTAL__) { describe('renderSubtreeIntoContainer', () => { it('empty test', () => { // Empty test to prevent "Your test suite must contain at least one test." error. diff --git a/packages/react-dom/src/client/ReactDOM.js b/packages/react-dom/src/client/ReactDOM.js index ee54497f232af..340cba54773e3 100644 --- a/packages/react-dom/src/client/ReactDOM.js +++ b/packages/react-dom/src/client/ReactDOM.js @@ -7,8 +7,8 @@ * @flow */ -import type {RootType} from './ReactDOMRoot'; import type {ReactNodeList} from 'shared/ReactTypes'; +import type {Container} from './ReactDOMHostConfig'; import '../shared/checkReact'; import './ReactDOMClientInjection'; @@ -35,7 +35,6 @@ import { attemptUserBlockingHydration, attemptContinuousHydration, attemptHydrationAtCurrentPriority, - act, } from 'react-reconciler/inline.dom'; import {createPortal as createPortalImpl} from 'shared/ReactPortal'; import {canUseDOM} from 'shared/ExecutionEnvironment'; @@ -45,22 +44,18 @@ import { enqueueStateRestore, restoreStateIfNeeded, } from 'legacy-events/ReactControlledComponent'; -import {injection as EventPluginHubInjection} from 'legacy-events/EventPluginHub'; import {runEventsInBatch} from 'legacy-events/EventBatching'; -import {eventNameDispatchConfigs} from 'legacy-events/EventPluginRegistry'; +import { + eventNameDispatchConfigs, + injectEventPluginsByName, +} from 'legacy-events/EventPluginRegistry'; import { accumulateTwoPhaseDispatches, accumulateDirectDispatches, } from 'legacy-events/EventPropagators'; import ReactVersion from 'shared/ReactVersion'; import invariant from 'shared/invariant'; -import { - exposeConcurrentModeAPIs, - disableUnstableCreatePortal, - disableUnstableRenderSubtreeIntoContainer, - warnUnstableRenderSubtreeIntoContainer, - isTestEnvironment, -} from 'shared/ReactFeatureFlags'; +import {warnUnstableRenderSubtreeIntoContainer} from 'shared/ReactFeatureFlags'; import { getInstanceFromNode, @@ -113,111 +108,117 @@ setBatchingImplementation( batchedEventUpdates, ); -export type DOMContainer = - | (Element & {_reactRootContainer: ?RootType, ...}) - | (Document & {_reactRootContainer: ?RootType, ...}); - function createPortal( children: ReactNodeList, - container: DOMContainer, + container: Container, key: ?string = null, -) { +): React$Portal { invariant( isValidContainer(container), 'Target container is not a DOM element.', ); // TODO: pass ReactDOM portal implementation as third argument + // $FlowFixMe The Flow type is opaque but there's no way to actually create it. return createPortalImpl(children, container, null, key); } -const ReactDOM: Object = { - createPortal, - - // Legacy - findDOMNode, - hydrate, - render, - unmountComponentAtNode, - - unstable_batchedUpdates: batchedUpdates, - - flushSync: flushSync, - - __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED: { - // Keep in sync with ReactDOMUnstableNativeDependencies.js - // ReactTestUtils.js, and ReactTestUtilsAct.js. This is an array for better minification. - Events: [ - getInstanceFromNode, - getNodeFromInstance, - getFiberCurrentPropsFromNode, - EventPluginHubInjection.injectEventPluginsByName, - eventNameDispatchConfigs, - accumulateTwoPhaseDispatches, - accumulateDirectDispatches, - enqueueStateRestore, - restoreStateIfNeeded, - dispatchEvent, - runEventsInBatch, - flushPassiveEffects, - IsThisRendererActing, - ], - }, - - version: ReactVersion, -}; - -if (exposeConcurrentModeAPIs) { - ReactDOM.createRoot = createRoot; - ReactDOM.createBlockingRoot = createBlockingRoot; - - ReactDOM.unstable_discreteUpdates = discreteUpdates; - ReactDOM.unstable_flushDiscreteUpdates = flushDiscreteUpdates; - ReactDOM.unstable_flushControlled = flushControlled; +function scheduleHydration(target: Node) { + if (target) { + queueExplicitHydrationTarget(target); + } +} - ReactDOM.unstable_scheduleHydration = target => { - if (target) { - queueExplicitHydrationTarget(target); +function renderSubtreeIntoContainer( + parentComponent: React$Component, + element: React$Element, + containerNode: Container, + callback: ?Function, +) { + if (__DEV__) { + if ( + warnUnstableRenderSubtreeIntoContainer && + !didWarnAboutUnstableRenderSubtreeIntoContainer + ) { + didWarnAboutUnstableRenderSubtreeIntoContainer = true; + console.warn( + 'ReactDOM.unstable_renderSubtreeIntoContainer() is deprecated ' + + 'and will be removed in a future major release. Consider using ' + + 'React Portals instead.', + ); } - }; + } + return unstable_renderSubtreeIntoContainer( + parentComponent, + element, + containerNode, + callback, + ); } -if (!disableUnstableRenderSubtreeIntoContainer) { - ReactDOM.unstable_renderSubtreeIntoContainer = function(...args) { - if (__DEV__) { - if ( - warnUnstableRenderSubtreeIntoContainer && - !didWarnAboutUnstableRenderSubtreeIntoContainer - ) { - didWarnAboutUnstableRenderSubtreeIntoContainer = true; - console.warn( - 'ReactDOM.unstable_renderSubtreeIntoContainer() is deprecated ' + - 'and will be removed in a future major release. Consider using ' + - 'React Portals instead.', - ); - } +function unstable_createPortal( + children: ReactNodeList, + container: Container, + key: ?string = null, +) { + if (__DEV__) { + if (!didWarnAboutUnstableCreatePortal) { + didWarnAboutUnstableCreatePortal = true; + console.warn( + 'The ReactDOM.unstable_createPortal() alias has been deprecated, ' + + 'and will be removed in React 17+. Update your code to use ' + + 'ReactDOM.createPortal() instead. It has the exact same API, ' + + 'but without the "unstable_" prefix.', + ); } - return unstable_renderSubtreeIntoContainer(...args); - }; + } + return createPortal(children, container, key); } -if (!disableUnstableCreatePortal) { +const Internals = { + // Keep in sync with ReactDOMUnstableNativeDependencies.js + // ReactTestUtils.js, and ReactTestUtilsAct.js. This is an array for better minification. + Events: [ + getInstanceFromNode, + getNodeFromInstance, + getFiberCurrentPropsFromNode, + injectEventPluginsByName, + eventNameDispatchConfigs, + accumulateTwoPhaseDispatches, + accumulateDirectDispatches, + enqueueStateRestore, + restoreStateIfNeeded, + dispatchEvent, + runEventsInBatch, + flushPassiveEffects, + IsThisRendererActing, + ], +}; + +export { + createPortal, + batchedUpdates as unstable_batchedUpdates, + flushSync, + Internals as __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED, + ReactVersion as version, + // Disabled behind disableLegacyReactDOMAPIs + findDOMNode, + hydrate, + render, + unmountComponentAtNode, + // exposeConcurrentModeAPIs + createRoot, + createBlockingRoot, + discreteUpdates as unstable_discreteUpdates, + flushDiscreteUpdates as unstable_flushDiscreteUpdates, + flushControlled as unstable_flushControlled, + scheduleHydration as unstable_scheduleHydration, + // Disabled behind disableUnstableRenderSubtreeIntoContainer + renderSubtreeIntoContainer as unstable_renderSubtreeIntoContainer, + // Disabled behind disableUnstableCreatePortal // Temporary alias since we already shipped React 16 RC with it. // TODO: remove in React 17. - ReactDOM.unstable_createPortal = function(...args) { - if (__DEV__) { - if (!didWarnAboutUnstableCreatePortal) { - didWarnAboutUnstableCreatePortal = true; - console.warn( - 'The ReactDOM.unstable_createPortal() alias has been deprecated, ' + - 'and will be removed in React 17+. Update your code to use ' + - 'ReactDOM.createPortal() instead. It has the exact same API, ' + - 'but without the "unstable_" prefix.', - ); - } - } - return createPortal(...args); - }; -} + unstable_createPortal, +}; const foundDevTools = injectIntoDevTools({ findFiberByHostInstance: getClosestInstanceFromNode, @@ -252,9 +253,3 @@ if (__DEV__) { } } } - -if (isTestEnvironment) { - ReactDOM.act = act; -} - -export default ReactDOM; diff --git a/packages/react-dom/src/client/ReactDOMClientInjection.js b/packages/react-dom/src/client/ReactDOMClientInjection.js index 604c3ff976bb8..4c0ba0d14f939 100644 --- a/packages/react-dom/src/client/ReactDOMClientInjection.js +++ b/packages/react-dom/src/client/ReactDOMClientInjection.js @@ -5,7 +5,6 @@ * LICENSE file in the root directory of this source tree. */ -import {injection as EventPluginHubInjection} from 'legacy-events/EventPluginHub'; import {setComponentTree} from 'legacy-events/EventPluginUtils'; import { @@ -15,15 +14,35 @@ import { } from './ReactDOMComponentTree'; import BeforeInputEventPlugin from '../events/BeforeInputEventPlugin'; import ChangeEventPlugin from '../events/ChangeEventPlugin'; -import DOMEventPluginOrder from '../events/DOMEventPluginOrder'; import EnterLeaveEventPlugin from '../events/EnterLeaveEventPlugin'; import SelectEventPlugin from '../events/SelectEventPlugin'; import SimpleEventPlugin from '../events/SimpleEventPlugin'; +import { + injectEventPluginOrder, + injectEventPluginsByName, +} from 'legacy-events/EventPluginRegistry'; + +/** + * Specifies a deterministic ordering of `EventPlugin`s. A convenient way to + * reason about plugins, without having to package every one of them. This + * is better than having plugins be ordered in the same order that they + * are injected because that ordering would be influenced by the packaging order. + * `ResponderEventPlugin` must occur before `SimpleEventPlugin` so that + * preventing default on events is convenient in `SimpleEventPlugin` handlers. + */ +const DOMEventPluginOrder = [ + 'ResponderEventPlugin', + 'SimpleEventPlugin', + 'EnterLeaveEventPlugin', + 'ChangeEventPlugin', + 'SelectEventPlugin', + 'BeforeInputEventPlugin', +]; /** * Inject modules for resolving DOM hierarchy and plugin ordering. */ -EventPluginHubInjection.injectEventPluginOrder(DOMEventPluginOrder); +injectEventPluginOrder(DOMEventPluginOrder); setComponentTree( getFiberCurrentPropsFromNode, getInstanceFromNode, @@ -34,7 +53,7 @@ setComponentTree( * Some important event plugins included by default (without having to require * them). */ -EventPluginHubInjection.injectEventPluginsByName({ +injectEventPluginsByName({ SimpleEventPlugin: SimpleEventPlugin, EnterLeaveEventPlugin: EnterLeaveEventPlugin, ChangeEventPlugin: ChangeEventPlugin, diff --git a/packages/react-dom/src/client/ReactDOMComponent.js b/packages/react-dom/src/client/ReactDOMComponent.js index e5111ae603562..5cc6981d82aad 100644 --- a/packages/react-dom/src/client/ReactDOMComponent.js +++ b/packages/react-dom/src/client/ReactDOMComponent.js @@ -7,11 +7,10 @@ * @flow */ -// TODO: direct imports like some-package/src/* are bad. Fix me. -import {getCurrentFiberOwnerNameInDevOrNull} from 'react-reconciler/src/ReactCurrentFiber'; import {registrationNameModules} from 'legacy-events/EventPluginRegistry'; import {canUseDOM} from 'shared/ExecutionEnvironment'; import endsWith from 'shared/endsWith'; +import invariant from 'shared/invariant'; import {setListenToResponderEventTypes} from '../events/DeprecatedDOMEventResponderSystem'; import { @@ -57,11 +56,7 @@ import { TOP_SUBMIT, TOP_TOGGLE, } from '../events/DOMTopLevelEventTypes'; -import { - listenTo, - trapBubbledEvent, - getListenerMapForElement, -} from '../events/ReactBrowserEventEmitter'; +import {getListenerMapForElement} from '../events/DOMEventListenerMap'; import { addResponderEventSystemEvent, removeActiveResponderEventSystemEvent, @@ -79,7 +74,12 @@ import { shouldRemoveAttribute, } from '../shared/DOMProperty'; import assertValidProps from '../shared/assertValidProps'; -import {DOCUMENT_NODE, DOCUMENT_FRAGMENT_NODE} from '../shared/HTMLNodeType'; +import { + DOCUMENT_NODE, + DOCUMENT_FRAGMENT_NODE, + ELEMENT_NODE, + COMMENT_NODE, +} from '../shared/HTMLNodeType'; import isCustomComponent from '../shared/isCustomComponent'; import possibleStandardNames from '../shared/possibleStandardNames'; import {validateProperties as validateARIAProperties} from '../shared/ReactDOMInvalidARIAHook'; @@ -89,10 +89,15 @@ import {validateProperties as validateUnknownProperties} from '../shared/ReactDO import { enableDeprecatedFlareAPI, enableTrustedTypesIntegration, + enableModernEventSystem, } from 'shared/ReactFeatureFlags'; +import { + legacyListenToEvent, + legacyTrapBubbledEvent, +} from '../events/DOMLegacyEventPluginSystem'; +import {listenToEvent} from '../events/DOMModernPluginEventSystem'; let didWarnInvalidHydration = false; -let didWarnShadyDOM = false; let didWarnScriptTags = false; const DANGEROUSLY_SET_INNER_HTML = 'dangerouslySetInnerHTML'; @@ -265,16 +270,36 @@ if (__DEV__) { } function ensureListeningTo( - rootContainerElement: Element | Node, + rootContainerInstance: Element | Node, registrationName: string, ): void { - const isDocumentOrFragment = - rootContainerElement.nodeType === DOCUMENT_NODE || - rootContainerElement.nodeType === DOCUMENT_FRAGMENT_NODE; - const doc = isDocumentOrFragment - ? rootContainerElement - : rootContainerElement.ownerDocument; - listenTo(registrationName, doc); + if (enableModernEventSystem) { + // If we have a comment node, then use the parent node, + // which should be an element. + const rootContainerElement = + rootContainerInstance.nodeType === COMMENT_NODE + ? rootContainerInstance.parentNode + : rootContainerInstance; + // Containers can only ever be element nodes. We do not + // want to register events to document fragments or documents + // with the modern plugin event system. + invariant( + rootContainerElement != null && + rootContainerElement.nodeType === ELEMENT_NODE, + 'ensureListeningTo(): received a container that was not an element node. ' + + 'This is likely a bug in React.', + ); + listenToEvent(registrationName, ((rootContainerElement: any): Element)); + } else { + // Legacy plugin event system path + const isDocumentOrFragment = + rootContainerInstance.nodeType === DOCUMENT_NODE || + rootContainerInstance.nodeType === DOCUMENT_FRAGMENT_NODE; + const doc = isDocumentOrFragment + ? rootContainerInstance + : rootContainerInstance.ownerDocument; + legacyListenToEvent(registrationName, ((doc: any): Document)); + } } function getOwnerDocumentFromRootContainer( @@ -511,18 +536,6 @@ export function setInitialProperties( const isCustomComponentTag = isCustomComponent(tag, rawProps); if (__DEV__) { validatePropertiesInDevelopment(tag, rawProps); - if ( - isCustomComponentTag && - !didWarnShadyDOM && - (domElement: any).shadyRoot - ) { - console.error( - '%s is using shady DOM. Using shady DOM with React can ' + - 'cause things to break subtly.', - getCurrentFiberOwnerNameInDevOrNull() || 'A component', - ); - didWarnShadyDOM = true; - } } // TODO: Make sure that we check isMounted before firing any of these events. @@ -531,41 +544,55 @@ export function setInitialProperties( case 'iframe': case 'object': case 'embed': - trapBubbledEvent(TOP_LOAD, domElement); + if (!enableModernEventSystem) { + legacyTrapBubbledEvent(TOP_LOAD, domElement); + } props = rawProps; break; case 'video': case 'audio': - // Create listener for each media event - for (let i = 0; i < mediaEventTypes.length; i++) { - trapBubbledEvent(mediaEventTypes[i], domElement); + if (!enableModernEventSystem) { + // Create listener for each media event + for (let i = 0; i < mediaEventTypes.length; i++) { + legacyTrapBubbledEvent(mediaEventTypes[i], domElement); + } } props = rawProps; break; case 'source': - trapBubbledEvent(TOP_ERROR, domElement); + if (!enableModernEventSystem) { + legacyTrapBubbledEvent(TOP_ERROR, domElement); + } props = rawProps; break; case 'img': case 'image': case 'link': - trapBubbledEvent(TOP_ERROR, domElement); - trapBubbledEvent(TOP_LOAD, domElement); + if (!enableModernEventSystem) { + legacyTrapBubbledEvent(TOP_ERROR, domElement); + legacyTrapBubbledEvent(TOP_LOAD, domElement); + } props = rawProps; break; case 'form': - trapBubbledEvent(TOP_RESET, domElement); - trapBubbledEvent(TOP_SUBMIT, domElement); + if (!enableModernEventSystem) { + legacyTrapBubbledEvent(TOP_RESET, domElement); + legacyTrapBubbledEvent(TOP_SUBMIT, domElement); + } props = rawProps; break; case 'details': - trapBubbledEvent(TOP_TOGGLE, domElement); + if (!enableModernEventSystem) { + legacyTrapBubbledEvent(TOP_TOGGLE, domElement); + } props = rawProps; break; case 'input': ReactDOMInputInitWrapperState(domElement, rawProps); props = ReactDOMInputGetHostProps(domElement, rawProps); - trapBubbledEvent(TOP_INVALID, domElement); + if (!enableModernEventSystem) { + legacyTrapBubbledEvent(TOP_INVALID, domElement); + } // For controlled components we always need to ensure we're listening // to onChange. Even if there is no listener. ensureListeningTo(rootContainerElement, 'onChange'); @@ -577,7 +604,9 @@ export function setInitialProperties( case 'select': ReactDOMSelectInitWrapperState(domElement, rawProps); props = ReactDOMSelectGetHostProps(domElement, rawProps); - trapBubbledEvent(TOP_INVALID, domElement); + if (!enableModernEventSystem) { + legacyTrapBubbledEvent(TOP_INVALID, domElement); + } // For controlled components we always need to ensure we're listening // to onChange. Even if there is no listener. ensureListeningTo(rootContainerElement, 'onChange'); @@ -585,7 +614,9 @@ export function setInitialProperties( case 'textarea': ReactDOMTextareaInitWrapperState(domElement, rawProps); props = ReactDOMTextareaGetHostProps(domElement, rawProps); - trapBubbledEvent(TOP_INVALID, domElement); + if (!enableModernEventSystem) { + legacyTrapBubbledEvent(TOP_INVALID, domElement); + } // For controlled components we always need to ensure we're listening // to onChange. Even if there is no listener. ensureListeningTo(rootContainerElement, 'onChange'); @@ -908,18 +939,6 @@ export function diffHydratedProperties( suppressHydrationWarning = rawProps[SUPPRESS_HYDRATION_WARNING] === true; isCustomComponentTag = isCustomComponent(tag, rawProps); validatePropertiesInDevelopment(tag, rawProps); - if ( - isCustomComponentTag && - !didWarnShadyDOM && - (domElement: any).shadyRoot - ) { - console.error( - '%s is using shady DOM. Using shady DOM with React can ' + - 'cause things to break subtly.', - getCurrentFiberOwnerNameInDevOrNull() || 'A component', - ); - didWarnShadyDOM = true; - } } // TODO: Make sure that we check isMounted before firing any of these events. @@ -927,34 +946,48 @@ export function diffHydratedProperties( case 'iframe': case 'object': case 'embed': - trapBubbledEvent(TOP_LOAD, domElement); + if (!enableModernEventSystem) { + legacyTrapBubbledEvent(TOP_LOAD, domElement); + } break; case 'video': case 'audio': - // Create listener for each media event - for (let i = 0; i < mediaEventTypes.length; i++) { - trapBubbledEvent(mediaEventTypes[i], domElement); + if (!enableModernEventSystem) { + // Create listener for each media event + for (let i = 0; i < mediaEventTypes.length; i++) { + legacyTrapBubbledEvent(mediaEventTypes[i], domElement); + } } break; case 'source': - trapBubbledEvent(TOP_ERROR, domElement); + if (!enableModernEventSystem) { + legacyTrapBubbledEvent(TOP_ERROR, domElement); + } break; case 'img': case 'image': case 'link': - trapBubbledEvent(TOP_ERROR, domElement); - trapBubbledEvent(TOP_LOAD, domElement); + if (!enableModernEventSystem) { + legacyTrapBubbledEvent(TOP_ERROR, domElement); + legacyTrapBubbledEvent(TOP_LOAD, domElement); + } break; case 'form': - trapBubbledEvent(TOP_RESET, domElement); - trapBubbledEvent(TOP_SUBMIT, domElement); + if (!enableModernEventSystem) { + legacyTrapBubbledEvent(TOP_RESET, domElement); + legacyTrapBubbledEvent(TOP_SUBMIT, domElement); + } break; case 'details': - trapBubbledEvent(TOP_TOGGLE, domElement); + if (!enableModernEventSystem) { + legacyTrapBubbledEvent(TOP_TOGGLE, domElement); + } break; case 'input': ReactDOMInputInitWrapperState(domElement, rawProps); - trapBubbledEvent(TOP_INVALID, domElement); + if (!enableModernEventSystem) { + legacyTrapBubbledEvent(TOP_INVALID, domElement); + } // For controlled components we always need to ensure we're listening // to onChange. Even if there is no listener. ensureListeningTo(rootContainerElement, 'onChange'); @@ -964,14 +997,18 @@ export function diffHydratedProperties( break; case 'select': ReactDOMSelectInitWrapperState(domElement, rawProps); - trapBubbledEvent(TOP_INVALID, domElement); + if (!enableModernEventSystem) { + legacyTrapBubbledEvent(TOP_INVALID, domElement); + } // For controlled components we always need to ensure we're listening // to onChange. Even if there is no listener. ensureListeningTo(rootContainerElement, 'onChange'); break; case 'textarea': ReactDOMTextareaInitWrapperState(domElement, rawProps); - trapBubbledEvent(TOP_INVALID, domElement); + if (!enableModernEventSystem) { + legacyTrapBubbledEvent(TOP_INVALID, domElement); + } // For controlled components we always need to ensure we're listening // to onChange. Even if there is no listener. ensureListeningTo(rootContainerElement, 'onChange'); diff --git a/packages/react-dom/src/client/ReactDOMFB.js b/packages/react-dom/src/client/ReactDOMFB.js deleted file mode 100644 index a995f485578d8..0000000000000 --- a/packages/react-dom/src/client/ReactDOMFB.js +++ /dev/null @@ -1,34 +0,0 @@ -/** - * Copyright (c) Facebook, Inc. and its affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - * - * @flow - */ - -import {getIsHydrating} from 'react-reconciler/src/ReactFiberHydrationContext'; -import {addUserTimingListener} from 'shared/ReactFeatureFlags'; - -import ReactDOM from './ReactDOM'; -import {isEnabled} from '../events/ReactBrowserEventEmitter'; -import {getClosestInstanceFromNode} from './ReactDOMComponentTree'; - -Object.assign( - (ReactDOM.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED: any), - { - // These are real internal dependencies that are trickier to remove: - ReactBrowserEventEmitter: { - isEnabled, - }, - ReactDOMComponentTree: { - getClosestInstanceFromNode, - }, - // Perf experiment - addUserTimingListener, - - getIsHydrating, - }, -); - -export default ReactDOM; diff --git a/packages/react-dom/src/client/ReactDOMHostConfig.js b/packages/react-dom/src/client/ReactDOMHostConfig.js index 2e90978b31c03..c61aa20c85836 100644 --- a/packages/react-dom/src/client/ReactDOMHostConfig.js +++ b/packages/react-dom/src/client/ReactDOMHostConfig.js @@ -7,6 +7,8 @@ * @flow */ +import type {RootType} from './ReactDOMRoot'; + import { precacheFiberNode, updateFiberProps, @@ -34,7 +36,7 @@ import {validateDOMNesting, updatedAncestorInfo} from './validateDOMNesting'; import { isEnabled as ReactBrowserEventEmitterIsEnabled, setEnabled as ReactBrowserEventEmitterSetEnabled, -} from '../events/ReactBrowserEventEmitter'; +} from '../events/ReactDOMEventListener'; import {getChildNamespace} from '../shared/DOMNamespaces'; import { ELEMENT_NODE, @@ -45,7 +47,6 @@ import { } from '../shared/HTMLNodeType'; import dangerousStyleValue from '../shared/dangerousStyleValue'; -import type {DOMContainer} from './ReactDOM'; import type { ReactDOMEventResponder, ReactDOMEventResponderInstance, @@ -58,6 +59,17 @@ import { } from '../events/DeprecatedDOMEventResponderSystem'; import {retryIfBlockedOn} from '../events/ReactDOMEventReplaying'; +import { + enableSuspenseServerRenderer, + enableDeprecatedFlareAPI, + enableFundamentalAPI, +} from 'shared/ReactFeatureFlags'; +import {HostComponent} from 'shared/ReactWorkTags'; +import { + RESPONDER_EVENT_SYSTEM, + IS_PASSIVE, +} from 'legacy-events/EventSystemFlags'; + export type Type = string; export type Props = { autoFocus?: boolean, @@ -88,7 +100,9 @@ export type EventTargetChildElement = { }, ... }; -export type Container = DOMContainer; +export type Container = + | (Element & {_reactRootContainer?: RootType, ...}) + | (Document & {_reactRootContainer?: RootType, ...}); export type Instance = Element; export type TextInstance = Text; export type SuspenseInstance = Comment & {_reactRetry?: () => void, ...}; @@ -112,16 +126,6 @@ type SelectionInformation = {| selectionRange: mixed, |}; -import { - enableSuspenseServerRenderer, - enableDeprecatedFlareAPI, - enableFundamentalAPI, -} from 'shared/ReactFeatureFlags'; -import { - RESPONDER_EVENT_SYSTEM, - IS_PASSIVE, -} from 'legacy-events/EventSystemFlags'; - let SUPPRESS_HYDRATION_WARNING; if (__DEV__) { SUPPRESS_HYDRATION_WARNING = 'suppressHydrationWarning'; @@ -418,7 +422,7 @@ export function appendChild( } export function appendChildToContainer( - container: DOMContainer, + container: Container, child: Instance | TextInstance, ): void { let parentNode; @@ -584,7 +588,28 @@ export function clearSuspenseBoundaryFromContainer( retryIfBlockedOn(container); } +function instanceContainsElem(instance: Instance, element: HTMLElement) { + let fiber = getClosestInstanceFromNode(element); + while (fiber !== null) { + if (fiber.tag === HostComponent && fiber.stateNode === element) { + return true; + } + fiber = fiber.return; + } + return false; +} + export function hideInstance(instance: Instance): void { + // Ensure we trigger `onBeforeBlur` if the active focused elment + // is ether the instance of a child or the instance. We need + // to traverse the Fiber tree here rather than use node.contains() + // as the child node might be inside a Portal. + if (enableDeprecatedFlareAPI && selectionInformation) { + const focusedElem = selectionInformation.focusedElem; + if (focusedElem !== null && instanceContainsElem(instance, focusedElem)) { + dispatchBeforeDetachedBlur(((focusedElem: any): HTMLElement)); + } + } // TODO: Does this work for all element types? What about MathML? Should we // pass host context to this method? instance = ((instance: any): HTMLElement); diff --git a/packages/react-dom/src/client/ReactDOMLegacy.js b/packages/react-dom/src/client/ReactDOMLegacy.js index 519a0be3bb551..97fbac293578a 100644 --- a/packages/react-dom/src/client/ReactDOMLegacy.js +++ b/packages/react-dom/src/client/ReactDOMLegacy.js @@ -7,7 +7,7 @@ * @flow */ -import type {DOMContainer} from './ReactDOM'; +import type {Container} from './ReactDOMHostConfig'; import type {RootType} from './ReactDOMRoot'; import type {ReactNodeList} from 'shared/ReactTypes'; @@ -43,7 +43,7 @@ let topLevelUpdateWarnings; let warnedAboutHydrateAPI = false; if (__DEV__) { - topLevelUpdateWarnings = (container: DOMContainer) => { + topLevelUpdateWarnings = (container: Container) => { if (container._reactRootContainer && container.nodeType !== COMMENT_NODE) { const hostInstance = findHostInstanceWithNoPortals( container._reactRootContainer._internalRoot.current, @@ -111,7 +111,7 @@ function shouldHydrateDueToLegacyHeuristic(container) { } function legacyCreateRootFromDOMContainer( - container: DOMContainer, + container: Container, forceHydrate: boolean, ): RootType { const shouldHydrate = @@ -175,7 +175,7 @@ function warnOnInvalidCallback(callback: mixed, callerName: string): void { function legacyRenderSubtreeIntoContainer( parentComponent: ?React$Component, children: ReactNodeList, - container: DOMContainer, + container: Container, forceHydrate: boolean, callback: ?Function, ) { @@ -255,7 +255,7 @@ export function findDOMNode( export function hydrate( element: React$Node, - container: DOMContainer, + container: Container, callback: ?Function, ) { invariant( @@ -286,7 +286,7 @@ export function hydrate( export function render( element: React$Element, - container: DOMContainer, + container: Container, callback: ?Function, ) { invariant( @@ -317,7 +317,7 @@ export function render( export function unstable_renderSubtreeIntoContainer( parentComponent: React$Component, element: React$Element, - containerNode: DOMContainer, + containerNode: Container, callback: ?Function, ) { invariant( @@ -337,7 +337,7 @@ export function unstable_renderSubtreeIntoContainer( ); } -export function unmountComponentAtNode(container: DOMContainer) { +export function unmountComponentAtNode(container: Container) { invariant( isValidContainer(container), 'unmountComponentAtNode(...): Target container is not a DOM element.', @@ -370,6 +370,7 @@ export function unmountComponentAtNode(container: DOMContainer) { // Unmount should not be batched. unbatchedUpdates(() => { legacyRenderSubtreeIntoContainer(null, null, container, false, () => { + // $FlowFixMe This should probably use `delete container._reactRootContainer` container._reactRootContainer = null; unmarkContainerAsRoot(container); }); diff --git a/packages/react-dom/src/client/ReactDOMOption.js b/packages/react-dom/src/client/ReactDOMOption.js index d18813cfa4459..a06eb826767e6 100644 --- a/packages/react-dom/src/client/ReactDOMOption.js +++ b/packages/react-dom/src/client/ReactDOMOption.js @@ -7,7 +7,7 @@ * @flow */ -import React from 'react'; +import * as React from 'react'; import {getToStringValue, toString} from './ToStringValue'; let didWarnSelectedSetOnOption = false; diff --git a/packages/react-dom/src/client/ReactDOMRoot.js b/packages/react-dom/src/client/ReactDOMRoot.js index f9282e919c0dd..7cbbbbd939201 100644 --- a/packages/react-dom/src/client/ReactDOMRoot.js +++ b/packages/react-dom/src/client/ReactDOMRoot.js @@ -7,7 +7,7 @@ * @flow */ -import type {DOMContainer} from './ReactDOM'; +import type {Container} from './ReactDOMHostConfig'; import type {RootTag} from 'shared/ReactRootTags'; import type {ReactNodeList} from 'shared/ReactTypes'; // TODO: This type is shared between the reconciler and ReactDOM, but will @@ -49,12 +49,12 @@ import {createContainer, updateContainer} from 'react-reconciler/inline.dom'; import invariant from 'shared/invariant'; import {BlockingRoot, ConcurrentRoot, LegacyRoot} from 'shared/ReactRootTags'; -function ReactDOMRoot(container: DOMContainer, options: void | RootOptions) { +function ReactDOMRoot(container: Container, options: void | RootOptions) { this._internalRoot = createRootImpl(container, ConcurrentRoot, options); } function ReactDOMBlockingRoot( - container: DOMContainer, + container: Container, tag: RootTag, options: void | RootOptions, ) { @@ -108,7 +108,7 @@ ReactDOMRoot.prototype.unmount = ReactDOMBlockingRoot.prototype.unmount = functi }; function createRootImpl( - container: DOMContainer, + container: Container, tag: RootTag, options: void | RootOptions, ) { @@ -123,13 +123,13 @@ function createRootImpl( container.nodeType === DOCUMENT_NODE ? container : container.ownerDocument; - eagerlyTrapReplayableEvents(doc); + eagerlyTrapReplayableEvents(container, doc); } return root; } export function createRoot( - container: DOMContainer, + container: Container, options?: RootOptions, ): RootType { invariant( @@ -141,7 +141,7 @@ export function createRoot( } export function createBlockingRoot( - container: DOMContainer, + container: Container, options?: RootOptions, ): RootType { invariant( @@ -153,7 +153,7 @@ export function createBlockingRoot( } export function createLegacyRoot( - container: DOMContainer, + container: Container, options?: RootOptions, ): RootType { return new ReactDOMBlockingRoot(container, LegacyRoot, options); diff --git a/packages/react-dom/src/events/DOMEventListenerMap.js b/packages/react-dom/src/events/DOMEventListenerMap.js new file mode 100644 index 0000000000000..f087a793d904c --- /dev/null +++ b/packages/react-dom/src/events/DOMEventListenerMap.js @@ -0,0 +1,49 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +import type {DOMTopLevelEventType} from 'legacy-events/TopLevelEventTypes'; + +import {registrationNameDependencies} from 'legacy-events/EventPluginRegistry'; + +const PossiblyWeakMap = typeof WeakMap === 'function' ? WeakMap : Map; +// prettier-ignore +const elementListenerMap: + // $FlowFixMe Work around Flow bug + | WeakMap + | Map< + Document | Element | Node, + Map void)>, + > = new PossiblyWeakMap(); + +export function getListenerMapForElement( + element: Document | Element | Node, +): Map void)> { + let listenerMap = elementListenerMap.get(element); + if (listenerMap === undefined) { + listenerMap = new Map(); + elementListenerMap.set(element, listenerMap); + } + return listenerMap; +} + +export function isListeningToAllDependencies( + registrationName: string, + mountAt: Document | Element, +): boolean { + const listenerMap = getListenerMapForElement(mountAt); + const dependencies = registrationNameDependencies[registrationName]; + + for (let i = 0; i < dependencies.length; i++) { + const dependency = dependencies[i]; + if (!listenerMap.has(dependency)) { + return false; + } + } + return true; +} diff --git a/packages/react-dom/src/events/DOMEventPluginOrder.js b/packages/react-dom/src/events/DOMEventPluginOrder.js deleted file mode 100644 index add0b12a238ac..0000000000000 --- a/packages/react-dom/src/events/DOMEventPluginOrder.js +++ /dev/null @@ -1,26 +0,0 @@ -/** - * Copyright (c) Facebook, Inc. and its affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */ - -/** - * Module that is injectable into `EventPluginHub`, that specifies a - * deterministic ordering of `EventPlugin`s. A convenient way to reason about - * plugins, without having to package every one of them. This is better than - * having plugins be ordered in the same order that they are injected because - * that ordering would be influenced by the packaging order. - * `ResponderEventPlugin` must occur before `SimpleEventPlugin` so that - * preventing default on events is convenient in `SimpleEventPlugin` handlers. - */ -const DOMEventPluginOrder = [ - 'ResponderEventPlugin', - 'SimpleEventPlugin', - 'EnterLeaveEventPlugin', - 'ChangeEventPlugin', - 'SelectEventPlugin', - 'BeforeInputEventPlugin', -]; - -export default DOMEventPluginOrder; diff --git a/packages/react-dom/src/events/DOMLegacyEventPluginSystem.js b/packages/react-dom/src/events/DOMLegacyEventPluginSystem.js new file mode 100644 index 0000000000000..d7881ac6903a8 --- /dev/null +++ b/packages/react-dom/src/events/DOMLegacyEventPluginSystem.js @@ -0,0 +1,379 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +import type {AnyNativeEvent} from 'legacy-events/PluginModuleType'; +import type {DOMTopLevelEventType} from 'legacy-events/TopLevelEventTypes'; +import type {EventSystemFlags} from 'legacy-events/EventSystemFlags'; +import type {Fiber} from 'react-reconciler/src/ReactFiber'; +import type {PluginModule} from 'legacy-events/PluginModuleType'; +import type {ReactSyntheticEvent} from 'legacy-events/ReactSyntheticEventType'; +import type {TopLevelType} from 'legacy-events/TopLevelEventTypes'; + +import {HostRoot, HostComponent, HostText} from 'shared/ReactWorkTags'; +import {IS_FIRST_ANCESTOR} from 'legacy-events/EventSystemFlags'; +import {batchedEventUpdates} from 'legacy-events/ReactGenericBatching'; +import {runEventsInBatch} from 'legacy-events/EventBatching'; +import {plugins} from 'legacy-events/EventPluginRegistry'; +import accumulateInto from 'legacy-events/accumulateInto'; +import {registrationNameDependencies} from 'legacy-events/EventPluginRegistry'; + +import getEventTarget from './getEventTarget'; +import {getClosestInstanceFromNode} from '../client/ReactDOMComponentTree'; +import {getListenerMapForElement} from './DOMEventListenerMap'; +import isEventSupported from './isEventSupported'; +import { + TOP_BLUR, + TOP_CANCEL, + TOP_CLOSE, + TOP_FOCUS, + TOP_INVALID, + TOP_RESET, + TOP_SCROLL, + TOP_SUBMIT, + getRawEventName, + mediaEventTypes, +} from './DOMTopLevelEventTypes'; +import {trapEventForPluginEventSystem} from './ReactDOMEventListener'; + +/** + * Summary of `DOMEventPluginSystem` event handling: + * + * - Top-level delegation is used to trap most native browser events. This + * may only occur in the main thread and is the responsibility of + * ReactDOMEventListener, which is injected and can therefore support + * pluggable event sources. This is the only work that occurs in the main + * thread. + * + * - We normalize and de-duplicate events to account for browser quirks. This + * may be done in the worker thread. + * + * - Forward these native events (with the associated top-level type used to + * trap it) to `EventPluginRegistry`, which in turn will ask plugins if they want + * to extract any synthetic events. + * + * - The `EventPluginRegistry` will then process each event by annotating them with + * "dispatches", a sequence of listeners and IDs that care about that event. + * + * - The `EventPluginRegistry` then dispatches the events. + * + * Overview of React and the event system: + * + * +------------+ . + * | DOM | . + * +------------+ . + * | . + * v . + * +------------+ . + * | ReactEvent | . + * | Listener | . + * +------------+ . +-----------+ + * | . +--------+|SimpleEvent| + * | . | |Plugin | + * +-----|------+ . v +-----------+ + * | | | . +--------------+ +------------+ + * | +-----------.--->|PluginRegistry| | Event | + * | | . | | +-----------+ | Propagators| + * | ReactEvent | . | | |TapEvent | |------------| + * | Emitter | . | |<---+|Plugin | |other plugin| + * | | . | | +-----------+ | utilities | + * | +-----------.--->| | +------------+ + * | | | . +--------------+ + * +-----|------+ . ^ +-----------+ + * | . | |Enter/Leave| + * + . +-------+|Plugin | + * +-------------+ . +-----------+ + * | application | . + * |-------------| . + * | | . + * | | . + * +-------------+ . + * . + * React Core . General Purpose Event Plugin System + */ + +const CALLBACK_BOOKKEEPING_POOL_SIZE = 10; +const callbackBookkeepingPool = []; + +type BookKeepingInstance = {| + topLevelType: DOMTopLevelEventType | null, + eventSystemFlags: EventSystemFlags, + nativeEvent: AnyNativeEvent | null, + targetInst: Fiber | null, + ancestors: Array, +|}; + +function releaseTopLevelCallbackBookKeeping( + instance: BookKeepingInstance, +): void { + instance.topLevelType = null; + instance.nativeEvent = null; + instance.targetInst = null; + instance.ancestors.length = 0; + if (callbackBookkeepingPool.length < CALLBACK_BOOKKEEPING_POOL_SIZE) { + callbackBookkeepingPool.push(instance); + } +} + +// Used to store ancestor hierarchy in top level callback +function getTopLevelCallbackBookKeeping( + topLevelType: DOMTopLevelEventType, + nativeEvent: AnyNativeEvent, + targetInst: Fiber | null, + eventSystemFlags: EventSystemFlags, +): BookKeepingInstance { + if (callbackBookkeepingPool.length) { + const instance = callbackBookkeepingPool.pop(); + instance.topLevelType = topLevelType; + instance.eventSystemFlags = eventSystemFlags; + instance.nativeEvent = nativeEvent; + instance.targetInst = targetInst; + return instance; + } + return { + topLevelType, + eventSystemFlags, + nativeEvent, + targetInst, + ancestors: [], + }; +} + +/** + * Find the deepest React component completely containing the root of the + * passed-in instance (for use when entire React trees are nested within each + * other). If React trees are not nested, returns null. + */ +function findRootContainerNode(inst) { + if (inst.tag === HostRoot) { + return inst.stateNode.containerInfo; + } + // TODO: It may be a good idea to cache this to prevent unnecessary DOM + // traversal, but caching is difficult to do correctly without using a + // mutation observer to listen for all DOM changes. + while (inst.return) { + inst = inst.return; + } + if (inst.tag !== HostRoot) { + // This can happen if we're in a detached tree. + return null; + } + return inst.stateNode.containerInfo; +} + +/** + * Allows registered plugins an opportunity to extract events from top-level + * native browser events. + * + * @return {*} An accumulation of synthetic events. + * @internal + */ +function extractPluginEvents( + topLevelType: TopLevelType, + targetInst: null | Fiber, + nativeEvent: AnyNativeEvent, + nativeEventTarget: null | EventTarget, + eventSystemFlags: EventSystemFlags, +): Array | ReactSyntheticEvent | null { + let events = null; + for (let i = 0; i < plugins.length; i++) { + // Not every plugin in the ordering may be loaded at runtime. + const possiblePlugin: PluginModule = plugins[i]; + if (possiblePlugin) { + const extractedEvents = possiblePlugin.extractEvents( + topLevelType, + targetInst, + nativeEvent, + nativeEventTarget, + eventSystemFlags, + ); + if (extractedEvents) { + events = accumulateInto(events, extractedEvents); + } + } + } + return events; +} + +function runExtractedPluginEventsInBatch( + topLevelType: TopLevelType, + targetInst: null | Fiber, + nativeEvent: AnyNativeEvent, + nativeEventTarget: null | EventTarget, + eventSystemFlags: EventSystemFlags, +) { + const events = extractPluginEvents( + topLevelType, + targetInst, + nativeEvent, + nativeEventTarget, + eventSystemFlags, + ); + runEventsInBatch(events); +} + +function handleTopLevel(bookKeeping: BookKeepingInstance) { + let targetInst = bookKeeping.targetInst; + + // Loop through the hierarchy, in case there's any nested components. + // It's important that we build the array of ancestors before calling any + // event handlers, because event handlers can modify the DOM, leading to + // inconsistencies with ReactMount's node cache. See #1105. + let ancestor = targetInst; + do { + if (!ancestor) { + const ancestors = bookKeeping.ancestors; + ((ancestors: any): Array).push(ancestor); + break; + } + const root = findRootContainerNode(ancestor); + if (!root) { + break; + } + const tag = ancestor.tag; + if (tag === HostComponent || tag === HostText) { + bookKeeping.ancestors.push(ancestor); + } + ancestor = getClosestInstanceFromNode(root); + } while (ancestor); + + for (let i = 0; i < bookKeeping.ancestors.length; i++) { + targetInst = bookKeeping.ancestors[i]; + const eventTarget = getEventTarget(bookKeeping.nativeEvent); + const topLevelType = ((bookKeeping.topLevelType: any): DOMTopLevelEventType); + const nativeEvent = ((bookKeeping.nativeEvent: any): AnyNativeEvent); + let eventSystemFlags = bookKeeping.eventSystemFlags; + + // If this is the first ancestor, we mark it on the system flags + if (i === 0) { + eventSystemFlags |= IS_FIRST_ANCESTOR; + } + + runExtractedPluginEventsInBatch( + topLevelType, + targetInst, + nativeEvent, + eventTarget, + eventSystemFlags, + ); + } +} + +export function dispatchEventForLegacyPluginEventSystem( + topLevelType: DOMTopLevelEventType, + eventSystemFlags: EventSystemFlags, + nativeEvent: AnyNativeEvent, + targetInst: null | Fiber, +): void { + const bookKeeping = getTopLevelCallbackBookKeeping( + topLevelType, + nativeEvent, + targetInst, + eventSystemFlags, + ); + + try { + // Event queue being processed in the same cycle allows + // `preventDefault`. + batchedEventUpdates(handleTopLevel, bookKeeping); + } finally { + releaseTopLevelCallbackBookKeeping(bookKeeping); + } +} + +/** + * We listen for bubbled touch events on the document object. + * + * Firefox v8.01 (and possibly others) exhibited strange behavior when + * mounting `onmousemove` events at some node that was not the document + * element. The symptoms were that if your mouse is not moving over something + * contained within that mount point (for example on the background) the + * top-level listeners for `onmousemove` won't be called. However, if you + * register the `mousemove` on the document object, then it will of course + * catch all `mousemove`s. This along with iOS quirks, justifies restricting + * top-level listeners to the document object only, at least for these + * movement types of events and possibly all events. + * + * @see http://www.quirksmode.org/blog/archives/2010/09/click_event_del.html + * + * Also, `keyup`/`keypress`/`keydown` do not bubble to the window on IE, but + * they bubble to document. + * + * @param {string} registrationName Name of listener (e.g. `onClick`). + * @param {object} mountAt Container where to mount the listener + */ +export function legacyListenToEvent( + registrationName: string, + mountAt: Document | Element, +): void { + const listenerMap = getListenerMapForElement(mountAt); + const dependencies = registrationNameDependencies[registrationName]; + + for (let i = 0; i < dependencies.length; i++) { + const dependency = dependencies[i]; + legacyListenToTopLevelEvent(dependency, mountAt, listenerMap); + } +} + +export function legacyListenToTopLevelEvent( + topLevelType: DOMTopLevelEventType, + mountAt: Document | Element, + listenerMap: Map void)>, +): void { + if (!listenerMap.has(topLevelType)) { + switch (topLevelType) { + case TOP_SCROLL: + legacyTrapCapturedEvent(TOP_SCROLL, mountAt); + break; + case TOP_FOCUS: + case TOP_BLUR: + legacyTrapCapturedEvent(TOP_FOCUS, mountAt); + legacyTrapCapturedEvent(TOP_BLUR, mountAt); + // We set the flag for a single dependency later in this function, + // but this ensures we mark both as attached rather than just one. + listenerMap.set(TOP_BLUR, null); + listenerMap.set(TOP_FOCUS, null); + break; + case TOP_CANCEL: + case TOP_CLOSE: + if (isEventSupported(getRawEventName(topLevelType))) { + legacyTrapCapturedEvent(topLevelType, mountAt); + } + break; + case TOP_INVALID: + case TOP_SUBMIT: + case TOP_RESET: + // We listen to them on the target DOM elements. + // Some of them bubble so we don't want them to fire twice. + break; + default: + // By default, listen on the top level to all non-media events. + // Media events don't bubble so adding the listener wouldn't do anything. + const isMediaEvent = mediaEventTypes.indexOf(topLevelType) !== -1; + if (!isMediaEvent) { + legacyTrapBubbledEvent(topLevelType, mountAt); + } + break; + } + listenerMap.set(topLevelType, null); + } +} + +export function legacyTrapBubbledEvent( + topLevelType: DOMTopLevelEventType, + element: Document | Element, +): void { + trapEventForPluginEventSystem(element, topLevelType, false); +} + +export function legacyTrapCapturedEvent( + topLevelType: DOMTopLevelEventType, + element: Document | Element, +): void { + trapEventForPluginEventSystem(element, topLevelType, true); +} diff --git a/packages/react-dom/src/events/DOMModernPluginEventSystem.js b/packages/react-dom/src/events/DOMModernPluginEventSystem.js new file mode 100644 index 0000000000000..aeeba5933d6a6 --- /dev/null +++ b/packages/react-dom/src/events/DOMModernPluginEventSystem.js @@ -0,0 +1,127 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +import type {AnyNativeEvent} from 'legacy-events/PluginModuleType'; +import type {DOMTopLevelEventType} from 'legacy-events/TopLevelEventTypes'; +import type {EventSystemFlags} from 'legacy-events/EventSystemFlags'; +import type {Fiber} from 'react-reconciler/src/ReactFiber'; + +import {registrationNameDependencies} from 'legacy-events/EventPluginRegistry'; + +import {trapEventForPluginEventSystem} from './ReactDOMEventListener'; +import {getListenerMapForElement} from './DOMEventListenerMap'; +import { + TOP_FOCUS, + TOP_LOAD, + TOP_ABORT, + TOP_CANCEL, + TOP_INVALID, + TOP_BLUR, + TOP_SCROLL, + TOP_CLOSE, + TOP_RESET, + TOP_SUBMIT, + TOP_CAN_PLAY, + TOP_CAN_PLAY_THROUGH, + TOP_DURATION_CHANGE, + TOP_EMPTIED, + TOP_ENCRYPTED, + TOP_ENDED, + TOP_ERROR, + TOP_WAITING, + TOP_VOLUME_CHANGE, + TOP_TIME_UPDATE, + TOP_SUSPEND, + TOP_STALLED, + TOP_SEEKING, + TOP_SEEKED, + TOP_PLAY, + TOP_PAUSE, + TOP_LOAD_START, + TOP_LOADED_DATA, + TOP_LOADED_METADATA, + TOP_RATE_CHANGE, + TOP_PROGRESS, + TOP_PLAYING, +} from './DOMTopLevelEventTypes'; + +const capturePhaseEvents = new Set([ + TOP_FOCUS, + TOP_BLUR, + TOP_SCROLL, + TOP_LOAD, + TOP_ABORT, + TOP_CANCEL, + TOP_CLOSE, + TOP_INVALID, + TOP_RESET, + TOP_SUBMIT, + TOP_ABORT, + TOP_CAN_PLAY, + TOP_CAN_PLAY_THROUGH, + TOP_DURATION_CHANGE, + TOP_EMPTIED, + TOP_ENCRYPTED, + TOP_ENDED, + TOP_ERROR, + TOP_LOADED_DATA, + TOP_LOADED_METADATA, + TOP_LOAD_START, + TOP_PAUSE, + TOP_PLAY, + TOP_PLAYING, + TOP_PROGRESS, + TOP_RATE_CHANGE, + TOP_SEEKED, + TOP_SEEKING, + TOP_STALLED, + TOP_SUSPEND, + TOP_TIME_UPDATE, + TOP_VOLUME_CHANGE, + TOP_WAITING, +]); + +export function listenToTopLevelEvent( + topLevelType: DOMTopLevelEventType, + rootContainerElement: Element, + listenerMap: Map void)>, +): void { + if (!listenerMap.has(topLevelType)) { + const isCapturePhase = capturePhaseEvents.has(topLevelType); + trapEventForPluginEventSystem( + rootContainerElement, + topLevelType, + isCapturePhase, + ); + listenerMap.set(topLevelType, null); + } +} + +export function listenToEvent( + registrationName: string, + rootContainerElement: Element, +): void { + const listenerMap = getListenerMapForElement(rootContainerElement); + const dependencies = registrationNameDependencies[registrationName]; + + for (let i = 0; i < dependencies.length; i++) { + const dependency = dependencies[i]; + listenToTopLevelEvent(dependency, rootContainerElement, listenerMap); + } +} + +export function dispatchEventForPluginEventSystem( + topLevelType: DOMTopLevelEventType, + eventSystemFlags: EventSystemFlags, + nativeEvent: AnyNativeEvent, + targetInst: null | Fiber, + rootContainer: Document | Element, +): void { + // TODO +} diff --git a/packages/react-dom/src/events/ReactBrowserEventEmitter.js b/packages/react-dom/src/events/ReactBrowserEventEmitter.js deleted file mode 100644 index 16cd407cba787..0000000000000 --- a/packages/react-dom/src/events/ReactBrowserEventEmitter.js +++ /dev/null @@ -1,203 +0,0 @@ -/** - * Copyright (c) Facebook, Inc. and its affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - * - * @flow - */ - -import {registrationNameDependencies} from 'legacy-events/EventPluginRegistry'; -import type {DOMTopLevelEventType} from 'legacy-events/TopLevelEventTypes'; -import { - TOP_BLUR, - TOP_CANCEL, - TOP_CLOSE, - TOP_FOCUS, - TOP_INVALID, - TOP_RESET, - TOP_SCROLL, - TOP_SUBMIT, - getRawEventName, - mediaEventTypes, -} from './DOMTopLevelEventTypes'; -import { - setEnabled, - isEnabled, - trapBubbledEvent, - trapCapturedEvent, -} from './ReactDOMEventListener'; -import isEventSupported from './isEventSupported'; - -/** - * Summary of `ReactBrowserEventEmitter` event handling: - * - * - Top-level delegation is used to trap most native browser events. This - * may only occur in the main thread and is the responsibility of - * ReactDOMEventListener, which is injected and can therefore support - * pluggable event sources. This is the only work that occurs in the main - * thread. - * - * - We normalize and de-duplicate events to account for browser quirks. This - * may be done in the worker thread. - * - * - Forward these native events (with the associated top-level type used to - * trap it) to `EventPluginHub`, which in turn will ask plugins if they want - * to extract any synthetic events. - * - * - The `EventPluginHub` will then process each event by annotating them with - * "dispatches", a sequence of listeners and IDs that care about that event. - * - * - The `EventPluginHub` then dispatches the events. - * - * Overview of React and the event system: - * - * +------------+ . - * | DOM | . - * +------------+ . - * | . - * v . - * +------------+ . - * | ReactEvent | . - * | Listener | . - * +------------+ . +-----------+ - * | . +--------+|SimpleEvent| - * | . | |Plugin | - * +-----|------+ . v +-----------+ - * | | | . +--------------+ +------------+ - * | +-----------.--->|EventPluginHub| | Event | - * | | . | | +-----------+ | Propagators| - * | ReactEvent | . | | |TapEvent | |------------| - * | Emitter | . | |<---+|Plugin | |other plugin| - * | | . | | +-----------+ | utilities | - * | +-----------.--->| | +------------+ - * | | | . +--------------+ - * +-----|------+ . ^ +-----------+ - * | . | |Enter/Leave| - * + . +-------+|Plugin | - * +-------------+ . +-----------+ - * | application | . - * |-------------| . - * | | . - * | | . - * +-------------+ . - * . - * React Core . General Purpose Event Plugin System - */ - -const PossiblyWeakMap = typeof WeakMap === 'function' ? WeakMap : Map; -// prettier-ignore -const elementListenerMap: - // $FlowFixMe Work around Flow bug - | WeakMap - | Map< - Document | Element | Node, - Map void)>, - > = new PossiblyWeakMap(); - -export function getListenerMapForElement( - element: Document | Element | Node, -): Map void)> { - let listenerMap = elementListenerMap.get(element); - if (listenerMap === undefined) { - listenerMap = new Map(); - elementListenerMap.set(element, listenerMap); - } - return listenerMap; -} - -/** - * We listen for bubbled touch events on the document object. - * - * Firefox v8.01 (and possibly others) exhibited strange behavior when - * mounting `onmousemove` events at some node that was not the document - * element. The symptoms were that if your mouse is not moving over something - * contained within that mount point (for example on the background) the - * top-level listeners for `onmousemove` won't be called. However, if you - * register the `mousemove` on the document object, then it will of course - * catch all `mousemove`s. This along with iOS quirks, justifies restricting - * top-level listeners to the document object only, at least for these - * movement types of events and possibly all events. - * - * @see http://www.quirksmode.org/blog/archives/2010/09/click_event_del.html - * - * Also, `keyup`/`keypress`/`keydown` do not bubble to the window on IE, but - * they bubble to document. - * - * @param {string} registrationName Name of listener (e.g. `onClick`). - * @param {object} mountAt Container where to mount the listener - */ -export function listenTo( - registrationName: string, - mountAt: Document | Element | Node, -): void { - const listeningSet = getListenerMapForElement(mountAt); - const dependencies = registrationNameDependencies[registrationName]; - - for (let i = 0; i < dependencies.length; i++) { - const dependency = dependencies[i]; - listenToTopLevel(dependency, mountAt, listeningSet); - } -} - -export function listenToTopLevel( - topLevelType: DOMTopLevelEventType, - mountAt: Document | Element | Node, - listenerMap: Map void)>, -): void { - if (!listenerMap.has(topLevelType)) { - switch (topLevelType) { - case TOP_SCROLL: - trapCapturedEvent(TOP_SCROLL, mountAt); - break; - case TOP_FOCUS: - case TOP_BLUR: - trapCapturedEvent(TOP_FOCUS, mountAt); - trapCapturedEvent(TOP_BLUR, mountAt); - // We set the flag for a single dependency later in this function, - // but this ensures we mark both as attached rather than just one. - listenerMap.set(TOP_BLUR, null); - listenerMap.set(TOP_FOCUS, null); - break; - case TOP_CANCEL: - case TOP_CLOSE: - if (isEventSupported(getRawEventName(topLevelType))) { - trapCapturedEvent(topLevelType, mountAt); - } - break; - case TOP_INVALID: - case TOP_SUBMIT: - case TOP_RESET: - // We listen to them on the target DOM elements. - // Some of them bubble so we don't want them to fire twice. - break; - default: - // By default, listen on the top level to all non-media events. - // Media events don't bubble so adding the listener wouldn't do anything. - const isMediaEvent = mediaEventTypes.indexOf(topLevelType) !== -1; - if (!isMediaEvent) { - trapBubbledEvent(topLevelType, mountAt); - } - break; - } - listenerMap.set(topLevelType, null); - } -} - -export function isListeningToAllDependencies( - registrationName: string, - mountAt: Document | Element, -): boolean { - const listenerMap = getListenerMapForElement(mountAt); - const dependencies = registrationNameDependencies[registrationName]; - - for (let i = 0; i < dependencies.length; i++) { - const dependency = dependencies[i]; - if (!listenerMap.has(dependency)) { - return false; - } - } - return true; -} - -export {setEnabled, isEnabled, trapBubbledEvent, trapCapturedEvent}; diff --git a/packages/react-dom/src/events/ReactDOMEventListener.js b/packages/react-dom/src/events/ReactDOMEventListener.js index d43c201168531..e6913dbeed5b2 100644 --- a/packages/react-dom/src/events/ReactDOMEventListener.js +++ b/packages/react-dom/src/events/ReactDOMEventListener.js @@ -8,7 +8,6 @@ */ import type {AnyNativeEvent} from 'legacy-events/PluginModuleType'; -import type {Fiber} from 'react-reconciler/src/ReactFiber'; import type {FiberRoot} from 'react-reconciler/src/ReactFiberRoot'; import type {Container, SuspenseInstance} from '../client/ReactDOMHostConfig'; import type {DOMTopLevelEventType} from 'legacy-events/TopLevelEventTypes'; @@ -18,11 +17,9 @@ import type {DOMTopLevelEventType} from 'legacy-events/TopLevelEventTypes'; import * as Scheduler from 'scheduler'; import { - batchedEventUpdates, discreteUpdates, flushDiscreteUpdatesIfNeeded, } from 'legacy-events/ReactGenericBatching'; -import {runExtractedPluginEventsInBatch} from 'legacy-events/EventPluginHub'; import {DEPRECATED_dispatchEventForResponderEventSystem} from './DeprecatedDOMEventResponderSystem'; import { isReplayableDiscreteEvent, @@ -36,12 +33,7 @@ import { getContainerFromFiber, getSuspenseInstanceFromFiber, } from 'react-reconciler/reflection'; -import { - HostRoot, - SuspenseComponent, - HostComponent, - HostText, -} from 'shared/ReactWorkTags'; +import {HostRoot, SuspenseComponent} from 'shared/ReactWorkTags'; import { type EventSystemFlags, PLUGIN_EVENT_SYSTEM, @@ -49,7 +41,6 @@ import { IS_PASSIVE, IS_ACTIVE, PASSIVE_NOT_SUPPORTED, - IS_FIRST_ANCESTOR, } from 'legacy-events/EventSystemFlags'; import { @@ -62,135 +53,24 @@ import {getClosestInstanceFromNode} from '../client/ReactDOMComponentTree'; import {getRawEventName} from './DOMTopLevelEventTypes'; import {passiveBrowserEventsSupported} from './checkPassiveEvents'; -import {enableDeprecatedFlareAPI} from 'shared/ReactFeatureFlags'; +import { + enableDeprecatedFlareAPI, + enableModernEventSystem, +} from 'shared/ReactFeatureFlags'; import { UserBlockingEvent, ContinuousEvent, DiscreteEvent, } from 'shared/ReactTypes'; import {getEventPriorityForPluginSystem} from './DOMEventProperties'; +import {dispatchEventForLegacyPluginEventSystem} from './DOMLegacyEventPluginSystem'; +import {dispatchEventForPluginEventSystem} from './DOMModernPluginEventSystem'; const { unstable_UserBlockingPriority: UserBlockingPriority, unstable_runWithPriority: runWithPriority, } = Scheduler; -const CALLBACK_BOOKKEEPING_POOL_SIZE = 10; -const callbackBookkeepingPool = []; - -type BookKeepingInstance = {| - topLevelType: DOMTopLevelEventType | null, - eventSystemFlags: EventSystemFlags, - nativeEvent: AnyNativeEvent | null, - targetInst: Fiber | null, - ancestors: Array, -|}; - -/** - * Find the deepest React component completely containing the root of the - * passed-in instance (for use when entire React trees are nested within each - * other). If React trees are not nested, returns null. - */ -function findRootContainerNode(inst) { - if (inst.tag === HostRoot) { - return inst.stateNode.containerInfo; - } - // TODO: It may be a good idea to cache this to prevent unnecessary DOM - // traversal, but caching is difficult to do correctly without using a - // mutation observer to listen for all DOM changes. - while (inst.return) { - inst = inst.return; - } - if (inst.tag !== HostRoot) { - // This can happen if we're in a detached tree. - return null; - } - return inst.stateNode.containerInfo; -} - -// Used to store ancestor hierarchy in top level callback -function getTopLevelCallbackBookKeeping( - topLevelType: DOMTopLevelEventType, - nativeEvent: AnyNativeEvent, - targetInst: Fiber | null, - eventSystemFlags: EventSystemFlags, -): BookKeepingInstance { - if (callbackBookkeepingPool.length) { - const instance = callbackBookkeepingPool.pop(); - instance.topLevelType = topLevelType; - instance.eventSystemFlags = eventSystemFlags; - instance.nativeEvent = nativeEvent; - instance.targetInst = targetInst; - return instance; - } - return { - topLevelType, - eventSystemFlags, - nativeEvent, - targetInst, - ancestors: [], - }; -} - -function releaseTopLevelCallbackBookKeeping( - instance: BookKeepingInstance, -): void { - instance.topLevelType = null; - instance.nativeEvent = null; - instance.targetInst = null; - instance.ancestors.length = 0; - if (callbackBookkeepingPool.length < CALLBACK_BOOKKEEPING_POOL_SIZE) { - callbackBookkeepingPool.push(instance); - } -} - -function handleTopLevel(bookKeeping: BookKeepingInstance) { - let targetInst = bookKeeping.targetInst; - - // Loop through the hierarchy, in case there's any nested components. - // It's important that we build the array of ancestors before calling any - // event handlers, because event handlers can modify the DOM, leading to - // inconsistencies with ReactMount's node cache. See #1105. - let ancestor = targetInst; - do { - if (!ancestor) { - const ancestors = bookKeeping.ancestors; - ((ancestors: any): Array).push(ancestor); - break; - } - const root = findRootContainerNode(ancestor); - if (!root) { - break; - } - const tag = ancestor.tag; - if (tag === HostComponent || tag === HostText) { - bookKeeping.ancestors.push(ancestor); - } - ancestor = getClosestInstanceFromNode(root); - } while (ancestor); - - for (let i = 0; i < bookKeeping.ancestors.length; i++) { - targetInst = bookKeeping.ancestors[i]; - const eventTarget = getEventTarget(bookKeeping.nativeEvent); - const topLevelType = ((bookKeeping.topLevelType: any): DOMTopLevelEventType); - const nativeEvent = ((bookKeeping.nativeEvent: any): AnyNativeEvent); - let eventSystemFlags = bookKeeping.eventSystemFlags; - - // If this is the first ancestor, we mark it on the system flags - if (i === 0) { - eventSystemFlags |= IS_FIRST_ANCESTOR; - } - - runExtractedPluginEventsInBatch( - topLevelType, - targetInst, - nativeEvent, - eventTarget, - eventSystemFlags, - ); - } -} - // TODO: can we stop exporting these? export let _enabled = true; @@ -202,20 +82,6 @@ export function isEnabled() { return _enabled; } -export function trapBubbledEvent( - topLevelType: DOMTopLevelEventType, - element: Document | Element | Node, -): void { - trapEventForPluginEventSystem(element, topLevelType, false); -} - -export function trapCapturedEvent( - topLevelType: DOMTopLevelEventType, - element: Document | Element | Node, -): void { - trapEventForPluginEventSystem(element, topLevelType, true); -} - export function addResponderEventSystemEvent( document: Document, topLevelType: string, @@ -243,6 +109,7 @@ export function addResponderEventSystemEvent( null, ((topLevelType: any): DOMTopLevelEventType), eventFlags, + document, ); if (passiveBrowserEventsSupported) { addEventCaptureListenerWithPassiveFlag( @@ -272,82 +139,78 @@ export function removeActiveResponderEventSystemEvent( } } -function trapEventForPluginEventSystem( - element: Document | Element | Node, +export function trapEventForPluginEventSystem( + container: Document | Element, topLevelType: DOMTopLevelEventType, capture: boolean, ): void { let listener; + let listenerWrapper; switch (getEventPriorityForPluginSystem(topLevelType)) { case DiscreteEvent: - listener = dispatchDiscreteEvent.bind( - null, - topLevelType, - PLUGIN_EVENT_SYSTEM, - ); + listenerWrapper = dispatchDiscreteEvent; break; case UserBlockingEvent: - listener = dispatchUserBlockingUpdate.bind( - null, - topLevelType, - PLUGIN_EVENT_SYSTEM, - ); + listenerWrapper = dispatchUserBlockingUpdate; break; case ContinuousEvent: default: - listener = dispatchEvent.bind(null, topLevelType, PLUGIN_EVENT_SYSTEM); + listenerWrapper = dispatchEvent; break; } + listener = listenerWrapper.bind( + null, + topLevelType, + PLUGIN_EVENT_SYSTEM, + container, + ); const rawEventName = getRawEventName(topLevelType); if (capture) { - addEventCaptureListener(element, rawEventName, listener); + addEventCaptureListener(container, rawEventName, listener); } else { - addEventBubbleListener(element, rawEventName, listener); + addEventBubbleListener(container, rawEventName, listener); } } -function dispatchDiscreteEvent(topLevelType, eventSystemFlags, nativeEvent) { +function dispatchDiscreteEvent( + topLevelType, + eventSystemFlags, + container, + nativeEvent, +) { flushDiscreteUpdatesIfNeeded(nativeEvent.timeStamp); - discreteUpdates(dispatchEvent, topLevelType, eventSystemFlags, nativeEvent); + discreteUpdates( + dispatchEvent, + topLevelType, + eventSystemFlags, + container, + nativeEvent, + ); } function dispatchUserBlockingUpdate( topLevelType, eventSystemFlags, + container, nativeEvent, ) { runWithPriority( UserBlockingPriority, - dispatchEvent.bind(null, topLevelType, eventSystemFlags, nativeEvent), - ); -} - -function dispatchEventForPluginEventSystem( - topLevelType: DOMTopLevelEventType, - eventSystemFlags: EventSystemFlags, - nativeEvent: AnyNativeEvent, - targetInst: null | Fiber, -): void { - const bookKeeping = getTopLevelCallbackBookKeeping( - topLevelType, - nativeEvent, - targetInst, - eventSystemFlags, + dispatchEvent.bind( + null, + topLevelType, + eventSystemFlags, + container, + nativeEvent, + ), ); - - try { - // Event queue being processed in the same cycle allows - // `preventDefault`. - batchedEventUpdates(handleTopLevel, bookKeeping); - } finally { - releaseTopLevelCallbackBookKeeping(bookKeeping); - } } export function dispatchEvent( topLevelType: DOMTopLevelEventType, eventSystemFlags: EventSystemFlags, + container: Document | Element, nativeEvent: AnyNativeEvent, ): void { if (!_enabled) { @@ -361,6 +224,7 @@ export function dispatchEvent( null, // Flags that we're not actually blocked on anything as far as we know. topLevelType, eventSystemFlags, + container, nativeEvent, ); return; @@ -369,6 +233,7 @@ export function dispatchEvent( const blockedOn = attemptToDispatchEvent( topLevelType, eventSystemFlags, + container, nativeEvent, ); @@ -380,7 +245,13 @@ export function dispatchEvent( if (isReplayableDiscreteEvent(topLevelType)) { // This this to be replayed later once the target is available. - queueDiscreteEvent(blockedOn, topLevelType, eventSystemFlags, nativeEvent); + queueDiscreteEvent( + blockedOn, + topLevelType, + eventSystemFlags, + container, + nativeEvent, + ); return; } @@ -389,6 +260,7 @@ export function dispatchEvent( blockedOn, topLevelType, eventSystemFlags, + container, nativeEvent, ) ) { @@ -403,12 +275,22 @@ export function dispatchEvent( // in case the event system needs to trace it. if (enableDeprecatedFlareAPI) { if (eventSystemFlags & PLUGIN_EVENT_SYSTEM) { - dispatchEventForPluginEventSystem( - topLevelType, - eventSystemFlags, - nativeEvent, - null, - ); + if (enableModernEventSystem) { + dispatchEventForPluginEventSystem( + topLevelType, + eventSystemFlags, + nativeEvent, + null, + container, + ); + } else { + dispatchEventForLegacyPluginEventSystem( + topLevelType, + eventSystemFlags, + nativeEvent, + null, + ); + } } if (eventSystemFlags & RESPONDER_EVENT_SYSTEM) { // React Flare event system @@ -421,12 +303,22 @@ export function dispatchEvent( ); } } else { - dispatchEventForPluginEventSystem( - topLevelType, - eventSystemFlags, - nativeEvent, - null, - ); + if (enableModernEventSystem) { + dispatchEventForPluginEventSystem( + topLevelType, + eventSystemFlags, + nativeEvent, + null, + container, + ); + } else { + dispatchEventForLegacyPluginEventSystem( + topLevelType, + eventSystemFlags, + nativeEvent, + null, + ); + } } } @@ -434,6 +326,7 @@ export function dispatchEvent( export function attemptToDispatchEvent( topLevelType: DOMTopLevelEventType, eventSystemFlags: EventSystemFlags, + container: Document | Element, nativeEvent: AnyNativeEvent, ): null | Container | SuspenseInstance { // TODO: Warn if _enabled is false. @@ -481,12 +374,22 @@ export function attemptToDispatchEvent( if (enableDeprecatedFlareAPI) { if (eventSystemFlags & PLUGIN_EVENT_SYSTEM) { - dispatchEventForPluginEventSystem( - topLevelType, - eventSystemFlags, - nativeEvent, - targetInst, - ); + if (enableModernEventSystem) { + dispatchEventForPluginEventSystem( + topLevelType, + eventSystemFlags, + nativeEvent, + targetInst, + container, + ); + } else { + dispatchEventForLegacyPluginEventSystem( + topLevelType, + eventSystemFlags, + nativeEvent, + targetInst, + ); + } } if (eventSystemFlags & RESPONDER_EVENT_SYSTEM) { // React Flare event system @@ -499,12 +402,22 @@ export function attemptToDispatchEvent( ); } } else { - dispatchEventForPluginEventSystem( - topLevelType, - eventSystemFlags, - nativeEvent, - targetInst, - ); + if (enableModernEventSystem) { + dispatchEventForPluginEventSystem( + topLevelType, + eventSystemFlags, + nativeEvent, + targetInst, + container, + ); + } else { + dispatchEventForLegacyPluginEventSystem( + topLevelType, + eventSystemFlags, + nativeEvent, + targetInst, + ); + } } // We're not blocked on anything. return null; diff --git a/packages/react-dom/src/events/ReactDOMEventReplaying.js b/packages/react-dom/src/events/ReactDOMEventReplaying.js index b0c999caf4781..a72fcdd2c5cde 100644 --- a/packages/react-dom/src/events/ReactDOMEventReplaying.js +++ b/packages/react-dom/src/events/ReactDOMEventReplaying.js @@ -16,6 +16,7 @@ import type {FiberRoot} from 'react-reconciler/src/ReactFiberRoot'; import { enableDeprecatedFlareAPI, enableSelectiveHydration, + enableModernEventSystem, } from 'shared/ReactFeatureFlags'; import { unstable_runWithPriority as runWithPriority, @@ -32,10 +33,7 @@ import { attemptToDispatchEvent, addResponderEventSystemEvent, } from './ReactDOMEventListener'; -import { - getListenerMapForElement, - listenToTopLevel, -} from './ReactBrowserEventEmitter'; +import {getListenerMapForElement} from './DOMEventListenerMap'; import { getInstanceFromNode, getClosestInstanceFromNode, @@ -120,12 +118,15 @@ import { TOP_BLUR, } from './DOMTopLevelEventTypes'; import {IS_REPLAYED} from 'legacy-events/EventSystemFlags'; +import {legacyListenToTopLevelEvent} from './DOMLegacyEventPluginSystem'; +import {listenToTopLevelEvent} from './DOMModernPluginEventSystem'; type QueuedReplayableEvent = {| blockedOn: null | Container | SuspenseInstance, topLevelType: DOMTopLevelEventType, eventSystemFlags: EventSystemFlags, nativeEvent: AnyNativeEvent, + container: Document | Element, |}; let hasScheduledReplayAttempt = false; @@ -212,12 +213,22 @@ export function isReplayableDiscreteEvent( return discreteReplayableEvents.indexOf(eventType) > -1; } -function trapReplayableEvent( +function trapReplayableEventForContainer( + topLevelType: DOMTopLevelEventType, + container: Container, + listenerMap: Map void)>, +) { + listenToTopLevelEvent(topLevelType, ((container: any): Element), listenerMap); +} + +function trapReplayableEventForDocument( topLevelType: DOMTopLevelEventType, document: Document, listenerMap: Map void)>, ) { - listenToTopLevel(topLevelType, document, listenerMap); + if (!enableModernEventSystem) { + legacyListenToTopLevelEvent(topLevelType, document, listenerMap); + } if (enableDeprecatedFlareAPI) { // Trap events for the responder system. const topLevelTypeString = unsafeCastDOMTopLevelTypeToString(topLevelType); @@ -237,15 +248,36 @@ function trapReplayableEvent( } } -export function eagerlyTrapReplayableEvents(document: Document) { - const listenerMap = getListenerMapForElement(document); +export function eagerlyTrapReplayableEvents( + container: Container, + document: Document, +) { + const listenerMapForDoc = getListenerMapForElement(document); + let listenerMapForContainer; + if (enableModernEventSystem) { + listenerMapForContainer = getListenerMapForElement(container); + } // Discrete discreteReplayableEvents.forEach(topLevelType => { - trapReplayableEvent(topLevelType, document, listenerMap); + if (enableModernEventSystem) { + trapReplayableEventForContainer( + topLevelType, + container, + listenerMapForContainer, + ); + } + trapReplayableEventForDocument(topLevelType, document, listenerMapForDoc); }); // Continuous continuousReplayableEvents.forEach(topLevelType => { - trapReplayableEvent(topLevelType, document, listenerMap); + if (enableModernEventSystem) { + trapReplayableEventForContainer( + topLevelType, + container, + listenerMapForContainer, + ); + } + trapReplayableEventForDocument(topLevelType, document, listenerMapForDoc); }); } @@ -253,6 +285,7 @@ function createQueuedReplayableEvent( blockedOn: null | Container | SuspenseInstance, topLevelType: DOMTopLevelEventType, eventSystemFlags: EventSystemFlags, + container: Document | Element, nativeEvent: AnyNativeEvent, ): QueuedReplayableEvent { return { @@ -260,6 +293,7 @@ function createQueuedReplayableEvent( topLevelType, eventSystemFlags: eventSystemFlags | IS_REPLAYED, nativeEvent, + container, }; } @@ -267,12 +301,14 @@ export function queueDiscreteEvent( blockedOn: null | Container | SuspenseInstance, topLevelType: DOMTopLevelEventType, eventSystemFlags: EventSystemFlags, + container: Document | Element, nativeEvent: AnyNativeEvent, ): void { const queuedEvent = createQueuedReplayableEvent( blockedOn, topLevelType, eventSystemFlags, + container, nativeEvent, ); queuedDiscreteEvents.push(queuedEvent); @@ -340,6 +376,7 @@ function accumulateOrCreateContinuousQueuedReplayableEvent( blockedOn: null | Container | SuspenseInstance, topLevelType: DOMTopLevelEventType, eventSystemFlags: EventSystemFlags, + container: Document | Element, nativeEvent: AnyNativeEvent, ): QueuedReplayableEvent { if ( @@ -350,6 +387,7 @@ function accumulateOrCreateContinuousQueuedReplayableEvent( blockedOn, topLevelType, eventSystemFlags, + container, nativeEvent, ); if (blockedOn !== null) { @@ -373,6 +411,7 @@ export function queueIfContinuousEvent( blockedOn: null | Container | SuspenseInstance, topLevelType: DOMTopLevelEventType, eventSystemFlags: EventSystemFlags, + container: Document | Element, nativeEvent: AnyNativeEvent, ): boolean { // These set relatedTarget to null because the replayed event will be treated as if we @@ -386,6 +425,7 @@ export function queueIfContinuousEvent( blockedOn, topLevelType, eventSystemFlags, + container, focusEvent, ); return true; @@ -397,6 +437,7 @@ export function queueIfContinuousEvent( blockedOn, topLevelType, eventSystemFlags, + container, dragEvent, ); return true; @@ -408,6 +449,7 @@ export function queueIfContinuousEvent( blockedOn, topLevelType, eventSystemFlags, + container, mouseEvent, ); return true; @@ -422,6 +464,7 @@ export function queueIfContinuousEvent( blockedOn, topLevelType, eventSystemFlags, + container, pointerEvent, ), ); @@ -437,6 +480,7 @@ export function queueIfContinuousEvent( blockedOn, topLevelType, eventSystemFlags, + container, pointerEvent, ), ); @@ -513,6 +557,7 @@ function attemptReplayContinuousQueuedEvent( let nextBlockedOn = attemptToDispatchEvent( queuedEvent.topLevelType, queuedEvent.eventSystemFlags, + queuedEvent.container, queuedEvent.nativeEvent, ); if (nextBlockedOn !== null) { @@ -555,6 +600,7 @@ function replayUnblockedEvents() { let nextBlockedOn = attemptToDispatchEvent( nextDiscreteEvent.topLevelType, nextDiscreteEvent.eventSystemFlags, + nextDiscreteEvent.container, nextDiscreteEvent.nativeEvent, ); if (nextBlockedOn !== null) { diff --git a/packages/react-dom/src/events/SelectEventPlugin.js b/packages/react-dom/src/events/SelectEventPlugin.js index b358e896eaf37..2cb52b4dfedbf 100644 --- a/packages/react-dom/src/events/SelectEventPlugin.js +++ b/packages/react-dom/src/events/SelectEventPlugin.js @@ -22,11 +22,11 @@ import { TOP_MOUSE_UP, TOP_SELECTION_CHANGE, } from './DOMTopLevelEventTypes'; -import {isListeningToAllDependencies} from './ReactBrowserEventEmitter'; import getActiveElement from '../client/getActiveElement'; import {getNodeFromInstance} from '../client/ReactDOMComponentTree'; import {hasSelectionCapabilities} from '../client/ReactInputSelection'; import {DOCUMENT_NODE} from '../shared/HTMLNodeType'; +import {isListeningToAllDependencies} from './DOMEventListenerMap'; const skipSelectionChangeEvent = canUseDOM && 'documentMode' in document && document.documentMode <= 11; @@ -166,11 +166,16 @@ const SelectEventPlugin = { nativeEvent, nativeEventTarget, eventSystemFlags, + container, ) { - const doc = getEventTargetDocument(nativeEventTarget); + const containerOrDoc = + container || getEventTargetDocument(nativeEventTarget); // Track whether all listeners exists for this plugin. If none exist, we do // not extract events. See #3639. - if (!doc || !isListeningToAllDependencies('onSelect', doc)) { + if ( + !containerOrDoc || + !isListeningToAllDependencies('onSelect', containerOrDoc) + ) { return null; } diff --git a/packages/react-dom/src/events/__tests__/DeprecatedDOMEventResponderSystem-test.internal.js b/packages/react-dom/src/events/__tests__/DeprecatedDOMEventResponderSystem-test.internal.js index 3711cbe30365b..c3fa398e83766 100644 --- a/packages/react-dom/src/events/__tests__/DeprecatedDOMEventResponderSystem-test.internal.js +++ b/packages/react-dom/src/events/__tests__/DeprecatedDOMEventResponderSystem-test.internal.js @@ -66,6 +66,11 @@ function dispatchClickEvent(element) { describe('DOMEventResponderSystem', () => { let container; + if (!__EXPERIMENTAL__) { + it("empty test so Jest doesn't complain", () => {}); + return; + } + beforeEach(() => { jest.resetModules(); ReactFeatureFlags = require('shared/ReactFeatureFlags'); diff --git a/packages/react-dom/src/events/forks/EventListener-www.js b/packages/react-dom/src/events/forks/EventListener-www.js index 99b337f0e7805..b76b9d2e390c1 100644 --- a/packages/react-dom/src/events/forks/EventListener-www.js +++ b/packages/react-dom/src/events/forks/EventListener-www.js @@ -16,16 +16,16 @@ export function addEventBubbleListener( element: Element, eventType: string, listener: Function, -): void { - EventListenerWWW.listen(element, eventType, listener); +) { + return EventListenerWWW.listen(element, eventType, listener); } export function addEventCaptureListener( element: Element, eventType: string, listener: Function, -): void { - EventListenerWWW.capture(element, eventType, listener); +) { + return EventListenerWWW.capture(element, eventType, listener); } export function addEventCaptureListenerWithPassiveFlag( @@ -33,8 +33,8 @@ export function addEventCaptureListenerWithPassiveFlag( eventType: string, listener: Function, passive: boolean, -): void { - EventListenerWWW.captureWithPassiveFlag( +) { + return EventListenerWWW.captureWithPassiveFlag( element, eventType, listener, diff --git a/packages/react-dom/src/server/ReactDOMFizzServerBrowser.js b/packages/react-dom/src/server/ReactDOMFizzServerBrowser.js index 93419fa27194b..cbf83b1b71aed 100644 --- a/packages/react-dom/src/server/ReactDOMFizzServerBrowser.js +++ b/packages/react-dom/src/server/ReactDOMFizzServerBrowser.js @@ -29,6 +29,4 @@ function renderToReadableStream(children: ReactNodeList): ReadableStream { }); } -export default { - renderToReadableStream, -}; +export {renderToReadableStream}; diff --git a/packages/react-dom/src/server/ReactDOMFizzServerNode.js b/packages/react-dom/src/server/ReactDOMFizzServerNode.js index 3815e1f958ef8..154b4fff204a6 100644 --- a/packages/react-dom/src/server/ReactDOMFizzServerNode.js +++ b/packages/react-dom/src/server/ReactDOMFizzServerNode.js @@ -25,6 +25,4 @@ function pipeToNodeWritable( startWork(request); } -export default { - pipeToNodeWritable, -}; +export {pipeToNodeWritable}; diff --git a/packages/react-dom/src/server/ReactDOMServerBrowser.js b/packages/react-dom/src/server/ReactDOMServerBrowser.js index 58f04b4960b31..4424040496b99 100644 --- a/packages/react-dom/src/server/ReactDOMServerBrowser.js +++ b/packages/react-dom/src/server/ReactDOMServerBrowser.js @@ -26,11 +26,10 @@ function renderToStaticNodeStream() { ); } -// Note: when changing this, also consider https://github.com/facebook/react/issues/11526 -export default { +export { renderToString, renderToStaticMarkup, renderToNodeStream, renderToStaticNodeStream, - version: ReactVersion, + ReactVersion as version, }; diff --git a/packages/react-dom/src/server/ReactDOMServerNode.js b/packages/react-dom/src/server/ReactDOMServerNode.js index b1f955eef53d7..1347434b6900c 100644 --- a/packages/react-dom/src/server/ReactDOMServerNode.js +++ b/packages/react-dom/src/server/ReactDOMServerNode.js @@ -13,11 +13,10 @@ import { renderToStaticNodeStream, } from './ReactDOMNodeStreamRenderer'; -// Note: when changing this, also consider https://github.com/facebook/react/issues/11526 -export default { +export { renderToString, renderToStaticMarkup, renderToNodeStream, renderToStaticNodeStream, - version: ReactVersion, + ReactVersion as version, }; diff --git a/packages/react-dom/src/server/ReactPartialRenderer.js b/packages/react-dom/src/server/ReactPartialRenderer.js index ecee219346a6a..e06f1df9bcd88 100644 --- a/packages/react-dom/src/server/ReactPartialRenderer.js +++ b/packages/react-dom/src/server/ReactPartialRenderer.js @@ -12,7 +12,7 @@ import type {ReactElement} from 'shared/ReactElementType'; import type {LazyComponent} from 'shared/ReactLazyComponent'; import type {ReactProvider, ReactContext} from 'shared/ReactTypes'; -import React from 'react'; +import * as React from 'react'; import invariant from 'shared/invariant'; import getComponentName from 'shared/getComponentName'; import describeComponentFrame from 'shared/describeComponentFrame'; diff --git a/packages/react-dom/src/server/ReactPartialRendererContext.js b/packages/react-dom/src/server/ReactPartialRendererContext.js index 808afae0bcdfd..74fdd8e321870 100644 --- a/packages/react-dom/src/server/ReactPartialRendererContext.js +++ b/packages/react-dom/src/server/ReactPartialRendererContext.js @@ -12,14 +12,11 @@ import type {ReactContext} from 'shared/ReactTypes'; import {disableLegacyContext} from 'shared/ReactFeatureFlags'; import {REACT_CONTEXT_TYPE, REACT_PROVIDER_TYPE} from 'shared/ReactSymbols'; -import ReactSharedInternals from 'shared/ReactSharedInternals'; import getComponentName from 'shared/getComponentName'; -import checkPropTypes from 'prop-types/checkPropTypes'; +import checkPropTypes from 'shared/checkPropTypes'; -let ReactDebugCurrentFrame; let didWarnAboutInvalidateContextType; if (__DEV__) { - ReactDebugCurrentFrame = ReactSharedInternals.ReactDebugCurrentFrame; didWarnAboutInvalidateContextType = new Set(); } @@ -42,13 +39,7 @@ function maskContext(type, context) { function checkContextTypes(typeSpecs, values, location: string) { if (__DEV__) { - checkPropTypes( - typeSpecs, - values, - location, - 'Component', - ReactDebugCurrentFrame.getCurrentStack, - ); + checkPropTypes(typeSpecs, values, location, 'Component'); } } diff --git a/packages/react-dom/src/shared/DOMProperty.js b/packages/react-dom/src/shared/DOMProperty.js index 1caa13baa3eb7..1eb36ead8416b 100644 --- a/packages/react-dom/src/shared/DOMProperty.js +++ b/packages/react-dom/src/shared/DOMProperty.js @@ -409,7 +409,7 @@ const capitalize = token => token[1].toUpperCase(); // or boolean value assignment. Regular attributes that just accept strings // and have the same names are omitted, just like in the HTML whitelist. // Some of these attributes can be hard to find. This list was created by -// scrapping the MDN documentation. +// scraping the MDN documentation. [ 'accent-height', 'alignment-baseline', diff --git a/packages/react-dom/src/shared/ReactControlledValuePropTypes.js b/packages/react-dom/src/shared/ReactControlledValuePropTypes.js index dfb6e0f715e1d..994419ba0c885 100644 --- a/packages/react-dom/src/shared/ReactControlledValuePropTypes.js +++ b/packages/react-dom/src/shared/ReactControlledValuePropTypes.js @@ -5,7 +5,7 @@ * LICENSE file in the root directory of this source tree. */ -import checkPropTypes from 'prop-types/checkPropTypes'; +import checkPropTypes from 'shared/checkPropTypes'; import ReactSharedInternals from 'shared/ReactSharedInternals'; import {enableDeprecatedFlareAPI} from 'shared/ReactFeatureFlags'; diff --git a/packages/react-dom/src/shared/checkReact.js b/packages/react-dom/src/shared/checkReact.js index cca72f505175b..860628b81fb71 100644 --- a/packages/react-dom/src/shared/checkReact.js +++ b/packages/react-dom/src/shared/checkReact.js @@ -7,7 +7,7 @@ * @flow */ -import React from 'react'; +import * as React from 'react'; import invariant from 'shared/invariant'; invariant( diff --git a/packages/react-dom/src/test-utils/ReactTestUtils.js b/packages/react-dom/src/test-utils/ReactTestUtils.js index 23e99cd15ae47..9449880c83ed4 100644 --- a/packages/react-dom/src/test-utils/ReactTestUtils.js +++ b/packages/react-dom/src/test-utils/ReactTestUtils.js @@ -5,8 +5,8 @@ * LICENSE file in the root directory of this source tree. */ -import React from 'react'; -import ReactDOM from 'react-dom'; +import * as React from 'react'; +import * as ReactDOM from 'react-dom'; import {findCurrentFiberUsingSlowPath} from 'react-reconciler/reflection'; import {get as getInstance} from 'shared/ReactInstanceMap'; import { @@ -62,7 +62,7 @@ let hasWarnedAboutDeprecatedMockComponent = false; */ function simulateNativeEventOnNode(topLevelType, node, fakeNativeEvent) { fakeNativeEvent.target = node; - dispatchEvent(topLevelType, PLUGIN_EVENT_SYSTEM, fakeNativeEvent); + dispatchEvent(topLevelType, PLUGIN_EVENT_SYSTEM, document, fakeNativeEvent); } /** @@ -159,243 +159,233 @@ function validateClassInstance(inst, methodName) { * utilities will suffice for testing purposes. * @lends ReactTestUtils */ -const ReactTestUtils = { - renderIntoDocument: function(element) { - const div = document.createElement('div'); - // None of our tests actually require attaching the container to the - // DOM, and doing so creates a mess that we rely on test isolation to - // clean up, so we're going to stop honoring the name of this method - // (and probably rename it eventually) if no problems arise. - // document.documentElement.appendChild(div); - return ReactDOM.render(element, div); - }, - - isElement: function(element) { - return React.isValidElement(element); - }, - - isElementOfType: function(inst, convenienceConstructor) { - return React.isValidElement(inst) && inst.type === convenienceConstructor; - }, - - isDOMComponent: function(inst) { - return !!(inst && inst.nodeType === ELEMENT_NODE && inst.tagName); - }, - - isDOMComponentElement: function(inst) { - return !!(inst && React.isValidElement(inst) && !!inst.tagName); - }, - - isCompositeComponent: function(inst) { - if (ReactTestUtils.isDOMComponent(inst)) { - // Accessing inst.setState warns; just return false as that'll be what - // this returns when we have DOM nodes as refs directly - return false; - } - return ( - inst != null && - typeof inst.render === 'function' && - typeof inst.setState === 'function' - ); - }, +function renderIntoDocument(element) { + const div = document.createElement('div'); + // None of our tests actually require attaching the container to the + // DOM, and doing so creates a mess that we rely on test isolation to + // clean up, so we're going to stop honoring the name of this method + // (and probably rename it eventually) if no problems arise. + // document.documentElement.appendChild(div); + return ReactDOM.render(element, div); +} - isCompositeComponentWithType: function(inst, type) { - if (!ReactTestUtils.isCompositeComponent(inst)) { - return false; - } - const internalInstance = getInstance(inst); - const constructor = internalInstance.type; - return constructor === type; - }, - - findAllInRenderedTree: function(inst, test) { - validateClassInstance(inst, 'findAllInRenderedTree'); - if (!inst) { - return []; - } - const internalInstance = getInstance(inst); - return findAllInRenderedFiberTreeInternal(internalInstance, test); - }, +function isElement(element) { + return React.isValidElement(element); +} - /** - * Finds all instance of components in the rendered tree that are DOM - * components with the class name matching `className`. - * @return {array} an array of all the matches. - */ - scryRenderedDOMComponentsWithClass: function(root, classNames) { - validateClassInstance(root, 'scryRenderedDOMComponentsWithClass'); - return ReactTestUtils.findAllInRenderedTree(root, function(inst) { - if (ReactTestUtils.isDOMComponent(inst)) { - let className = inst.className; - if (typeof className !== 'string') { - // SVG, probably. - className = inst.getAttribute('class') || ''; - } - const classList = className.split(/\s+/); - - if (!Array.isArray(classNames)) { - invariant( - classNames !== undefined, - 'TestUtils.scryRenderedDOMComponentsWithClass expects a ' + - 'className as a second argument.', - ); - classNames = classNames.split(/\s+/); - } - return classNames.every(function(name) { - return classList.indexOf(name) !== -1; - }); - } - return false; - }); - }, +function isElementOfType(inst, convenienceConstructor) { + return React.isValidElement(inst) && inst.type === convenienceConstructor; +} - /** - * Like scryRenderedDOMComponentsWithClass but expects there to be one result, - * and returns that one result, or throws exception if there is any other - * number of matches besides one. - * @return {!ReactDOMComponent} The one match. - */ - findRenderedDOMComponentWithClass: function(root, className) { - validateClassInstance(root, 'findRenderedDOMComponentWithClass'); - const all = ReactTestUtils.scryRenderedDOMComponentsWithClass( - root, - className, - ); - if (all.length !== 1) { - throw new Error( - 'Did not find exactly one match (found: ' + - all.length + - ') ' + - 'for class:' + - className, - ); - } - return all[0]; - }, +function isDOMComponent(inst) { + return !!(inst && inst.nodeType === ELEMENT_NODE && inst.tagName); +} - /** - * Finds all instance of components in the rendered tree that are DOM - * components with the tag name matching `tagName`. - * @return {array} an array of all the matches. - */ - scryRenderedDOMComponentsWithTag: function(root, tagName) { - validateClassInstance(root, 'scryRenderedDOMComponentsWithTag'); - return ReactTestUtils.findAllInRenderedTree(root, function(inst) { - return ( - ReactTestUtils.isDOMComponent(inst) && - inst.tagName.toUpperCase() === tagName.toUpperCase() - ); - }); - }, +function isDOMComponentElement(inst) { + return !!(inst && React.isValidElement(inst) && !!inst.tagName); +} - /** - * Like scryRenderedDOMComponentsWithTag but expects there to be one result, - * and returns that one result, or throws exception if there is any other - * number of matches besides one. - * @return {!ReactDOMComponent} The one match. - */ - findRenderedDOMComponentWithTag: function(root, tagName) { - validateClassInstance(root, 'findRenderedDOMComponentWithTag'); - const all = ReactTestUtils.scryRenderedDOMComponentsWithTag(root, tagName); - if (all.length !== 1) { - throw new Error( - 'Did not find exactly one match (found: ' + - all.length + - ') ' + - 'for tag:' + - tagName, - ); - } - return all[0]; - }, +function isCompositeComponent(inst) { + if (isDOMComponent(inst)) { + // Accessing inst.setState warns; just return false as that'll be what + // this returns when we have DOM nodes as refs directly + return false; + } + return ( + inst != null && + typeof inst.render === 'function' && + typeof inst.setState === 'function' + ); +} - /** - * Finds all instances of components with type equal to `componentType`. - * @return {array} an array of all the matches. - */ - scryRenderedComponentsWithType: function(root, componentType) { - validateClassInstance(root, 'scryRenderedComponentsWithType'); - return ReactTestUtils.findAllInRenderedTree(root, function(inst) { - return ReactTestUtils.isCompositeComponentWithType(inst, componentType); - }); - }, +function isCompositeComponentWithType(inst, type) { + if (!isCompositeComponent(inst)) { + return false; + } + const internalInstance = getInstance(inst); + const constructor = internalInstance.type; + return constructor === type; +} - /** - * Same as `scryRenderedComponentsWithType` but expects there to be one result - * and returns that one result, or throws exception if there is any other - * number of matches besides one. - * @return {!ReactComponent} The one match. - */ - findRenderedComponentWithType: function(root, componentType) { - validateClassInstance(root, 'findRenderedComponentWithType'); - const all = ReactTestUtils.scryRenderedComponentsWithType( - root, - componentType, - ); - if (all.length !== 1) { - throw new Error( - 'Did not find exactly one match (found: ' + - all.length + - ') ' + - 'for componentType:' + - componentType, - ); - } - return all[0]; - }, +function findAllInRenderedTree(inst, test) { + validateClassInstance(inst, 'findAllInRenderedTree'); + if (!inst) { + return []; + } + const internalInstance = getInstance(inst); + return findAllInRenderedFiberTreeInternal(internalInstance, test); +} - /** - * Pass a mocked component module to this method to augment it with - * useful methods that allow it to be used as a dummy React component. - * Instead of rendering as usual, the component will become a simple - *
containing any provided children. - * - * @param {object} module the mock function object exported from a - * module that defines the component to be mocked - * @param {?string} mockTagName optional dummy root tag name to return - * from render method (overrides - * module.mockTagName if provided) - * @return {object} the ReactTestUtils object (for chaining) - */ - mockComponent: function(module, mockTagName) { - if (__DEV__) { - if (!hasWarnedAboutDeprecatedMockComponent) { - hasWarnedAboutDeprecatedMockComponent = true; - console.warn( - 'ReactTestUtils.mockComponent() is deprecated. ' + - 'Use shallow rendering or jest.mock() instead.\n\n' + - 'See https://fb.me/test-utils-mock-component for more information.', +/** + * Finds all instance of components in the rendered tree that are DOM + * components with the class name matching `className`. + * @return {array} an array of all the matches. + */ +function scryRenderedDOMComponentsWithClass(root, classNames) { + validateClassInstance(root, 'scryRenderedDOMComponentsWithClass'); + return findAllInRenderedTree(root, function(inst) { + if (isDOMComponent(inst)) { + let className = inst.className; + if (typeof className !== 'string') { + // SVG, probably. + className = inst.getAttribute('class') || ''; + } + const classList = className.split(/\s+/); + + if (!Array.isArray(classNames)) { + invariant( + classNames !== undefined, + 'TestUtils.scryRenderedDOMComponentsWithClass expects a ' + + 'className as a second argument.', ); + classNames = classNames.split(/\s+/); } + return classNames.every(function(name) { + return classList.indexOf(name) !== -1; + }); } + return false; + }); +} - mockTagName = mockTagName || module.mockTagName || 'div'; +/** + * Like scryRenderedDOMComponentsWithClass but expects there to be one result, + * and returns that one result, or throws exception if there is any other + * number of matches besides one. + * @return {!ReactDOMComponent} The one match. + */ +function findRenderedDOMComponentWithClass(root, className) { + validateClassInstance(root, 'findRenderedDOMComponentWithClass'); + const all = scryRenderedDOMComponentsWithClass(root, className); + if (all.length !== 1) { + throw new Error( + 'Did not find exactly one match (found: ' + + all.length + + ') ' + + 'for class:' + + className, + ); + } + return all[0]; +} - module.prototype.render.mockImplementation(function() { - return React.createElement(mockTagName, null, this.props.children); - }); +/** + * Finds all instance of components in the rendered tree that are DOM + * components with the tag name matching `tagName`. + * @return {array} an array of all the matches. + */ +function scryRenderedDOMComponentsWithTag(root, tagName) { + validateClassInstance(root, 'scryRenderedDOMComponentsWithTag'); + return findAllInRenderedTree(root, function(inst) { + return ( + isDOMComponent(inst) && + inst.tagName.toUpperCase() === tagName.toUpperCase() + ); + }); +} - return this; - }, +/** + * Like scryRenderedDOMComponentsWithTag but expects there to be one result, + * and returns that one result, or throws exception if there is any other + * number of matches besides one. + * @return {!ReactDOMComponent} The one match. + */ +function findRenderedDOMComponentWithTag(root, tagName) { + validateClassInstance(root, 'findRenderedDOMComponentWithTag'); + const all = scryRenderedDOMComponentsWithTag(root, tagName); + if (all.length !== 1) { + throw new Error( + 'Did not find exactly one match (found: ' + + all.length + + ') ' + + 'for tag:' + + tagName, + ); + } + return all[0]; +} - nativeTouchData: function(x, y) { - return { - touches: [{pageX: x, pageY: y}], - }; - }, +/** + * Finds all instances of components with type equal to `componentType`. + * @return {array} an array of all the matches. + */ +function scryRenderedComponentsWithType(root, componentType) { + validateClassInstance(root, 'scryRenderedComponentsWithType'); + return findAllInRenderedTree(root, function(inst) { + return isCompositeComponentWithType(inst, componentType); + }); +} - Simulate: null, - SimulateNative: {}, +/** + * Same as `scryRenderedComponentsWithType` but expects there to be one result + * and returns that one result, or throws exception if there is any other + * number of matches besides one. + * @return {!ReactComponent} The one match. + */ +function findRenderedComponentWithType(root, componentType) { + validateClassInstance(root, 'findRenderedComponentWithType'); + const all = scryRenderedComponentsWithType(root, componentType); + if (all.length !== 1) { + throw new Error( + 'Did not find exactly one match (found: ' + + all.length + + ') ' + + 'for componentType:' + + componentType, + ); + } + return all[0]; +} - act, -}; +/** + * Pass a mocked component module to this method to augment it with + * useful methods that allow it to be used as a dummy React component. + * Instead of rendering as usual, the component will become a simple + *
containing any provided children. + * + * @param {object} module the mock function object exported from a + * module that defines the component to be mocked + * @param {?string} mockTagName optional dummy root tag name to return + * from render method (overrides + * module.mockTagName if provided) + * @return {object} the ReactTestUtils object (for chaining) + */ +function mockComponent(module, mockTagName) { + if (__DEV__) { + if (!hasWarnedAboutDeprecatedMockComponent) { + hasWarnedAboutDeprecatedMockComponent = true; + console.warn( + 'ReactTestUtils.mockComponent() is deprecated. ' + + 'Use shallow rendering or jest.mock() instead.\n\n' + + 'See https://fb.me/test-utils-mock-component for more information.', + ); + } + } + + mockTagName = mockTagName || module.mockTagName || 'div'; + + module.prototype.render.mockImplementation(function() { + return React.createElement(mockTagName, null, this.props.children); + }); + + return this; +} + +function nativeTouchData(x, y) { + return { + touches: [{pageX: x, pageY: y}], + }; +} + +const Simulate = {}; +const SimulateNative = {}; /** * Exports: * - * - `ReactTestUtils.Simulate.click(Element)` - * - `ReactTestUtils.Simulate.mouseMove(Element)` - * - `ReactTestUtils.Simulate.change(Element)` + * - `Simulate.click(Element)` + * - `Simulate.mouseMove(Element)` + * - `Simulate.change(Element)` * - ... (All keys from event plugin `eventTypes` objects) */ function makeSimulator(eventType) { @@ -407,7 +397,7 @@ function makeSimulator(eventType) { 'Note that TestUtils.Simulate will not work if you are using shallow rendering.', ); invariant( - !ReactTestUtils.isCompositeComponent(domNode), + !isCompositeComponent(domNode), 'TestUtils.Simulate expected a DOM node as the first argument but received ' + 'a component instance. Pass the DOM node you wish to simulate the event on instead.', ); @@ -450,15 +440,13 @@ function makeSimulator(eventType) { } function buildSimulators() { - ReactTestUtils.Simulate = {}; - let eventType; for (eventType in eventNameDispatchConfigs) { /** * @param {!Element|ReactDOMComponent} domComponentOrNode * @param {?object} eventData Fake event data to use in SyntheticEvent. */ - ReactTestUtils.Simulate[eventType] = makeSimulator(eventType); + Simulate[eventType] = makeSimulator(eventType); } } @@ -467,16 +455,16 @@ buildSimulators(); /** * Exports: * - * - `ReactTestUtils.SimulateNative.click(Element/ReactDOMComponent)` - * - `ReactTestUtils.SimulateNative.mouseMove(Element/ReactDOMComponent)` - * - `ReactTestUtils.SimulateNative.mouseIn/ReactDOMComponent)` - * - `ReactTestUtils.SimulateNative.mouseOut(Element/ReactDOMComponent)` + * - `SimulateNative.click(Element/ReactDOMComponent)` + * - `SimulateNative.mouseMove(Element/ReactDOMComponent)` + * - `SimulateNative.mouseIn/ReactDOMComponent)` + * - `SimulateNative.mouseOut(Element/ReactDOMComponent)` * - ... (All keys from `BrowserEventConstants.topLevelTypes`) * * Note: Top level event types are a subset of the entire set of handler types * (which include a broader set of "synthetic" events). For example, onDragDone * is a synthetic event. Except when testing an event plugin or React's event - * handling code specifically, you probably want to use ReactTestUtils.Simulate + * handling code specifically, you probably want to use Simulate * to dispatch synthetic events. */ @@ -484,7 +472,7 @@ function makeNativeSimulator(eventType, topLevelType) { return function(domComponentOrNode, nativeEventData) { const fakeNativeEvent = new Event(eventType); Object.assign(fakeNativeEvent, nativeEventData); - if (ReactTestUtils.isDOMComponent(domComponentOrNode)) { + if (isDOMComponent(domComponentOrNode)) { simulateNativeEventOnDOMComponent( topLevelType, domComponentOrNode, @@ -576,10 +564,27 @@ function makeNativeSimulator(eventType, topLevelType) { * @param {!Element|ReactDOMComponent} domComponentOrNode * @param {?Event} nativeEventData Fake native event to use in SyntheticEvent. */ - ReactTestUtils.SimulateNative[eventType] = makeNativeSimulator( - eventType, - topLevelType, - ); + SimulateNative[eventType] = makeNativeSimulator(eventType, topLevelType); }); -export default ReactTestUtils; +export { + renderIntoDocument, + isElement, + isElementOfType, + isDOMComponent, + isDOMComponentElement, + isCompositeComponent, + isCompositeComponentWithType, + findAllInRenderedTree, + scryRenderedDOMComponentsWithClass, + findRenderedDOMComponentWithClass, + scryRenderedDOMComponentsWithTag, + findRenderedDOMComponentWithTag, + scryRenderedComponentsWithType, + findRenderedComponentWithType, + mockComponent, + nativeTouchData, + Simulate, + SimulateNative, + act, +}; diff --git a/packages/react-dom/src/test-utils/ReactTestUtilsAct.js b/packages/react-dom/src/test-utils/ReactTestUtilsAct.js index 7aa5b916820de..cdf387cc1ce75 100644 --- a/packages/react-dom/src/test-utils/ReactTestUtilsAct.js +++ b/packages/react-dom/src/test-utils/ReactTestUtilsAct.js @@ -9,7 +9,7 @@ import type {Thenable} from 'react-reconciler/src/ReactFiberWorkLoop'; -import ReactDOM from 'react-dom'; +import * as ReactDOM from 'react-dom'; import ReactSharedInternals from 'shared/ReactSharedInternals'; import enqueueTask from 'shared/enqueueTask'; import * as Scheduler from 'scheduler'; diff --git a/packages/react-dom/src/unstable-native-dependencies/ReactDOMUnstableNativeDependencies.js b/packages/react-dom/src/unstable-native-dependencies/ReactDOMUnstableNativeDependencies.js index 26b8f728cd804..622819940bd28 100644 --- a/packages/react-dom/src/unstable-native-dependencies/ReactDOMUnstableNativeDependencies.js +++ b/packages/react-dom/src/unstable-native-dependencies/ReactDOMUnstableNativeDependencies.js @@ -5,7 +5,7 @@ * LICENSE file in the root directory of this source tree. */ -import ReactDOM from 'react-dom'; +import * as ReactDOM from 'react-dom'; import {setComponentTree} from 'legacy-events/EventPluginUtils'; import ResponderEventPlugin from 'legacy-events/ResponderEventPlugin'; import ResponderTouchHistoryStore from 'legacy-events/ResponderTouchHistoryStore'; diff --git a/packages/react-dom/test-utils.js b/packages/react-dom/test-utils.js index 1d4f69189e026..3eaa6b7b8c5d1 100644 --- a/packages/react-dom/test-utils.js +++ b/packages/react-dom/test-utils.js @@ -7,10 +7,4 @@ * @flow */ -'use strict'; - -const ReactTestUtils = require('./src/test-utils/ReactTestUtils'); - -// TODO: decide on the top-level export form. -// This is hacky but makes it work with both Rollup and Jest. -module.exports = ReactTestUtils.default || ReactTestUtils; +export * from './src/test-utils/ReactTestUtils'; diff --git a/packages/react-dom/testing.classic.fb.js b/packages/react-dom/testing.classic.fb.js new file mode 100644 index 0000000000000..841f6a1d8f8db --- /dev/null +++ b/packages/react-dom/testing.classic.fb.js @@ -0,0 +1,11 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +export * from './index.classic.fb.js'; +export {act} from 'react-reconciler/inline.dom'; diff --git a/packages/react-dom/testing.experimental.js b/packages/react-dom/testing.experimental.js new file mode 100644 index 0000000000000..0727208336ccd --- /dev/null +++ b/packages/react-dom/testing.experimental.js @@ -0,0 +1,11 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +export * from './index.experimental.js'; +export {act} from 'react-reconciler/inline.dom'; diff --git a/packages/react-dom/testing.js b/packages/react-dom/testing.js index 2a016ba16e9db..6169f8727267a 100644 --- a/packages/react-dom/testing.js +++ b/packages/react-dom/testing.js @@ -7,10 +7,5 @@ * @flow */ -'use strict'; - -const ReactDOM = require('./src/client/ReactDOM'); - -// TODO: decide on the top-level export form. -// This is hacky but makes it work with both Rollup and Jest. -module.exports = ReactDOM.default || ReactDOM; +export * from './index.js'; +export {act} from 'react-reconciler/inline.dom'; diff --git a/packages/react-dom/testing.modern.fb.js b/packages/react-dom/testing.modern.fb.js new file mode 100644 index 0000000000000..4eca948d49902 --- /dev/null +++ b/packages/react-dom/testing.modern.fb.js @@ -0,0 +1,11 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +export * from './index.modern.fb.js'; +export {act} from 'react-reconciler/inline.dom'; diff --git a/packages/react-dom/testing.stable.js b/packages/react-dom/testing.stable.js new file mode 100644 index 0000000000000..474972b70d816 --- /dev/null +++ b/packages/react-dom/testing.stable.js @@ -0,0 +1,11 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +export * from './index.stable.js'; +export {act} from 'react-reconciler/inline.dom'; diff --git a/packages/react-dom/unstable-fizz.browser.js b/packages/react-dom/unstable-fizz.browser.js index 4fb175ec6825e..322d4e364e1ae 100644 --- a/packages/react-dom/unstable-fizz.browser.js +++ b/packages/react-dom/unstable-fizz.browser.js @@ -7,10 +7,4 @@ * @flow */ -'use strict'; - -const ReactDOMFizzServerBrowser = require('./src/server/ReactDOMFizzServerBrowser'); - -// TODO: decide on the top-level export form. -// This is hacky but makes it work with both Rollup and Jest -module.exports = ReactDOMFizzServerBrowser.default || ReactDOMFizzServerBrowser; +export * from './src/server/ReactDOMFizzServerBrowser'; diff --git a/packages/react-dom/unstable-fizz.js b/packages/react-dom/unstable-fizz.js index 81d1ccee698d1..835fb25dca4a3 100644 --- a/packages/react-dom/unstable-fizz.js +++ b/packages/react-dom/unstable-fizz.js @@ -7,6 +7,4 @@ * @flow */ -'use strict'; - -module.exports = require('./unstable-fizz.node'); +export * from './unstable-fizz.node'; diff --git a/packages/react-dom/unstable-fizz.node.js b/packages/react-dom/unstable-fizz.node.js index ef943c74eccf5..ae7462ed70713 100644 --- a/packages/react-dom/unstable-fizz.node.js +++ b/packages/react-dom/unstable-fizz.node.js @@ -7,10 +7,4 @@ * @flow */ -'use strict'; - -const ReactDOMFizzServerNode = require('./src/server/ReactDOMFizzServerNode'); - -// TODO: decide on the top-level export form. -// This is hacky but makes it work with both Rollup and Jest -module.exports = ReactDOMFizzServerNode.default || ReactDOMFizzServerNode; +export * from './src/server/ReactDOMFizzServerNode'; diff --git a/packages/react-dom/unstable-native-dependencies.js b/packages/react-dom/unstable-native-dependencies.js index d44d492b8f198..ec502fb890690 100644 --- a/packages/react-dom/unstable-native-dependencies.js +++ b/packages/react-dom/unstable-native-dependencies.js @@ -7,6 +7,4 @@ * @flow */ -'use strict'; - -module.exports = require('./src/unstable-native-dependencies/ReactDOMUnstableNativeDependencies'); +export * from './src/unstable-native-dependencies/ReactDOMUnstableNativeDependencies'; diff --git a/packages/react-flight-dom-webpack/index.js b/packages/react-flight-dom-webpack/index.js index 5b82a9d829acf..67e9a28e029db 100644 --- a/packages/react-flight-dom-webpack/index.js +++ b/packages/react-flight-dom-webpack/index.js @@ -7,10 +7,4 @@ * @flow */ -'use strict'; - -const ReactFlightDOMClient = require('./src/ReactFlightDOMClient'); - -// TODO: decide on the top-level export form. -// This is hacky but makes it work with both Rollup and Jest -module.exports = ReactFlightDOMClient.default || ReactFlightDOMClient; +export * from './src/ReactFlightDOMClient'; diff --git a/packages/react-flight-dom-webpack/server.browser.js b/packages/react-flight-dom-webpack/server.browser.js index b6d542749d0c4..2329292ffc6af 100644 --- a/packages/react-flight-dom-webpack/server.browser.js +++ b/packages/react-flight-dom-webpack/server.browser.js @@ -7,11 +7,4 @@ * @flow */ -'use strict'; - -const ReactFlightDOMServerBrowser = require('./src/ReactFlightDOMServerBrowser'); - -// TODO: decide on the top-level export form. -// This is hacky but makes it work with both Rollup and Jest -module.exports = - ReactFlightDOMServerBrowser.default || ReactFlightDOMServerBrowser; +export * from './src/ReactFlightDOMServerBrowser'; diff --git a/packages/react-flight-dom-webpack/server.js b/packages/react-flight-dom-webpack/server.js index 03006336ba4fe..6010f4e3d5b22 100644 --- a/packages/react-flight-dom-webpack/server.js +++ b/packages/react-flight-dom-webpack/server.js @@ -7,6 +7,4 @@ * @flow */ -'use strict'; - -module.exports = require('./server.node'); +export * from './server.node'; diff --git a/packages/react-flight-dom-webpack/server.node.js b/packages/react-flight-dom-webpack/server.node.js index 1cbf1781b0f86..352e53b0cc6f1 100644 --- a/packages/react-flight-dom-webpack/server.node.js +++ b/packages/react-flight-dom-webpack/server.node.js @@ -7,10 +7,4 @@ * @flow */ -'use strict'; - -const ReactFlightDOMServerNode = require('./src/ReactFlightDOMServerNode'); - -// TODO: decide on the top-level export form. -// This is hacky but makes it work with both Rollup and Jest -module.exports = ReactFlightDOMServerNode.default || ReactFlightDOMServerNode; +export * from './src/ReactFlightDOMServerNode'; diff --git a/packages/react-flight-dom-webpack/src/ReactFlightDOMClient.js b/packages/react-flight-dom-webpack/src/ReactFlightDOMClient.js index e9c44f25021e5..908ffee1a4ba7 100644 --- a/packages/react-flight-dom-webpack/src/ReactFlightDOMClient.js +++ b/packages/react-flight-dom-webpack/src/ReactFlightDOMClient.js @@ -79,8 +79,4 @@ function readFromXHR(request: XMLHttpRequest): ReactModelRoot { return getModelRoot(response); } -export default { - readFromXHR, - readFromFetch, - readFromReadableStream, -}; +export {readFromXHR, readFromFetch, readFromReadableStream}; diff --git a/packages/react-flight-dom-webpack/src/ReactFlightDOMServerBrowser.js b/packages/react-flight-dom-webpack/src/ReactFlightDOMServerBrowser.js index 2aaf9ed3c0c4d..b7538e48a2bdf 100644 --- a/packages/react-flight-dom-webpack/src/ReactFlightDOMServerBrowser.js +++ b/packages/react-flight-dom-webpack/src/ReactFlightDOMServerBrowser.js @@ -29,6 +29,4 @@ function renderToReadableStream(model: ReactModel): ReadableStream { }); } -export default { - renderToReadableStream, -}; +export {renderToReadableStream}; diff --git a/packages/react-flight-dom-webpack/src/ReactFlightDOMServerNode.js b/packages/react-flight-dom-webpack/src/ReactFlightDOMServerNode.js index 9e6fa042647ea..6fde6197f4a31 100644 --- a/packages/react-flight-dom-webpack/src/ReactFlightDOMServerNode.js +++ b/packages/react-flight-dom-webpack/src/ReactFlightDOMServerNode.js @@ -26,6 +26,4 @@ function pipeToNodeWritable(model: ReactModel, destination: Writable): void { startWork(request); } -export default { - pipeToNodeWritable, -}; +export {pipeToNodeWritable}; diff --git a/packages/react-flight/index.js b/packages/react-flight/index.js index 7abd9455bd0aa..67fd8e660489a 100644 --- a/packages/react-flight/index.js +++ b/packages/react-flight/index.js @@ -17,10 +17,4 @@ // `react-server/inline-typed` (which *is*) for the current renderer. // On CI, we run Flow checks for each renderer separately. -'use strict'; - -const ReactFlightClient = require('./src/ReactFlightClient'); - -// TODO: decide on the top-level export form. -// This is hacky but makes it work with both Rollup and Jest. -module.exports = ReactFlightClient.default || ReactFlightClient; +export * from './src/ReactFlightClient'; diff --git a/packages/react-flight/package.json b/packages/react-flight/package.json index e6eabd3c4b02e..88c50d4dc58de 100644 --- a/packages/react-flight/package.json +++ b/packages/react-flight/package.json @@ -29,8 +29,7 @@ }, "dependencies": { "loose-envify": "^1.1.0", - "object-assign": "^4.1.1", - "prop-types": "^15.6.2" + "object-assign": "^4.1.1" }, "browserify": { "transform": [ diff --git a/packages/react-interactions/events/context-menu.js b/packages/react-interactions/events/context-menu.js index 3844d285ffc2e..d5a0953841351 100644 --- a/packages/react-interactions/events/context-menu.js +++ b/packages/react-interactions/events/context-menu.js @@ -7,6 +7,4 @@ * @flow */ -'use strict'; - -module.exports = require('./src/dom/ContextMenu'); +export * from './src/dom/ContextMenu'; diff --git a/packages/react-interactions/events/focus.js b/packages/react-interactions/events/focus.js index bf20a55d0efed..25ed14068a582 100644 --- a/packages/react-interactions/events/focus.js +++ b/packages/react-interactions/events/focus.js @@ -7,6 +7,4 @@ * @flow */ -'use strict'; - -module.exports = require('./src/dom/Focus'); +export * from './src/dom/Focus'; diff --git a/packages/react-interactions/events/hover.js b/packages/react-interactions/events/hover.js index 377f71510bb34..f630f2b605d76 100644 --- a/packages/react-interactions/events/hover.js +++ b/packages/react-interactions/events/hover.js @@ -7,6 +7,4 @@ * @flow */ -'use strict'; - -module.exports = require('./src/dom/Hover'); +export * from './src/dom/Hover'; diff --git a/packages/react-interactions/events/input.js b/packages/react-interactions/events/input.js index 39e7ad3b15eb6..80ce61ef45a85 100644 --- a/packages/react-interactions/events/input.js +++ b/packages/react-interactions/events/input.js @@ -7,6 +7,4 @@ * @flow */ -'use strict'; - -module.exports = require('./src/dom/Input'); +export * from './src/dom/Input'; diff --git a/packages/react-interactions/events/keyboard.js b/packages/react-interactions/events/keyboard.js index ffb65d759ec37..c86699dfe57ca 100644 --- a/packages/react-interactions/events/keyboard.js +++ b/packages/react-interactions/events/keyboard.js @@ -7,6 +7,4 @@ * @flow */ -'use strict'; - -module.exports = require('./src/dom/Keyboard'); +export * from './src/dom/Keyboard'; diff --git a/packages/react-interactions/events/press-legacy.js b/packages/react-interactions/events/press-legacy.js index 4b2d60e7f81d0..81398ac3753ba 100644 --- a/packages/react-interactions/events/press-legacy.js +++ b/packages/react-interactions/events/press-legacy.js @@ -7,6 +7,4 @@ * @flow */ -'use strict'; - -module.exports = require('./src/dom/PressLegacy'); +export * from './src/dom/PressLegacy'; diff --git a/packages/react-interactions/events/press.js b/packages/react-interactions/events/press.js index c7eee75eeab13..2802eba048116 100644 --- a/packages/react-interactions/events/press.js +++ b/packages/react-interactions/events/press.js @@ -7,6 +7,4 @@ * @flow */ -'use strict'; - -module.exports = require('./src/dom/Press'); +export * from './src/dom/Press'; diff --git a/packages/react-interactions/events/src/dom/ContextMenu.js b/packages/react-interactions/events/src/dom/ContextMenu.js index 1861eef1a624a..5304e6be527ff 100644 --- a/packages/react-interactions/events/src/dom/ContextMenu.js +++ b/packages/react-interactions/events/src/dom/ContextMenu.js @@ -14,7 +14,7 @@ import type { } from 'shared/ReactDOMTypes'; import type {ReactEventResponderListener} from 'shared/ReactTypes'; -import React from 'react'; +import * as React from 'react'; import {DiscreteEvent} from 'shared/ReactTypes'; type ContextMenuProps = {| diff --git a/packages/react-interactions/events/src/dom/Focus.js b/packages/react-interactions/events/src/dom/Focus.js index d97723dfe6201..5b4e184915280 100644 --- a/packages/react-interactions/events/src/dom/Focus.js +++ b/packages/react-interactions/events/src/dom/Focus.js @@ -14,7 +14,7 @@ import type { } from 'shared/ReactDOMTypes'; import type {ReactEventResponderListener} from 'shared/ReactTypes'; -import React from 'react'; +import * as React from 'react'; import {DiscreteEvent} from 'shared/ReactTypes'; /** @@ -36,7 +36,7 @@ type FocusState = { isFocused: boolean, isFocusVisible: boolean, pointerType: PointerType, - ... + addedRootEvents?: boolean, }; type FocusProps = { @@ -416,6 +416,7 @@ const focusResponderImpl = { isFocused: false, isFocusVisible: false, pointerType: '', + addedRootEvents: false, }; }, onMount() { @@ -622,7 +623,10 @@ const focusWithinResponderImpl = { onBeforeBlurWithin, DiscreteEvent, ); - context.addRootEventTypes(rootEventTypes); + if (!state.addedRootEvents) { + state.addedRootEvents = true; + context.addRootEventTypes(rootEventTypes); + } } else { // We want to propagate to next focusWithin responder // if this responder doesn't handle beforeblur @@ -660,7 +664,10 @@ const focusWithinResponderImpl = { if (detachedTarget !== null && detachedTarget === event.target) { dispatchBlurWithinEvents(context, event, props, state); state.detachedTarget = null; - context.removeRootEventTypes(rootEventTypes); + if (state.addedRootEvents) { + state.addedRootEvents = false; + context.removeRootEventTypes(rootEventTypes); + } } } }, diff --git a/packages/react-interactions/events/src/dom/Hover.js b/packages/react-interactions/events/src/dom/Hover.js index 503ee787ec253..73c18714c9211 100644 --- a/packages/react-interactions/events/src/dom/Hover.js +++ b/packages/react-interactions/events/src/dom/Hover.js @@ -14,7 +14,7 @@ import type { } from 'shared/ReactDOMTypes'; import type {ReactEventResponderListener} from 'shared/ReactTypes'; -import React from 'react'; +import * as React from 'react'; import {UserBlockingEvent} from 'shared/ReactTypes'; type HoverProps = { diff --git a/packages/react-interactions/events/src/dom/Input.js b/packages/react-interactions/events/src/dom/Input.js index ba4c6f5f7d52d..f7f40546d4ff3 100644 --- a/packages/react-interactions/events/src/dom/Input.js +++ b/packages/react-interactions/events/src/dom/Input.js @@ -12,7 +12,7 @@ import type { ReactDOMResponderContext, } from 'shared/ReactDOMTypes'; -import React from 'react'; +import * as React from 'react'; import {DiscreteEvent} from 'shared/ReactTypes'; import type {ReactEventResponderListener} from 'shared/ReactTypes'; diff --git a/packages/react-interactions/events/src/dom/Keyboard.js b/packages/react-interactions/events/src/dom/Keyboard.js index 289fc9067065c..fa3575849cd3e 100644 --- a/packages/react-interactions/events/src/dom/Keyboard.js +++ b/packages/react-interactions/events/src/dom/Keyboard.js @@ -13,7 +13,7 @@ import type { } from 'shared/ReactDOMTypes'; import type {ReactEventResponderListener} from 'shared/ReactTypes'; -import React from 'react'; +import * as React from 'react'; import {DiscreteEvent} from 'shared/ReactTypes'; import {isVirtualClick} from './shared'; diff --git a/packages/react-interactions/events/src/dom/Press.js b/packages/react-interactions/events/src/dom/Press.js index 6c5a240eab491..4cdef4aad4dea 100644 --- a/packages/react-interactions/events/src/dom/Press.js +++ b/packages/react-interactions/events/src/dom/Press.js @@ -9,7 +9,7 @@ import type {PointerType} from 'shared/ReactDOMTypes'; -import React from 'react'; +import * as React from 'react'; import {useTap} from 'react-interactions/events/tap'; import {useKeyboard} from 'react-interactions/events/keyboard'; diff --git a/packages/react-interactions/events/src/dom/PressLegacy.js b/packages/react-interactions/events/src/dom/PressLegacy.js index c81bd3de9a5ff..c9791ce3d7b4f 100644 --- a/packages/react-interactions/events/src/dom/PressLegacy.js +++ b/packages/react-interactions/events/src/dom/PressLegacy.js @@ -17,7 +17,7 @@ import type { ReactEventResponderListener, } from 'shared/ReactTypes'; -import React from 'react'; +import * as React from 'react'; import {DiscreteEvent, UserBlockingEvent} from 'shared/ReactTypes'; type PressProps = {| @@ -126,6 +126,7 @@ const rootEventTypes = hasPointerEvents 'click', 'keyup', 'scroll', + 'blur', ] : [ 'click', @@ -138,6 +139,7 @@ const rootEventTypes = hasPointerEvents 'dragstart', 'mouseup_active', 'touchend', + 'blur', ]; function isFunction(obj): boolean { @@ -881,6 +883,14 @@ const pressResponderImpl = { case 'touchcancel': case 'dragstart': { dispatchCancel(event, context, props, state); + break; + } + case 'blur': { + // If we encounter a blur that happens on the pressed target + // then disengage the blur. + if (isPressed && target === state.pressTarget) { + dispatchCancel(event, context, props, state); + } } } }, diff --git a/packages/react-interactions/events/src/dom/Tap.js b/packages/react-interactions/events/src/dom/Tap.js index 127483d26dc44..9d81531099f8a 100644 --- a/packages/react-interactions/events/src/dom/Tap.js +++ b/packages/react-interactions/events/src/dom/Tap.js @@ -14,7 +14,7 @@ import type { } from 'shared/ReactDOMTypes'; import type {ReactEventResponderListener} from 'shared/ReactTypes'; -import React from 'react'; +import * as React from 'react'; import { buttonsEnum, dispatchDiscreteEvent, @@ -103,6 +103,7 @@ const rootEventTypes = hasPointerEvents 'pointermove', 'pointercancel', 'scroll', + 'blur', ] : [ 'click_active', @@ -114,6 +115,7 @@ const rootEventTypes = hasPointerEvents 'touchmove', 'touchcancel', 'scroll', + 'blur', ]; /** @@ -697,6 +699,13 @@ const responderImpl = { removeRootEventTypes(context, state); break; } + case 'blur': { + // If we encounter a blur that happens on the pressed target + // then disengage the blur. + if (state.isActive && nativeEvent.target === state.responderTarget) { + dispatchCancel(context, props, state); + } + } } }, onUnmount( diff --git a/packages/react-interactions/events/src/dom/__tests__/ContextMenu-test.internal.js b/packages/react-interactions/events/src/dom/__tests__/ContextMenu-test.internal.js index f57d416459776..affcf8e6fbfa5 100644 --- a/packages/react-interactions/events/src/dom/__tests__/ContextMenu-test.internal.js +++ b/packages/react-interactions/events/src/dom/__tests__/ContextMenu-test.internal.js @@ -38,6 +38,11 @@ const table = [[forcePointerEvents], [!forcePointerEvents]]; describe.each(table)('ContextMenu responder', hasPointerEvents => { let container; + if (!__EXPERIMENTAL__) { + it("empty test so Jest doesn't complain", () => {}); + return; + } + beforeEach(() => { initializeModules(hasPointerEvents); container = document.createElement('div'); diff --git a/packages/react-interactions/events/src/dom/__tests__/Focus-test.internal.js b/packages/react-interactions/events/src/dom/__tests__/Focus-test.internal.js index 66946f07abcce..0b751d5c063dc 100644 --- a/packages/react-interactions/events/src/dom/__tests__/Focus-test.internal.js +++ b/packages/react-interactions/events/src/dom/__tests__/Focus-test.internal.js @@ -38,6 +38,11 @@ const table = [[forcePointerEvents], [!forcePointerEvents]]; describe.each(table)('Focus responder', hasPointerEvents => { let container; + if (!__EXPERIMENTAL__) { + it("empty test so Jest doesn't complain", () => {}); + return; + } + beforeEach(() => { initializeModules(hasPointerEvents); container = document.createElement('div'); diff --git a/packages/react-interactions/events/src/dom/__tests__/FocusWithin-test.internal.js b/packages/react-interactions/events/src/dom/__tests__/FocusWithin-test.internal.js index 0c2b783b03c0e..6d474b429bd2e 100644 --- a/packages/react-interactions/events/src/dom/__tests__/FocusWithin-test.internal.js +++ b/packages/react-interactions/events/src/dom/__tests__/FocusWithin-test.internal.js @@ -16,6 +16,7 @@ let ReactFeatureFlags; let ReactDOM; let FocusWithinResponder; let useFocusWithin; +let Scheduler; const initializeModules = hasPointerEvents => { setPointerEvent(hasPointerEvents); @@ -27,6 +28,7 @@ const initializeModules = hasPointerEvents => { FocusWithinResponder = require('react-interactions/events/focus') .FocusWithinResponder; useFocusWithin = require('react-interactions/events/focus').useFocusWithin; + Scheduler = require('scheduler'); }; const forcePointerEvents = true; @@ -35,6 +37,11 @@ const table = [[forcePointerEvents], [!forcePointerEvents]]; describe.each(table)('FocusWithin responder', hasPointerEvents => { let container; + if (!__EXPERIMENTAL__) { + it("empty test so Jest doesn't complain", () => {}); + return; + } + beforeEach(() => { initializeModules(); container = document.createElement('div'); @@ -336,6 +343,68 @@ describe.each(table)('FocusWithin responder', hasPointerEvents => { expect.objectContaining({isTargetAttached: false}), ); }); + + it.experimental( + 'is called after a focused suspended element is hidden', + () => { + const Suspense = React.Suspense; + let suspend = false; + let resolve; + let promise = new Promise(resolvePromise => (resolve = resolvePromise)); + + function Child() { + if (suspend) { + throw promise; + } else { + return ; + } + } + + const Component = ({show}) => { + const listener = useFocusWithin({ + onBeforeBlurWithin, + onBlurWithin, + }); + + return ( +
+ + + +
+ ); + }; + + const container2 = document.createElement('div'); + document.body.appendChild(container2); + + let root = ReactDOM.createRoot(container2); + root.render(); + Scheduler.unstable_flushAll(); + jest.runAllTimers(); + expect(container2.innerHTML).toBe('
'); + + const inner = innerRef.current; + const target = createEventTarget(inner); + target.keydown({key: 'Tab'}); + target.focus(); + expect(onBeforeBlurWithin).toHaveBeenCalledTimes(0); + expect(onBlurWithin).toHaveBeenCalledTimes(0); + + suspend = true; + root.render(); + Scheduler.unstable_flushAll(); + jest.runAllTimers(); + expect(container2.innerHTML).toBe( + '
Loading...
', + ); + expect(onBeforeBlurWithin).toHaveBeenCalledTimes(1); + expect(onBlurWithin).toHaveBeenCalledTimes(1); + resolve(); + + document.body.removeChild(container2); + }, + ); }); it('expect displayName to show up for event component', () => { diff --git a/packages/react-interactions/events/src/dom/__tests__/Hover-test.internal.js b/packages/react-interactions/events/src/dom/__tests__/Hover-test.internal.js index 06437852b45c9..35f71ebabafee 100644 --- a/packages/react-interactions/events/src/dom/__tests__/Hover-test.internal.js +++ b/packages/react-interactions/events/src/dom/__tests__/Hover-test.internal.js @@ -34,6 +34,11 @@ const table = [[forcePointerEvents], [!forcePointerEvents]]; describe.each(table)('Hover responder', hasPointerEvents => { let container; + if (!__EXPERIMENTAL__) { + it("empty test so Jest doesn't complain", () => {}); + return; + } + beforeEach(() => { initializeModules(hasPointerEvents); container = document.createElement('div'); diff --git a/packages/react-interactions/events/src/dom/__tests__/Input-test.internal.js b/packages/react-interactions/events/src/dom/__tests__/Input-test.internal.js index cf1bb6670f57d..5248e58a067f0 100644 --- a/packages/react-interactions/events/src/dom/__tests__/Input-test.internal.js +++ b/packages/react-interactions/events/src/dom/__tests__/Input-test.internal.js @@ -45,6 +45,11 @@ const modulesInit = () => { describe('Input event responder', () => { let container; + if (!__EXPERIMENTAL__) { + it("empty test so Jest doesn't complain", () => {}); + return; + } + beforeEach(() => { jest.resetModules(); modulesInit(); diff --git a/packages/react-interactions/events/src/dom/__tests__/Keyboard-test.internal.js b/packages/react-interactions/events/src/dom/__tests__/Keyboard-test.internal.js index 68a69dfba6c52..bd649765d5e60 100644 --- a/packages/react-interactions/events/src/dom/__tests__/Keyboard-test.internal.js +++ b/packages/react-interactions/events/src/dom/__tests__/Keyboard-test.internal.js @@ -28,6 +28,11 @@ function initializeModules(hasPointerEvents) { describe('Keyboard responder', () => { let container; + if (!__EXPERIMENTAL__) { + it("empty test so Jest doesn't complain", () => {}); + return; + } + beforeEach(() => { initializeModules(); container = document.createElement('div'); diff --git a/packages/react-interactions/events/src/dom/__tests__/MixedResponders-test-internal.js b/packages/react-interactions/events/src/dom/__tests__/MixedResponders-test-internal.js index aeb265329ed27..6dc3ee24ec7f6 100644 --- a/packages/react-interactions/events/src/dom/__tests__/MixedResponders-test-internal.js +++ b/packages/react-interactions/events/src/dom/__tests__/MixedResponders-test-internal.js @@ -19,6 +19,11 @@ let Scheduler; describe('mixing responders with the heritage event system', () => { let container; + if (!__EXPERIMENTAL__) { + it("empty test so Jest doesn't complain", () => {}); + return; + } + beforeEach(() => { ReactFeatureFlags = require('shared/ReactFeatureFlags'); ReactFeatureFlags.enableDeprecatedFlareAPI = true; diff --git a/packages/react-interactions/events/src/dom/__tests__/Press-test.internal.js b/packages/react-interactions/events/src/dom/__tests__/Press-test.internal.js index 6987280090aae..07aa6c5e3bdce 100644 --- a/packages/react-interactions/events/src/dom/__tests__/Press-test.internal.js +++ b/packages/react-interactions/events/src/dom/__tests__/Press-test.internal.js @@ -38,6 +38,11 @@ const pointerTypesTable = [['mouse'], ['touch']]; describeWithPointerEvent('Press responder', hasPointerEvents => { let container; + if (!__EXPERIMENTAL__) { + it("empty test so Jest doesn't complain", () => {}); + return; + } + beforeEach(() => { initializeModules(hasPointerEvents); container = document.createElement('div'); @@ -695,4 +700,25 @@ describeWithPointerEvent('Press responder', hasPointerEvents => { target.pointerup(); target.pointerdown(); }); + + it('when blur occurs on a pressed target, we should disengage press', () => { + const onPress = jest.fn(); + const onPressStart = jest.fn(); + const onPressEnd = jest.fn(); + const buttonRef = React.createRef(); + + const Component = () => { + const listener = usePress({onPress, onPressStart, onPressEnd}); + return