diff --git a/docs/docs/installation.md b/docs/docs/installation.md index 0039fc7750..d4ee06ae54 100644 --- a/docs/docs/installation.md +++ b/docs/docs/installation.md @@ -95,22 +95,26 @@ Similarly, you can render a React component inside a DOM element somewhere insid By default, React includes many helpful warnings. These warnings are very useful in development. However, they make React larger and slower so you should make sure to use the production version when you deploy the app. -#### Create React App +#### Brunch -If you use [Create React App](https://github.com/facebookincubator/create-react-app), `npm run build` will create an optimized build of your app in the `build` folder. - -#### Webpack - -Include both `DefinePlugin` and `UglifyJsPlugin` into your production Webpack configuration as described in [this guide](https://webpack.js.org/guides/production-build/). +To create an optimized production build with Brunch, just add the `-p` flag to the build command. See the [Brunch docs](http://brunch.io/docs/commands) for more details. #### Browserify Run Browserify with `NODE_ENV` environment variable set to `production` and use [UglifyJS](https://github.com/mishoo/UglifyJS) as the last build step so that development-only code gets stripped out. +#### Create React App + +If you use [Create React App](https://github.com/facebookincubator/create-react-app), `npm run build` will create an optimized build of your app in the `build` folder. + #### Rollup Use [rollup-plugin-replace](https://github.com/rollup/rollup-plugin-replace) plugin together with [rollup-plugin-commonjs](https://github.com/rollup/rollup-plugin-commonjs) (in that order) to remove development-only code. [See this gist](https://gist.github.com/Rich-Harris/cb14f4bc0670c47d00d191565be36bf0) for a complete setup example. +#### Webpack + +Include both `DefinePlugin` and `UglifyJsPlugin` into your production Webpack configuration as described in [this guide](https://webpack.js.org/guides/production-build/). + ### Using a CDN If you don't want to use npm to manage client packages, the `react` and `react-dom` npm packages also provide single-file distributions in `dist` folders, which are hosted on a CDN: diff --git a/docs/docs/optimizing-performance.md b/docs/docs/optimizing-performance.md index fe1d93077a..cbb1b9849b 100644 --- a/docs/docs/optimizing-performance.md +++ b/docs/docs/optimizing-performance.md @@ -11,20 +11,10 @@ Internally, React uses several clever techniques to minimize the number of costl If you're benchmarking or experiencing performance problems in your React apps, make sure you're testing with the minified production build: -* For Create React App, you need to run `npm run build` and follow the instructions. * For single-file builds, we offer production-ready `.min.js` versions. +* For Brunch, you need to add the `-p` flag to the `build` command. * For Browserify, you need to run it with `NODE_ENV=production`. -* For Webpack, you need to add this to plugins in your production config: - -```js -new webpack.DefinePlugin({ - 'process.env': { - NODE_ENV: JSON.stringify('production') - } -}), -new webpack.optimize.UglifyJsPlugin() -``` - +* For Create React App, you need to run `npm run build` and follow the instructions. * For Rollup, you need to use the [replace](https://github.com/rollup/rollup-plugin-replace) plugin *before* the [commonjs](https://github.com/rollup/rollup-plugin-commonjs) plugin so that development-only modules are not imported. For a complete setup example [see this gist](https://gist.github.com/Rich-Harris/cb14f4bc0670c47d00d191565be36bf0). ```js @@ -37,6 +27,17 @@ plugins: [ ] ``` +* For Webpack, you need to add this to plugins in your production config: + +```js +new webpack.DefinePlugin({ + 'process.env': { + NODE_ENV: JSON.stringify('production') + } +}), +new webpack.optimize.UglifyJsPlugin() +``` + The development build includes extra warnings that are helpful when building your apps, but it is slower due to the extra bookkeeping it does. ## Profiling Components with Chrome Timeline diff --git a/package.json b/package.json index 28f26f8ead..81e4550c91 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "babel-jest": "^19.0.0", "babel-plugin-check-es2015-constants": "^6.5.0", "babel-plugin-syntax-trailing-function-commas": "^6.5.0", + "babel-plugin-transform-async-to-generator": "^6.22.0", "babel-plugin-transform-class-properties": "^6.11.5", "babel-plugin-transform-es2015-arrow-functions": "^6.5.2", "babel-plugin-transform-es2015-block-scoped-functions": "^6.5.0", @@ -119,7 +120,7 @@ "./scripts/jest/environment.js" ], "setupTestFrameworkScriptFile": "./scripts/jest/test-framework-setup.js", - "testRegex": "/__tests__/", + "testRegex": "/__tests__/.*(\\.js|coffee|ts)$", "moduleFileExtensions": [ "js", "json", diff --git a/scripts/circleci/build_gh_pages.sh b/scripts/circleci/build_gh_pages.sh index fcf49f2af9..c0ecc81e35 100755 --- a/scripts/circleci/build_gh_pages.sh +++ b/scripts/circleci/build_gh_pages.sh @@ -2,7 +2,7 @@ set -e -if [ -z $CI_PULL_REQUEST ] && [ "$CIRCLE_BRANCH" = "$REACT_WEBSITE_BRANCH" ]; then +if [ "$CIRCLE_BRANCH" = "$REACT_WEBSITE_BRANCH" ]; then GH_PAGES_DIR=`pwd`/../react-gh-pages diff --git a/scripts/error-codes/codes.json b/scripts/error-codes/codes.json index e4662c0912..fb8006fdf2 100644 --- a/scripts/error-codes/codes.json +++ b/scripts/error-codes/codes.json @@ -142,5 +142,6 @@ "140": "Expected hook events to fire for the child before its parent includes it in onSetChildren().", "141": "Expected onSetChildren() to fire for a container child before its parent includes it in onSetChildren().", "142": "Expected onBeforeMountComponent() parent and onSetChildren() to be consistent (%s has parents %s and %s).", - "143": "React.Children.only expected to receive a single React element child." + "143": "React.Children.only expected to receive a single React element child.", + "144": "React.PropTypes type checking code is stripped in production." } diff --git a/scripts/fiber/record-tests b/scripts/fiber/record-tests index 78cfabce04..b7c57bf18a 100755 --- a/scripts/fiber/record-tests +++ b/scripts/fiber/record-tests @@ -151,7 +151,7 @@ function recordTests(maxWorkers, trackFacts) { ); if (trackFacts) { - const fact = runResults.numPassedTests + '/' + runResults.numTotalTests; + const fact = runResults.numPassedTests + '/' + (runResults.numPassedTests + runResults.numFailedTests); // TODO: Shelling out here is silly. child_process.spawnSync( process.execPath, diff --git a/scripts/fiber/tests-failing.txt b/scripts/fiber/tests-failing.txt index 9c2bebbd84..325811e749 100644 --- a/scripts/fiber/tests-failing.txt +++ b/scripts/fiber/tests-failing.txt @@ -1,21 +1,6 @@ src/isomorphic/classic/__tests__/ReactContextValidator-test.js * should pass previous context to lifecycles -src/renderers/__tests__/ReactComponentTreeHook-test.js -* can be retrieved by ID - -src/renderers/__tests__/ReactHostOperationHistoryHook-test.js -* gets recorded during an update - -src/renderers/__tests__/ReactPerf-test.js -* should count no-op update as waste -* should count no-op update in child as waste -* should include stats for components unmounted during measurement -* should include lifecycle methods in measurements -* should include render time of functional components -* should not count time in a portal towards lifecycle method -* should work when measurement starts during reconciliation - src/renderers/dom/shared/__tests__/ReactDOMComponent-test.js * gives source code refs for unknown prop warning (ssr) * gives source code refs for unknown prop warning for exact elements (ssr) diff --git a/scripts/fiber/tests-passing-except-dev.txt b/scripts/fiber/tests-passing-except-dev.txt index bd22db6cba..ea25216a39 100644 --- a/scripts/fiber/tests-passing-except-dev.txt +++ b/scripts/fiber/tests-passing-except-dev.txt @@ -1,132 +1,3 @@ -src/renderers/__tests__/ReactComponentTreeHook-test.js -* uses displayName or Unknown for classic components -* uses displayName, name, or ReactComponent for modern components -* uses displayName, name, or Object for factory components -* uses displayName, name, or StatelessComponent for functional components -* reports a host tree correctly -* reports a simple tree with composites correctly -* reports a tree with composites correctly -* ignores null children -* ignores false children -* reports text nodes as children -* reports a single text node as a child -* reports a single number node as a child -* reports a zero as a child -* skips empty nodes for multiple children -* reports html content as no children -* updates text of a single text child -* updates from no children to a single text child -* updates from a single text child to no children -* updates from html content to a single text child -* updates from a single text child to html content -* updates from no children to multiple text children -* updates from multiple text children to no children -* updates from html content to multiple text children -* updates from multiple text children to html content -* updates from html content to no children -* updates from no children to html content -* updates from one text child to multiple text children -* updates from multiple text children to one text child -* updates text nodes when reordering -* updates host nodes when reordering with keys -* updates host nodes when reordering without keys -* updates a single composite child of a different type -* updates a single composite child of the same type -* updates from no children to a single composite child -* updates from a single composite child to no children -* updates mixed children -* updates with a host child -* updates from null to a host child -* updates from a host child to null -* updates from a host child to a composite child -* updates from a composite child to a host child -* updates from null to a composite child -* updates from a composite child to null -* updates with a host child -* updates from null to a host child -* updates from a host child to null -* updates from a host child to a composite child -* updates from a composite child to a host child -* updates from null to a composite child -* updates from a composite child to null -* tracks owner correctly -* purges unmounted components automatically -* reports update counts -* does not report top-level wrapper as a root -* registers inlined text nodes -* works - -src/renderers/__tests__/ReactComponentTreeHook-test.native.js -* uses displayName or Unknown for classic components -* uses displayName, name, or ReactComponent for modern components -* uses displayName, name, or Object for factory components -* uses displayName, name, or StatelessComponent for functional components -* reports a host tree correctly -* reports a simple tree with composites correctly -* reports a tree with composites correctly -* ignores null children -* ignores false children -* reports text nodes as children -* reports a single text node as a child -* reports a single number node as a child -* reports a zero as a child -* skips empty nodes for multiple children -* updates text of a single text child -* updates from no children to a single text child -* updates from a single text child to no children -* updates from no children to multiple text children -* updates from multiple text children to no children -* updates from one text child to multiple text children -* updates from multiple text children to one text child -* updates text nodes when reordering -* updates host nodes when reordering with keys -* updates host nodes when reordering with keys -* updates a single composite child of a different type -* updates a single composite child of the same type -* updates from no children to a single composite child -* updates from a single composite child to no children -* updates mixed children -* updates with a host child -* updates from null to a host child -* updates from a host child to null -* updates from a host child to a composite child -* updates from a composite child to a host child -* updates from null to a composite child -* updates from a composite child to null -* updates with a host child -* updates from null to a host child -* updates from a host child to null -* updates from a host child to a composite child -* updates from a composite child to a host child -* updates from null to a composite child -* updates from a composite child to null -* tracks owner correctly -* purges unmounted components automatically -* reports update counts -* does not report top-level wrapper as a root - -src/renderers/__tests__/ReactHostOperationHistoryHook-test.js -* gets recorded for host roots -* gets recorded for composite roots -* gets recorded when a native is mounted deeply instead of null -* gets recorded during mount -* gets recorded during an update -* gets ignored if the styles are shallowly equal -* gets recorded during mount -* gets recorded during mount -* gets recorded during mount -* gets recorded during an update from text content -* gets recorded during an update from html -* gets recorded during an update from children -* gets recorded when composite renders to a different type -* gets recorded when composite renders to null after a native -* gets recorded during an update from text content -* gets recorded during an update from html -* gets recorded during an update from children -* gets reported when a child is inserted -* gets reported when a child is inserted -* gets reported when a child is removed - src/renderers/dom/shared/__tests__/ReactDOMComponent-test.js * should not warn when server-side rendering `onScroll` * should warn about incorrect casing on properties (ssr) diff --git a/scripts/fiber/tests-passing.txt b/scripts/fiber/tests-passing.txt index ade485d62d..e6ffdc28ab 100644 --- a/scripts/fiber/tests-passing.txt +++ b/scripts/fiber/tests-passing.txt @@ -157,13 +157,6 @@ src/isomorphic/children/__tests__/onlyChild-test.js * should not fail when passed interpolated single child * should return the only child -src/isomorphic/children/__tests__/sliceChildren-test.js -* should render the whole set if start zero is supplied -* should render the remaining set if no end index is supplied -* should exclude everything at or after the end index -* should allow static children to be sliced -* should slice nested children - src/isomorphic/classic/__tests__/ReactContextValidator-test.js * should filter out context not in contextTypes * should pass next context to lifecycles @@ -568,8 +561,6 @@ src/renderers/__tests__/ReactComponentLifeCycle-test.js src/renderers/__tests__/ReactComponentTreeHook-test.js * gets created -* is created during mounting -* is created when calling renderToString during render src/renderers/__tests__/ReactCompositeComponent-test.js * should support module pattern components @@ -680,17 +671,6 @@ src/renderers/__tests__/ReactErrorBoundaries-test.js * renders empty output if error boundary does not handle the error * passes first error when two errors happen in commit -src/renderers/__tests__/ReactHostOperationHistoryHook-test.js -* gets ignored for composite roots that return null -* gets recorded during an update -* gets recorded as a removal during an update -* gets recorded during an update -* gets recorded during an update -* gets ignored if new text is equal -* gets ignored if new text is equal -* gets ignored if the type has not changed -* gets ignored if new html is equal - src/renderers/__tests__/ReactIdentity-test.js * should allow key property to express identity * should use composite identity @@ -757,41 +737,6 @@ src/renderers/__tests__/ReactMultiChildText-test.js * should throw if rendering both HTML and children * should render between nested components and inline children -src/renderers/__tests__/ReactPerf-test.js -* should not count initial render as waste -* should not count unmount as waste -* should not count content update as waste -* should not count child addition as waste -* should not count child removal as waste -* should not count property update as waste -* should not count style update as waste -* should not count property removal as waste -* should not count raw HTML update as waste -* should not count child reordering as waste -* should not count text update as waste -* should not count replacing null with a host as waste -* should not count replacing a host with null as waste -* warns once when using getMeasurementsSummaryMap -* warns once when using printDOM -* returns isRunning state -* start has no effect when already running -* stop has no effect when already stopped -* should print console error only once -* should not print errant warnings if render() throws -* should not print errant warnings if componentWillMount() throws -* should not print errant warnings if componentDidMount() throws -* should not print errant warnings if portal throws in render() -* should not print errant warnings if portal throws in componentWillMount() -* should not print errant warnings if portal throws in componentDidMount() - -src/renderers/__tests__/ReactStateSetters-test.js -* createStateSetter should update state -* createStateKeySetter should update state -* createStateKeySetter is memoized -* createStateSetter should update state from mixin -* createStateKeySetter should update state with mixin -* createStateKeySetter is memoized with mixin - src/renderers/__tests__/ReactStatelessComponent-test.js * should render stateless component * should update stateless component @@ -1580,6 +1525,21 @@ src/renderers/shared/fiber/__tests__/ReactIncrementalErrorHandling-test.js * should ignore errors thrown in log method to prevent cycle * should relay info about error boundary and retry attempts if applicable +src/renderers/shared/fiber/__tests__/ReactIncrementalPerf-test.js +* measures a simple reconciliation +* skips parents during setState +* warns on cascading renders from setState +* warns on cascading renders from top-level render +* does not treat setState from cWM or cWRP as cascading +* captures all lifecycles +* measures deprioritized work +* measures deferred work in chunks +* recovers from fatal errors +* recovers from caught errors +* deduplicates lifecycle names during commit to reduce overhead +* supports coroutines +* supports portals + src/renderers/shared/fiber/__tests__/ReactIncrementalReflection-test.js * handles isMounted even when the initial render is deferred * handles isMounted when an unmount is deferred @@ -1656,7 +1616,6 @@ src/renderers/shared/shared/event/__tests__/EventPluginRegistry-test.js * should publish registration names of injected plugins * should throw if multiple registration names collide * should throw if an invalid event is published -* should be able to get the plugin from synthetic events src/renderers/shared/shared/event/eventPlugins/__tests__/ResponderEventPlugin-test.js * should do nothing when no one wants to respond @@ -1695,6 +1654,7 @@ src/renderers/shared/utils/__tests__/ReactErrorUtils-test.js * should return null if no error is thrown (development) * can nest with same debug name (development) * does not return nested errors (development) +* can be shimmed (development) * it should rethrow errors caught by invokeGuardedCallbackAndCatchFirstError (production) * should call the callback the passed arguments (production) * should call the callback with the provided context (production) @@ -1702,6 +1662,7 @@ src/renderers/shared/utils/__tests__/ReactErrorUtils-test.js * should return null if no error is thrown (production) * can nest with same debug name (production) * does not return nested errors (production) +* can be shimmed (production) src/renderers/shared/utils/__tests__/accumulateInto-test.js * throws if the second item is null diff --git a/scripts/jest/preprocessor.js b/scripts/jest/preprocessor.js index 20f372dc57..4a48cfe9e5 100644 --- a/scripts/jest/preprocessor.js +++ b/scripts/jest/preprocessor.js @@ -22,6 +22,7 @@ var pathToBabel = path.join(require.resolve('babel-core'), '..', 'package.json') var pathToModuleMap = require.resolve('fbjs/module-map'); var pathToBabelPluginDevWithCode = require.resolve('../error-codes/dev-expression-with-codes'); var pathToBabelPluginModules = require.resolve('fbjs-scripts/babel-6/rewrite-modules'); +var pathToBabelPluginAsyncToGenerator = require.resolve('babel-plugin-transform-async-to-generator'); var pathToBabelrc = path.join(__dirname, '..', '..', '.babelrc'); var pathToErrorCodes = require.resolve('../error-codes/codes.json'); @@ -54,11 +55,17 @@ module.exports = { !filePath.match(/\/node_modules\//) && !filePath.match(/\/third_party\//) ) { + // for test files, we also apply the async-await transform, but we want to + // make sure we don't accidentally apply that transform to product code. + var isTestFile = !!filePath.match(/\/__tests__\//); return babel.transform( src, Object.assign( {filename: path.relative(process.cwd(), filePath)}, - babelOptions + babelOptions, + isTestFile ? { + plugins: [pathToBabelPluginAsyncToGenerator].concat(babelOptions.plugins), + } : {} ) ).code; } diff --git a/src/addons/link/ReactStateSetters.js b/src/addons/link/ReactStateSetters.js deleted file mode 100644 index 977826d301..0000000000 --- a/src/addons/link/ReactStateSetters.js +++ /dev/null @@ -1,104 +0,0 @@ -/** - * Copyright 2013-present, Facebook, Inc. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. - * - * @providesModule ReactStateSetters - */ - -'use strict'; - -var ReactStateSetters = { - /** - * Returns a function that calls the provided function, and uses the result - * of that to set the component's state. - * - * @param {ReactCompositeComponent} component - * @param {function} funcReturningState Returned callback uses this to - * determine how to update state. - * @return {function} callback that when invoked uses funcReturningState to - * determined the object literal to setState. - */ - createStateSetter: function(component, funcReturningState) { - return function(a, b, c, d, e, f) { - var partialState = funcReturningState.call(component, a, b, c, d, e, f); - if (partialState) { - component.setState(partialState); - } - }; - }, - - /** - * Returns a single-argument callback that can be used to update a single - * key in the component's state. - * - * Note: this is memoized function, which makes it inexpensive to call. - * - * @param {ReactCompositeComponent} component - * @param {string} key The key in the state that you should update. - * @return {function} callback of 1 argument which calls setState() with - * the provided keyName and callback argument. - */ - createStateKeySetter: function(component, key) { - // Memoize the setters. - var cache = component.__keySetters || (component.__keySetters = {}); - return cache[key] || (cache[key] = createStateKeySetter(component, key)); - }, -}; - -function createStateKeySetter(component, key) { - // Partial state is allocated outside of the function closure so it can be - // reused with every call, avoiding memory allocation when this function - // is called. - var partialState = {}; - return function stateKeySetter(value) { - partialState[key] = value; - component.setState(partialState); - }; -} - -ReactStateSetters.Mixin = { - /** - * Returns a function that calls the provided function, and uses the result - * of that to set the component's state. - * - * For example, these statements are equivalent: - * - * this.setState({x: 1}); - * this.createStateSetter(function(xValue) { - * return {x: xValue}; - * })(1); - * - * @param {function} funcReturningState Returned callback uses this to - * determine how to update state. - * @return {function} callback that when invoked uses funcReturningState to - * determined the object literal to setState. - */ - createStateSetter: function(funcReturningState) { - return ReactStateSetters.createStateSetter(this, funcReturningState); - }, - - /** - * Returns a single-argument callback that can be used to update a single - * key in the component's state. - * - * For example, these statements are equivalent: - * - * this.setState({x: 1}); - * this.createStateKeySetter('x')(1); - * - * Note: this is memoized function, which makes it inexpensive to call. - * - * @param {string} key The key in the state that you should update. - * @return {function} callback of 1 argument which calls setState() with - * the provided keyName and callback argument. - */ - createStateKeySetter: function(key) { - return ReactStateSetters.createStateKeySetter(this, key); - }, -}; - -module.exports = ReactStateSetters; diff --git a/src/isomorphic/children/__tests__/sliceChildren-test.js b/src/isomorphic/children/__tests__/sliceChildren-test.js deleted file mode 100644 index d09ab6757f..0000000000 --- a/src/isomorphic/children/__tests__/sliceChildren-test.js +++ /dev/null @@ -1,93 +0,0 @@ -/** - * Copyright 2013-present, Facebook, Inc. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. - * - * @emails react-core - */ - -'use strict'; - -describe('sliceChildren', () => { - - var React; - - var sliceChildren; - - beforeEach(() => { - React = require('react'); - - sliceChildren = require('sliceChildren'); - }); - - it('should render the whole set if start zero is supplied', () => { - var fullSet = [ -
, -
, -
, - ]; - var children = sliceChildren(fullSet, 0); - expect(children).toEqual([ -
, -
, -
, - ]); - }); - - it('should render the remaining set if no end index is supplied', () => { - var fullSet = [ -
, -
, -
, - ]; - var children = sliceChildren(fullSet, 1); - expect(children).toEqual([ -
, -
, - ]); - }); - - it('should exclude everything at or after the end index', () => { - var fullSet = [ -
, -
, -
, -
, - ]; - var children = sliceChildren(fullSet, 1, 2); - expect(children).toEqual([ -
, - ]); - }); - - it('should allow static children to be sliced', () => { - var a = ; - var b = ; - var c = ; - - var el =
{a}{b}{c}
; - var children = sliceChildren(el.props.children, 1, 2); - expect(children).toEqual([ - , - ]); - }); - - it('should slice nested children', () => { - var fullSet = [ -
, - [ -
, -
, - ], -
, - ]; - var children = sliceChildren(fullSet, 1, 2); - expect(children).toEqual([ -
, - ]); - }); - -}); diff --git a/src/isomorphic/children/sliceChildren.js b/src/isomorphic/children/sliceChildren.js deleted file mode 100644 index 691af1da34..0000000000 --- a/src/isomorphic/children/sliceChildren.js +++ /dev/null @@ -1,34 +0,0 @@ -/** - * Copyright 2013-present, Facebook, Inc. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. - * - * @providesModule sliceChildren - */ - -'use strict'; - -var ReactChildren = require('ReactChildren'); - -/** - * Slice children that are typically specified as `props.children`. This version - * of slice children ignores empty child components. - * - * @param {*} children The children set to filter. - * @param {number} start The first zero-based index to include in the subset. - * @param {?number} end The non-inclusive last index of the subset. - * @return {object} mirrored array with mapped children - */ -function sliceChildren(children, start, end) { - if (children == null) { - return children; - } - - var array = ReactChildren.toArray(children); - return array.slice(start, end); -} - -module.exports = sliceChildren; diff --git a/src/isomorphic/classic/element/ReactDebugCurrentFrame.js b/src/isomorphic/classic/element/ReactDebugCurrentFrame.js index f4efdd672b..ee8ab2248e 100644 --- a/src/isomorphic/classic/element/ReactDebugCurrentFrame.js +++ b/src/isomorphic/classic/element/ReactDebugCurrentFrame.js @@ -20,9 +20,11 @@ const ReactDebugCurrentFrame = {}; if (__DEV__) { var { getStackAddendumByID, - getStackAddendumByWorkInProgressFiber, getCurrentStackAddendum, } = require('ReactComponentTreeHook'); + var { + getStackAddendumByWorkInProgressFiber, + } = require('ReactFiberComponentTreeHook'); // Component that is being worked on ReactDebugCurrentFrame.current = (null : Fiber | DebugID | null); diff --git a/src/isomorphic/classic/element/ReactElementValidator.js b/src/isomorphic/classic/element/ReactElementValidator.js index b70fedc37e..f2a699dc9e 100644 --- a/src/isomorphic/classic/element/ReactElementValidator.js +++ b/src/isomorphic/classic/element/ReactElementValidator.js @@ -19,7 +19,6 @@ 'use strict'; var ReactCurrentOwner = require('ReactCurrentOwner'); -var ReactComponentTreeHook = require('ReactComponentTreeHook'); var ReactElement = require('ReactElement'); var checkReactTypeSpec = require('checkReactTypeSpec'); @@ -31,6 +30,9 @@ var getIteratorFn = require('getIteratorFn'); if (__DEV__) { var warning = require('fbjs/lib/warning'); var ReactDebugCurrentFrame = require('ReactDebugCurrentFrame'); + var { + getCurrentStackAddendum, + } = require('ReactComponentTreeHook'); } function getDeclarationErrorAddendum() { @@ -122,7 +124,7 @@ function validateExplicitKey(element, parentType) { '%s%s See https://fb.me/react-warning-keys for more information.%s', currentComponentErrorInfo, childOwner, - ReactComponentTreeHook.getCurrentStackAddendum(element) + getCurrentStackAddendum(element) ); } @@ -225,7 +227,7 @@ var ReactElementValidator = { info += getDeclarationErrorAddendum(); } - info += ReactComponentTreeHook.getCurrentStackAddendum(); + info += getCurrentStackAddendum(); warning( false, diff --git a/src/isomorphic/classic/types/__tests__/ReactPropTypesProduction-test.js b/src/isomorphic/classic/types/__tests__/ReactPropTypesProduction-test.js index 07a46a8774..67842710e4 100644 --- a/src/isomorphic/classic/types/__tests__/ReactPropTypesProduction-test.js +++ b/src/isomorphic/classic/types/__tests__/ReactPropTypesProduction-test.js @@ -49,7 +49,7 @@ describe('ReactPropTypesProduction', function() { 'prop' ); }).toThrowError( - 'React.PropTypes type checking code is stripped in production.' + 'Minified React error #144' ); } diff --git a/src/isomorphic/hooks/ReactComponentTreeHook.js b/src/isomorphic/hooks/ReactComponentTreeHook.js index 2359fd1c34..332c5cac8c 100644 --- a/src/isomorphic/hooks/ReactComponentTreeHook.js +++ b/src/isomorphic/hooks/ReactComponentTreeHook.js @@ -13,17 +13,13 @@ 'use strict'; var ReactCurrentOwner = require('ReactCurrentOwner'); -var ReactTypeOfWork = require('ReactTypeOfWork'); var { - IndeterminateComponent, - FunctionalComponent, - ClassComponent, - HostComponent, -} = ReactTypeOfWork; - -var getComponentName = require('getComponentName'); + getStackAddendumByWorkInProgressFiber, + describeComponentFrame, +} = require('ReactFiberComponentTreeHook'); var invariant = require('fbjs/lib/invariant'); var warning = require('fbjs/lib/warning'); +var getComponentName = require('getComponentName'); import type { ReactElement, Source } from 'ReactElementType'; import type { DebugID } from 'ReactInstanceType'; @@ -159,17 +155,6 @@ function purgeDeep(id) { } } -function describeComponentFrame(name, source, ownerName) { - return '\n in ' + (name || 'Unknown') + ( - source ? - ' (at ' + source.fileName.replace(/^.*[\\\/]/, '') + ':' + - source.lineNumber + ')' : - ownerName ? - ' (created by ' + ownerName + ')' : - '' - ); -} - function getDisplayName(element: ?ReactElement): string { if (element == null) { return '#empty'; @@ -183,10 +168,11 @@ function getDisplayName(element: ?ReactElement): string { } function describeID(id: DebugID): string { - var name = ReactComponentTreeHook.getDisplayName(id); - var element = ReactComponentTreeHook.getElement(id); - var ownerID = ReactComponentTreeHook.getOwnerID(id); - var ownerName; + const name = ReactComponentTreeHook.getDisplayName(id); + const element = ReactComponentTreeHook.getElement(id); + const ownerID = ReactComponentTreeHook.getOwnerID(id); + let ownerName; + if (ownerID) { ownerName = ReactComponentTreeHook.getDisplayName(ownerID); } @@ -196,26 +182,7 @@ function describeID(id: DebugID): string { 'building stack', id ); - return describeComponentFrame(name, element && element._source, ownerName); -} - -function describeFiber(fiber : Fiber) : string { - switch (fiber.tag) { - case IndeterminateComponent: - case FunctionalComponent: - case ClassComponent: - case HostComponent: - var owner = fiber._debugOwner; - var source = fiber._debugSource; - var name = getComponentName(fiber); - var ownerName = null; - if (owner) { - ownerName = getComponentName(owner); - } - return describeComponentFrame(name, source, ownerName); - default: - return ''; - } + return describeComponentFrame(name || '', element && element._source, ownerName || ''); } var ReactComponentTreeHook = { @@ -356,7 +323,7 @@ var ReactComponentTreeHook = { const workInProgress = ((currentOwner : any) : Fiber); // Safe because if current owner exists, we are reconciling, // and it is guaranteed to be the work-in-progress version. - info += ReactComponentTreeHook.getStackAddendumByWorkInProgressFiber(workInProgress); + info += getStackAddendumByWorkInProgressFiber(workInProgress); } else if (typeof currentOwner._debugID === 'number') { info += ReactComponentTreeHook.getStackAddendumByID(currentOwner._debugID); } @@ -373,20 +340,6 @@ var ReactComponentTreeHook = { return info; }, - // This function can only be called with a work-in-progress fiber and - // only during begin or complete phase. Do not call it under any other - // circumstances. - getStackAddendumByWorkInProgressFiber(workInProgress : Fiber) : string { - var info = ''; - var node = workInProgress; - do { - info += describeFiber(node); - // Otherwise this return pointer might point to the wrong tree: - node = node.return; - } while (node); - return info; - }, - getChildIDs(id: DebugID): Array { var item = getItem(id); return item ? item.childIDs : []; diff --git a/src/renderers/__tests__/ReactComponentTreeHook-test.js b/src/renderers/__tests__/ReactComponentTreeHook-test.js index a9c19c13eb..da91ab2627 100644 --- a/src/renderers/__tests__/ReactComponentTreeHook-test.js +++ b/src/renderers/__tests__/ReactComponentTreeHook-test.js @@ -11,6 +11,9 @@ 'use strict'; +var ReactDOMFeatureFlags = require('ReactDOMFeatureFlags'); +var describeStack = ReactDOMFeatureFlags.useFiber ? describe.skip : describe; + describe('ReactComponentTreeHook', () => { var React; var ReactDOM; @@ -30,6 +33,148 @@ describe('ReactComponentTreeHook', () => { ReactComponentTreeTestUtils = require('ReactComponentTreeTestUtils'); }); + // This is the only part used both by Stack and Fiber. + describe('stack addenda', () => { + it('gets created', () => { + function getAddendum(element) { + var addendum = ReactComponentTreeHook.getCurrentStackAddendum(element); + return addendum.replace(/\(at .+?:\d+\)/g, '(at **)'); + } + + var Anon = React.createClass({displayName: null, render: () => null}); + var Orange = React.createClass({render: () => null}); + + expectDev(getAddendum()).toBe( + '' + ); + expectDev(getAddendum(
)).toBe( + '\n in div (at **)' + ); + expectDev(getAddendum()).toBe( + '\n in Unknown (at **)' + ); + expectDev(getAddendum()).toBe( + '\n in Orange (at **)' + ); + expectDev(getAddendum(React.createElement(Orange))).toBe( + '\n in Orange' + ); + + var renders = 0; + var rOwnedByQ; + + function Q() { + return (rOwnedByQ = React.createElement(R)); + } + function R() { + return
; + } + class S extends React.Component { + componentDidMount() { + // Check that the parent path is still fetched when only S itself is on + // the stack. + this.forceUpdate(); + } + render() { + expectDev(getAddendum()).toBe( + '\n in S (at **)' + + '\n in div (at **)' + + '\n in R (created by Q)' + + '\n in Q (at **)' + ); + expectDev(getAddendum()).toBe( + '\n in span (at **)' + + '\n in S (at **)' + + '\n in div (at **)' + + '\n in R (created by Q)' + + '\n in Q (at **)' + ); + expectDev(getAddendum(React.createElement('span'))).toBe( + '\n in span (created by S)' + + '\n in S (at **)' + + '\n in div (at **)' + + '\n in R (created by Q)' + + '\n in Q (at **)' + ); + renders++; + return null; + } + } + ReactDOM.render(, document.createElement('div')); + expectDev(renders).toBe(2); + + // Make sure owner is fetched for the top element too. + expectDev(getAddendum(rOwnedByQ)).toBe( + '\n in R (created by Q)' + ); + }); + + // These are features and regression tests that only affect + // the Stack implementation of the stack addendum. + if (!ReactDOMFeatureFlags.useFiber) { + it('can be retrieved by ID', () => { + function getAddendum(id) { + var addendum = ReactComponentTreeHook.getStackAddendumByID(id); + return addendum.replace(/\(at .+?:\d+\)/g, '(at **)'); + } + + class Q extends React.Component { + render() { + return null; + } + } + + var q = ReactDOM.render(, document.createElement('div')); + expectDev(getAddendum(ReactInstanceMap.get(q)._debugID)).toBe( + '\n in Q (at **)' + ); + + spyOn(console, 'error'); + getAddendum(-17); + expectDev(console.error.calls.count()).toBe(1); + expectDev(console.error.calls.argsFor(0)[0]).toBe( + 'Warning: ReactComponentTreeHook: Missing React element for ' + + 'debugID -17 when building stack' + ); + }); + + it('is created during mounting', () => { + // https://github.com/facebook/react/issues/7187 + var el = document.createElement('div'); + var portalEl = document.createElement('div'); + class Foo extends React.Component { + componentWillMount() { + ReactDOM.render(
, portalEl); + } + render() { + return
; + } + } + ReactDOM.render(, el); + }); + + it('is created when calling renderToString during render', () => { + // https://github.com/facebook/react/issues/7190 + var el = document.createElement('div'); + class Foo extends React.Component { + render() { + return ( +
+
+ {ReactDOMServer.renderToString(
)} +
+
+ ); + } + } + ReactDOM.render(, el); + }); + } + }); + + // The rest of this file is not relevant for Fiber. + // TODO: remove tests below when we delete Stack. + function assertTreeMatches(pairs) { if (!Array.isArray(pairs[0])) { pairs = [pairs]; @@ -106,7 +251,7 @@ describe('ReactComponentTreeHook', () => { }); } - describe('mount', () => { + describeStack('mount', () => { it('uses displayName or Unknown for classic components', () => { class Foo extends React.Component { render() { @@ -520,7 +665,7 @@ describe('ReactComponentTreeHook', () => { }); }); - describe('update', () => { + describeStack('update', () => { describe('host component', () => { it('updates text of a single text child', () => { var elementBefore =
Hi.
; @@ -1601,276 +1746,144 @@ describe('ReactComponentTreeHook', () => { }); }); - it('tracks owner correctly', () => { - class Foo extends React.Component { - render() { - return

Hi.

; + describeStack('misc', () => { + it('tracks owner correctly', () => { + class Foo extends React.Component { + render() { + return

Hi.

; + } + } + function Bar({children}) { + return
{children} Mom
; } - } - function Bar({children}) { - return
{children} Mom
; - } - // Note that owner is not calculated for text nodes - // because they are not created from real elements. - var element =
; - var tree = { - displayName: 'article', - children: [{ - displayName: 'Foo', + // Note that owner is not calculated for text nodes + // because they are not created from real elements. + var element =
; + var tree = { + displayName: 'article', children: [{ - displayName: 'Bar', - ownerDisplayName: 'Foo', + displayName: 'Foo', children: [{ - displayName: 'div', - ownerDisplayName: 'Bar', + displayName: 'Bar', + ownerDisplayName: 'Foo', children: [{ - displayName: 'h1', - ownerDisplayName: 'Foo', + displayName: 'div', + ownerDisplayName: 'Bar', children: [{ + displayName: 'h1', + ownerDisplayName: 'Foo', + children: [{ + displayName: '#text', + text: 'Hi.', + }], + }, { displayName: '#text', - text: 'Hi.', + text: ' Mom', }], - }, { - displayName: '#text', - text: ' Mom', }], }], }], - }], - }; - assertTreeMatches([element, tree]); - }); - - it('purges unmounted components automatically', () => { - var node = document.createElement('div'); - var renderBar = true; - var fooInstance; - var barInstance; - - class Foo extends React.Component { - render() { - fooInstance = ReactInstanceMap.get(this); - return renderBar ? : null; - } - } - - class Bar extends React.Component { - render() { - barInstance = ReactInstanceMap.get(this); - return null; - } - } - - ReactDOM.render(, node); - ReactComponentTreeTestUtils.expectTree(barInstance._debugID, { - displayName: 'Bar', - parentDisplayName: 'Foo', - parentID: fooInstance._debugID, - children: [], - }, 'Foo'); - - renderBar = false; - ReactDOM.render(, node); - ReactDOM.render(, node); - ReactComponentTreeTestUtils.expectTree(barInstance._debugID, { - displayName: 'Unknown', - children: [], - parentID: null, - }, 'Foo'); - - ReactDOM.unmountComponentAtNode(node); - ReactComponentTreeTestUtils.expectTree(barInstance._debugID, { - displayName: 'Unknown', - children: [], - parentID: null, - }, 'Foo'); - }); - - it('reports update counts', () => { - var node = document.createElement('div'); - - ReactDOM.render(
, node); - var divID = ReactComponentTreeHook.getRootIDs()[0]; - expectDev(ReactComponentTreeHook.getUpdateCount(divID)).toEqual(0); - - ReactDOM.render(, node); - var spanID = ReactComponentTreeHook.getRootIDs()[0]; - expectDev(ReactComponentTreeHook.getUpdateCount(divID)).toEqual(0); - expectDev(ReactComponentTreeHook.getUpdateCount(spanID)).toEqual(0); - - ReactDOM.render(, node); - expectDev(ReactComponentTreeHook.getUpdateCount(divID)).toEqual(0); - expectDev(ReactComponentTreeHook.getUpdateCount(spanID)).toEqual(1); - - ReactDOM.render(, node); - expectDev(ReactComponentTreeHook.getUpdateCount(divID)).toEqual(0); - expectDev(ReactComponentTreeHook.getUpdateCount(spanID)).toEqual(2); - - ReactDOM.unmountComponentAtNode(node); - expectDev(ReactComponentTreeHook.getUpdateCount(divID)).toEqual(0); - expectDev(ReactComponentTreeHook.getUpdateCount(spanID)).toEqual(0); - }); - - it('does not report top-level wrapper as a root', () => { - var node = document.createElement('div'); - - ReactDOM.render(
, node); - expectDev(ReactComponentTreeTestUtils.getRootDisplayNames()).toEqual(['div']); - - ReactDOM.render(
, node); - expectDev(ReactComponentTreeTestUtils.getRootDisplayNames()).toEqual(['div']); - - ReactDOM.unmountComponentAtNode(node); - expectDev(ReactComponentTreeTestUtils.getRootDisplayNames()).toEqual([]); - expectDev(ReactComponentTreeTestUtils.getRegisteredDisplayNames()).toEqual([]); - }); - - it('registers inlined text nodes', () => { - var node = document.createElement('div'); - - ReactDOM.render(
hi
, node); - expectDev(ReactComponentTreeTestUtils.getRegisteredDisplayNames()).toEqual(['div', '#text']); - - ReactDOM.unmountComponentAtNode(node); - expectDev(ReactComponentTreeTestUtils.getRegisteredDisplayNames()).toEqual([]); - }); - - describe('stack addenda', () => { - it('gets created', () => { - function getAddendum(element) { - var addendum = ReactComponentTreeHook.getCurrentStackAddendum(element); - return addendum.replace(/\(at .+?:\d+\)/g, '(at **)'); - } - - var Anon = React.createClass({displayName: null, render: () => null}); - var Orange = React.createClass({render: () => null}); - - expectDev(getAddendum()).toBe( - '' - ); - expectDev(getAddendum(
)).toBe( - '\n in div (at **)' - ); - expectDev(getAddendum()).toBe( - '\n in Unknown (at **)' - ); - expectDev(getAddendum()).toBe( - '\n in Orange (at **)' - ); - expectDev(getAddendum(React.createElement(Orange))).toBe( - '\n in Orange' - ); - - var renders = 0; - var rOwnedByQ; - - function Q() { - return (rOwnedByQ = React.createElement(R)); - } - function R() { - return
; - } - class S extends React.Component { - componentDidMount() { - // Check that the parent path is still fetched when only S itself is on - // the stack. - this.forceUpdate(); - } - render() { - expectDev(getAddendum()).toBe( - '\n in S (at **)' + - '\n in div (at **)' + - '\n in R (created by Q)' + - '\n in Q (at **)' - ); - expectDev(getAddendum()).toBe( - '\n in span (at **)' + - '\n in S (at **)' + - '\n in div (at **)' + - '\n in R (created by Q)' + - '\n in Q (at **)' - ); - expectDev(getAddendum(React.createElement('span'))).toBe( - '\n in span (created by S)' + - '\n in S (at **)' + - '\n in div (at **)' + - '\n in R (created by Q)' + - '\n in Q (at **)' - ); - renders++; - return null; - } - } - ReactDOM.render(, document.createElement('div')); - expectDev(renders).toBe(2); - - // Make sure owner is fetched for the top element too. - expectDev(getAddendum(rOwnedByQ)).toBe( - '\n in R (created by Q)' - ); + }; + assertTreeMatches([element, tree]); }); - it('can be retrieved by ID', () => { - function getAddendum(id) { - var addendum = ReactComponentTreeHook.getStackAddendumByID(id); - return addendum.replace(/\(at .+?:\d+\)/g, '(at **)'); + it('purges unmounted components automatically', () => { + var node = document.createElement('div'); + var renderBar = true; + var fooInstance; + var barInstance; + + class Foo extends React.Component { + render() { + fooInstance = ReactInstanceMap.get(this); + return renderBar ? : null; + } } - class Q extends React.Component { + class Bar extends React.Component { render() { + barInstance = ReactInstanceMap.get(this); return null; } } - var q = ReactDOM.render(, document.createElement('div')); - expectDev(getAddendum(ReactInstanceMap.get(q)._debugID)).toBe( - '\n in Q (at **)' - ); + ReactDOM.render(, node); + ReactComponentTreeTestUtils.expectTree(barInstance._debugID, { + displayName: 'Bar', + parentDisplayName: 'Foo', + parentID: fooInstance._debugID, + children: [], + }, 'Foo'); - spyOn(console, 'error'); - getAddendum(-17); - expectDev(console.error.calls.count()).toBe(1); - expectDev(console.error.calls.argsFor(0)[0]).toBe( - 'Warning: ReactComponentTreeHook: Missing React element for ' + - 'debugID -17 when building stack' - ); + renderBar = false; + ReactDOM.render(, node); + ReactDOM.render(, node); + ReactComponentTreeTestUtils.expectTree(barInstance._debugID, { + displayName: 'Unknown', + children: [], + parentID: null, + }, 'Foo'); + + ReactDOM.unmountComponentAtNode(node); + ReactComponentTreeTestUtils.expectTree(barInstance._debugID, { + displayName: 'Unknown', + children: [], + parentID: null, + }, 'Foo'); }); - it('is created during mounting', () => { - // https://github.com/facebook/react/issues/7187 - var el = document.createElement('div'); - var portalEl = document.createElement('div'); - class Foo extends React.Component { - componentWillMount() { - ReactDOM.render(
, portalEl); - } - render() { - return
; - } - } - ReactDOM.render(, el); + it('reports update counts', () => { + var node = document.createElement('div'); + + ReactDOM.render(
, node); + var divID = ReactComponentTreeHook.getRootIDs()[0]; + expectDev(ReactComponentTreeHook.getUpdateCount(divID)).toEqual(0); + + ReactDOM.render(, node); + var spanID = ReactComponentTreeHook.getRootIDs()[0]; + expectDev(ReactComponentTreeHook.getUpdateCount(divID)).toEqual(0); + expectDev(ReactComponentTreeHook.getUpdateCount(spanID)).toEqual(0); + + ReactDOM.render(, node); + expectDev(ReactComponentTreeHook.getUpdateCount(divID)).toEqual(0); + expectDev(ReactComponentTreeHook.getUpdateCount(spanID)).toEqual(1); + + ReactDOM.render(, node); + expectDev(ReactComponentTreeHook.getUpdateCount(divID)).toEqual(0); + expectDev(ReactComponentTreeHook.getUpdateCount(spanID)).toEqual(2); + + ReactDOM.unmountComponentAtNode(node); + expectDev(ReactComponentTreeHook.getUpdateCount(divID)).toEqual(0); + expectDev(ReactComponentTreeHook.getUpdateCount(spanID)).toEqual(0); }); - it('is created when calling renderToString during render', () => { - // https://github.com/facebook/react/issues/7190 - var el = document.createElement('div'); - class Foo extends React.Component { - render() { - return ( -
-
- {ReactDOMServer.renderToString(
)} -
-
- ); - } - } - ReactDOM.render(, el); + it('does not report top-level wrapper as a root', () => { + var node = document.createElement('div'); + + ReactDOM.render(
, node); + expectDev(ReactComponentTreeTestUtils.getRootDisplayNames()).toEqual(['div']); + + ReactDOM.render(
, node); + expectDev(ReactComponentTreeTestUtils.getRootDisplayNames()).toEqual(['div']); + + ReactDOM.unmountComponentAtNode(node); + expectDev(ReactComponentTreeTestUtils.getRootDisplayNames()).toEqual([]); + expectDev(ReactComponentTreeTestUtils.getRegisteredDisplayNames()).toEqual([]); + }); + + it('registers inlined text nodes', () => { + var node = document.createElement('div'); + + ReactDOM.render(
hi
, node); + expectDev(ReactComponentTreeTestUtils.getRegisteredDisplayNames()).toEqual(['div', '#text']); + + ReactDOM.unmountComponentAtNode(node); + expectDev(ReactComponentTreeTestUtils.getRegisteredDisplayNames()).toEqual([]); }); }); - describe('in environment without Map, Set and Array.from', () => { + describeStack('in environment without Map, Set and Array.from', () => { var realMap; var realSet; var realArrayFrom; diff --git a/src/renderers/__tests__/ReactComponentTreeHook-test.native.js b/src/renderers/__tests__/ReactComponentTreeHook-test.native.js index 23c934c274..17a8fa4986 100644 --- a/src/renderers/__tests__/ReactComponentTreeHook-test.native.js +++ b/src/renderers/__tests__/ReactComponentTreeHook-test.native.js @@ -11,7 +11,15 @@ 'use strict'; -describe('ReactComponentTreeHook', () => { +var ReactNativeFeatureFlags = require('ReactNativeFeatureFlags'); +var describeStack = ReactNativeFeatureFlags.useFiber ? describe.skip : describe; + +// These tests are only relevant for the Stack version of the tree hook. +// This file is for RN. There is a sibling file that has some tree hook +// tests that are still relevant in Fiber. +// TODO: remove this file when we delete Stack. + +describeStack('ReactComponentTreeHook', () => { var React; var ReactNative; var ReactInstanceMap; diff --git a/src/renderers/__tests__/ReactHostOperationHistoryHook-test.js b/src/renderers/__tests__/ReactHostOperationHistoryHook-test.js index b830342b75..d6c12a6f4c 100644 --- a/src/renderers/__tests__/ReactHostOperationHistoryHook-test.js +++ b/src/renderers/__tests__/ReactHostOperationHistoryHook-test.js @@ -11,12 +11,16 @@ 'use strict'; -describe('ReactHostOperationHistoryHook', () => { +var ReactDOMFeatureFlags = require('ReactDOMFeatureFlags'); +var describeStack = ReactDOMFeatureFlags.useFiber ? describe.skip : describe; + +// This is only used by ReactPerf which is currently not supported on Fiber. +// Use browser timeline integration instead. +describeStack('ReactHostOperationHistoryHook', () => { var React; var ReactPerf; var ReactDOM; var ReactDOMComponentTree; - var ReactDOMFeatureFlags; var ReactHostOperationHistoryHook; beforeEach(() => { @@ -26,7 +30,6 @@ describe('ReactHostOperationHistoryHook', () => { ReactPerf = require('react-dom/lib/ReactPerf'); ReactDOM = require('react-dom'); ReactDOMComponentTree = require('ReactDOMComponentTree'); - ReactDOMFeatureFlags = require('ReactDOMFeatureFlags'); ReactHostOperationHistoryHook = require('ReactHostOperationHistoryHook'); ReactPerf.start(); diff --git a/src/renderers/__tests__/ReactPerf-test.js b/src/renderers/__tests__/ReactPerf-test.js index 7d5cb809c2..2abc0d17df 100644 --- a/src/renderers/__tests__/ReactPerf-test.js +++ b/src/renderers/__tests__/ReactPerf-test.js @@ -11,7 +11,12 @@ 'use strict'; -describe('ReactPerf', () => { +var ReactDOMFeatureFlags = require('ReactDOMFeatureFlags'); +var describeStack = ReactDOMFeatureFlags.useFiber ? describe.skip : describe; + +// ReactPerf is currently not supported on Fiber. +// Use browser timeline integration instead. +describeStack('ReactPerf', () => { var React; var ReactDOM; var ReactPerf; diff --git a/src/renderers/__tests__/ReactStateSetters-test.js b/src/renderers/__tests__/ReactStateSetters-test.js deleted file mode 100644 index dcc22a743b..0000000000 --- a/src/renderers/__tests__/ReactStateSetters-test.js +++ /dev/null @@ -1,152 +0,0 @@ -/** - * Copyright 2013-present, Facebook, Inc. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. - * - * @emails react-core - */ - -'use strict'; - -var React = require('react'); -var ReactStateSetters = require('ReactStateSetters'); -var ReactTestUtils = require('ReactTestUtils'); - -var TestComponent; -var TestComponentWithMixin; - -describe('ReactStateSetters', () => { - beforeEach(() => { - jest.resetModules(); - - TestComponent = class extends React.Component { - state = {foo: 'foo'}; - - render() { - return
; - } - }; - - TestComponentWithMixin = React.createClass({ - mixins: [ReactStateSetters.Mixin], - - getInitialState: function() { - return {foo: 'foo'}; - }, - - render: function() { - return
; - }, - }); - }); - - it('createStateSetter should update state', () => { - var instance = ; - instance = ReactTestUtils.renderIntoDocument(instance); - expect(instance.state).toEqual({foo: 'foo'}); - - var setter = ReactStateSetters.createStateSetter( - instance, - function(a, b, c) { - return { - foo: a + b + c, - bar: a * b * c, - }; - } - ); - expect(instance.state).toEqual({foo: 'foo'}); - - setter(1, 2, 3); - expect(instance.state).toEqual({foo: 6, bar: 6}); - - setter(10, 11, 12); - expect(instance.state).toEqual({foo: 33, bar: 1320}); - }); - - it('createStateKeySetter should update state', () => { - var instance = ; - instance = ReactTestUtils.renderIntoDocument(instance); - expect(instance.state).toEqual({foo: 'foo'}); - - var setter = ReactStateSetters.createStateKeySetter(instance, 'foo'); - - expect(instance.state).toEqual({foo: 'foo'}); - - setter('bar'); - expect(instance.state).toEqual({foo: 'bar'}); - - setter('baz'); - expect(instance.state).toEqual({foo: 'baz'}); - }); - - it('createStateKeySetter is memoized', () => { - var instance = ; - instance = ReactTestUtils.renderIntoDocument(instance); - expect(instance.state).toEqual({foo: 'foo'}); - - var foo1 = ReactStateSetters.createStateKeySetter(instance, 'foo'); - var bar1 = ReactStateSetters.createStateKeySetter(instance, 'bar'); - - var foo2 = ReactStateSetters.createStateKeySetter(instance, 'foo'); - var bar2 = ReactStateSetters.createStateKeySetter(instance, 'bar'); - - expect(foo2).toBe(foo1); - expect(bar2).toBe(bar1); - }); - - it('createStateSetter should update state from mixin', () => { - var instance = ; - instance = ReactTestUtils.renderIntoDocument(instance); - expect(instance.state).toEqual({foo: 'foo'}); - - var setter = instance.createStateSetter( - function(a, b, c) { - return { - foo: a + b + c, - bar: a * b * c, - }; - } - ); - expect(instance.state).toEqual({foo: 'foo'}); - - setter(1, 2, 3); - expect(instance.state).toEqual({foo: 6, bar: 6}); - - setter(10, 11, 12); - expect(instance.state).toEqual({foo: 33, bar: 1320}); - }); - - it('createStateKeySetter should update state with mixin', () => { - var instance = ; - instance = ReactTestUtils.renderIntoDocument(instance); - expect(instance.state).toEqual({foo: 'foo'}); - - var setter = instance.createStateKeySetter('foo'); - - expect(instance.state).toEqual({foo: 'foo'}); - - setter('bar'); - expect(instance.state).toEqual({foo: 'bar'}); - - setter('baz'); - expect(instance.state).toEqual({foo: 'baz'}); - }); - - it('createStateKeySetter is memoized with mixin', () => { - var instance = ; - instance = ReactTestUtils.renderIntoDocument(instance); - expect(instance.state).toEqual({foo: 'foo'}); - - var foo1 = instance.createStateKeySetter('foo'); - var bar1 = instance.createStateKeySetter('bar'); - - var foo2 = instance.createStateKeySetter('foo'); - var bar2 = instance.createStateKeySetter('bar'); - - expect(foo2).toBe(foo1); - expect(bar2).toBe(bar1); - }); -}); diff --git a/src/renderers/dom/fiber/ReactDOMFiber.js b/src/renderers/dom/fiber/ReactDOMFiber.js index cd0988f512..f1f61f1eb4 100644 --- a/src/renderers/dom/fiber/ReactDOMFiber.js +++ b/src/renderers/dom/fiber/ReactDOMFiber.js @@ -330,7 +330,7 @@ var DOMRenderer = ReactFiberReconciler({ scheduleDeferredCallback: ReactDOMFrameScheduling.rIC, - useSyncScheduling: true, + useSyncScheduling: !ReactDOMFeatureFlags.fiberAsyncScheduling, }); diff --git a/src/renderers/dom/fiber/wrappers/ReactDOMFiberInput.js b/src/renderers/dom/fiber/wrappers/ReactDOMFiberInput.js index 36be73bac9..d9e2467a9c 100644 --- a/src/renderers/dom/fiber/wrappers/ReactDOMFiberInput.js +++ b/src/renderers/dom/fiber/wrappers/ReactDOMFiberInput.js @@ -292,16 +292,16 @@ function updateNamedCousins(rootNode, props) { // and the same name are rendered into the same form (same as #1939). // That's probably okay; we don't support it just as we don't support // mixing React radio buttons with non-React ones. - var otherInstance = ReactDOMComponentTree.getInstanceFromNode(otherNode); + var otherProps = ReactDOMComponentTree.getFiberCurrentPropsFromNode(otherNode); invariant( - otherInstance, + otherProps, 'ReactDOMInput: Mixing React and non-React radio inputs with the ' + 'same `name` is not supported.' ); // If this is a controlled radio button group, forcing the input that // was previously checked to update will cause it to be come re-checked // as appropriate. - ReactDOMInput.updateWrapper(otherNode, otherInstance.memoizedProps); + ReactDOMInput.updateWrapper(otherNode, otherProps); } } } diff --git a/src/renderers/dom/shared/ReactDOMFeatureFlags.js b/src/renderers/dom/shared/ReactDOMFeatureFlags.js index 5e9d93267c..a53cc0dff5 100644 --- a/src/renderers/dom/shared/ReactDOMFeatureFlags.js +++ b/src/renderers/dom/shared/ReactDOMFeatureFlags.js @@ -12,6 +12,7 @@ 'use strict'; var ReactDOMFeatureFlags = { + fiberAsyncScheduling: false, useCreateElement: true, useFiber: false, }; diff --git a/src/renderers/dom/shared/__tests__/ReactDOMServerIntegration-test.js b/src/renderers/dom/shared/__tests__/ReactDOMServerIntegration-test.js index daf84a2f21..afe70f87cc 100644 --- a/src/renderers/dom/shared/__tests__/ReactDOMServerIntegration-test.js +++ b/src/renderers/dom/shared/__tests__/ReactDOMServerIntegration-test.js @@ -19,41 +19,44 @@ let ReactDOMServer; // Helper functions for rendering tests // ==================================== +// promisified version of ReactDOM.render() +function asyncReactDOMRender(reactElement, domElement) { + return new Promise(resolve => + ReactDOM.render(reactElement, domElement, resolve)); +} // performs fn asynchronously and expects count errors logged to console.error. // will fail the test if the count of errors logged is not equal to count. -function expectErrors(fn, count) { +async function expectErrors(fn, count) { if (console.error.calls && console.error.calls.reset) { console.error.calls.reset(); } else { spyOn(console, 'error'); } - return fn().then((result) => { - if (console.error.calls.count() !== count) { - console.log(`We expected ${count} warning(s), but saw ${console.error.calls.count()} warning(s).`); - if (console.error.calls.count() > 0) { - console.log(`We saw these warnings:`); - for (var i = 0; i < console.error.calls.count(); i++) { - console.log(console.error.calls.argsFor(i)[0]); - } + const result = await fn(); + if (console.error.calls.count() !== count && console.error.calls.count() !== 0) { + console.log(`We expected ${count} warning(s), but saw ${console.error.calls.count()} warning(s).`); + if (console.error.calls.count() > 0) { + console.log(`We saw these warnings:`); + for (var i = 0; i < console.error.calls.count(); i++) { + console.log(console.error.calls.argsFor(i)[0]); } } - expectDev(console.error.calls.count()).toBe(count); - return result; - }); + } + expectDev(console.error.calls.count()).toBe(count); + return result; } // renders the reactElement into domElement, and expects a certain number of errors. // returns a Promise that resolves when the render is complete. function renderIntoDom(reactElement, domElement, errorCount = 0) { return expectErrors( - () => new Promise((resolve) => { + async () => { ExecutionEnvironment.canUseDOM = true; - ReactDOM.render(reactElement, domElement, () => { - ExecutionEnvironment.canUseDOM = false; - resolve(domElement.firstChild); - }); - }), + await asyncReactDOMRender(reactElement, domElement); + ExecutionEnvironment.canUseDOM = false; + return domElement.firstChild; + }, errorCount ); } @@ -61,15 +64,14 @@ function renderIntoDom(reactElement, domElement, errorCount = 0) { // Renders text using SSR and then stuffs it into a DOM node; returns the DOM // element that corresponds with the reactElement. // Does not render on client or perform client-side revival. -function serverRender(reactElement, errorCount = 0) { - return expectErrors( +async function serverRender(reactElement, errorCount = 0) { + const markup = await expectErrors( () => Promise.resolve(ReactDOMServer.renderToString(reactElement)), - errorCount) - .then((markup) => { - var domElement = document.createElement('div'); - domElement.innerHTML = markup; - return domElement.firstChild; - }); + errorCount + ); + var domElement = document.createElement('div'); + domElement.innerHTML = markup; + return domElement.firstChild; } const clientCleanRender = (element, errorCount = 0) => { @@ -77,12 +79,11 @@ const clientCleanRender = (element, errorCount = 0) => { return renderIntoDom(element, div, errorCount); }; -const clientRenderOnServerString = (element, errorCount = 0) => { - return serverRender(element, errorCount).then((markup) => { - var domElement = document.createElement('div'); - domElement.innerHTML = markup; - return renderIntoDom(element, domElement, errorCount); - }); +const clientRenderOnServerString = async (element, errorCount = 0) => { + const markup = await serverRender(element, errorCount); + var domElement = document.createElement('div'); + domElement.innerHTML = markup; + return renderIntoDom(element, domElement, errorCount); }; const clientRenderOnBadMarkup = (element, errorCount = 0) => { @@ -143,24 +144,26 @@ describe('ReactDOMServerIntegration', () => { }); describe('basic rendering', function() { - itRenders('a blank div', render => - render(
).then(e => expect(e.tagName).toBe('DIV'))); + itRenders('a blank div', async render => { + const e = await render(
); + expect(e.tagName).toBe('DIV'); + }); - itRenders('a div with inline styles', render => - render(
).then(e => { - expect(e.style.color).toBe('red'); - expect(e.style.width).toBe('30px'); - }) - ); + itRenders('a div with inline styles', async render => { + const e = await render(
); + expect(e.style.color).toBe('red'); + expect(e.style.width).toBe('30px'); + }); - itRenders('a self-closing tag', render => - render(
).then(e => expect(e.tagName).toBe('BR'))); + itRenders('a self-closing tag', async render => { + const e = await render(
); + expect(e.tagName).toBe('BR'); + }); - itRenders('a self-closing tag as a child', render => - render(

).then(e => { - expect(e.childNodes.length).toBe(1); - expect(e.firstChild.tagName).toBe('BR'); - }) - ); + itRenders('a self-closing tag as a child', async render => { + const e = await render(

); + expect(e.childNodes.length).toBe(1); + expect(e.firstChild.tagName).toBe('BR'); + }); }); }); diff --git a/src/renderers/dom/shared/hooks/ReactDOMInvalidARIAHook.js b/src/renderers/dom/shared/hooks/ReactDOMInvalidARIAHook.js index 8dc38050b1..098023cde7 100644 --- a/src/renderers/dom/shared/hooks/ReactDOMInvalidARIAHook.js +++ b/src/renderers/dom/shared/hooks/ReactDOMInvalidARIAHook.js @@ -12,7 +12,6 @@ 'use strict'; var DOMProperty = require('DOMProperty'); -var ReactComponentTreeHook = require('react/lib/ReactComponentTreeHook'); var ReactDebugCurrentFiber = require('ReactDebugCurrentFiber'); var warning = require('fbjs/lib/warning'); @@ -20,10 +19,16 @@ var warning = require('fbjs/lib/warning'); var warnedProperties = {}; var rARIA = new RegExp('^(aria)-[' + DOMProperty.ATTRIBUTE_NAME_CHAR + ']*$'); +if (__DEV__) { + var { + getStackAddendumByID, + } = require('react/lib/ReactComponentTreeHook'); +} + function getStackAddendum(debugID) { if (debugID != null) { // This can only happen on Stack - return ReactComponentTreeHook.getStackAddendumByID(debugID); + return getStackAddendumByID(debugID); } else { // This can only happen on Fiber return ReactDebugCurrentFiber.getCurrentFiberStackAddendum(); diff --git a/src/renderers/dom/shared/hooks/ReactDOMNullInputValuePropHook.js b/src/renderers/dom/shared/hooks/ReactDOMNullInputValuePropHook.js index 24b6b64f7f..a1e8cfbe16 100644 --- a/src/renderers/dom/shared/hooks/ReactDOMNullInputValuePropHook.js +++ b/src/renderers/dom/shared/hooks/ReactDOMNullInputValuePropHook.js @@ -11,17 +11,21 @@ 'use strict'; -var ReactComponentTreeHook = require('react/lib/ReactComponentTreeHook'); var ReactDebugCurrentFiber = require('ReactDebugCurrentFiber'); - var warning = require('fbjs/lib/warning'); +if (__DEV__) { + var { + getStackAddendumByID, + } = require('react/lib/ReactComponentTreeHook'); +} + var didWarnValueNull = false; function getStackAddendum(debugID) { if (debugID != null) { // This can only happen on Stack - return ReactComponentTreeHook.getStackAddendumByID(debugID); + return getStackAddendumByID(debugID); } else { // This can only happen on Fiber return ReactDebugCurrentFiber.getCurrentFiberStackAddendum(); diff --git a/src/renderers/shared/fiber/ReactChildFiber.js b/src/renderers/shared/fiber/ReactChildFiber.js index 9dd69f1505..0d51f03db6 100644 --- a/src/renderers/shared/fiber/ReactChildFiber.js +++ b/src/renderers/shared/fiber/ReactChildFiber.js @@ -40,7 +40,7 @@ var ReactCurrentOwner = require('react/lib/ReactCurrentOwner'); if (__DEV__) { var { getCurrentFiberStackAddendum } = require('ReactDebugCurrentFiber'); - var { getComponentName } = require('ReactFiberTreeReflection'); + var getComponentName = require('getComponentName'); var warning = require('fbjs/lib/warning'); var didWarnAboutMaps = false; } @@ -1353,15 +1353,15 @@ exports.cloneChildFibers = function(current : Fiber | null, workInProgress : Fib newChild.return = workInProgress; } newChild.sibling = null; - } - - // If there is no alternate, then we don't need to clone the children. - // If the children of the alternate fiber is a different set, then we don't - // need to clone. We need to reset the return fiber though since we'll - // traverse down into them. - let child = workInProgress.child; - while (child !== null) { - child.return = workInProgress; - child = child.sibling; + } else { + // If there is no alternate, then we don't need to clone the children. + // If the children of the alternate fiber is a different set, then we don't + // need to clone. We need to reset the return fiber though since we'll + // traverse down into them. + let child = workInProgress.child; + while (child !== null) { + child.return = workInProgress; + child = child.sibling; + } } }; diff --git a/src/renderers/shared/fiber/ReactDebugCurrentFiber.js b/src/renderers/shared/fiber/ReactDebugCurrentFiber.js index 33ad128f72..539c19e085 100644 --- a/src/renderers/shared/fiber/ReactDebugCurrentFiber.js +++ b/src/renderers/shared/fiber/ReactDebugCurrentFiber.js @@ -18,7 +18,7 @@ type LifeCyclePhase = 'render' | 'getChildContext'; if (__DEV__) { var getComponentName = require('getComponentName'); - var { getStackAddendumByWorkInProgressFiber } = require('react/lib/ReactComponentTreeHook'); + var { getStackAddendumByWorkInProgressFiber } = require('ReactFiberComponentTreeHook'); } function getCurrentFiberOwnerName() : string | null { diff --git a/src/renderers/shared/fiber/ReactDebugFiberPerf.js b/src/renderers/shared/fiber/ReactDebugFiberPerf.js new file mode 100644 index 0000000000..0469cb9872 --- /dev/null +++ b/src/renderers/shared/fiber/ReactDebugFiberPerf.js @@ -0,0 +1,408 @@ +/** + * Copyright 2013-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * @providesModule ReactDebugFiberPerf + * @flow + */ + +import type { Fiber } from 'ReactFiber'; + +type MeasurementPhase = + 'componentWillMount' | + 'componentWillUnmount' | + 'componentWillReceiveProps' | + 'shouldComponentUpdate' | + 'componentWillUpdate' | + 'componentDidUpdate' | + 'componentDidMount' | + 'getChildContext'; + +// Trust the developer to only use this with a __DEV__ check +let ReactDebugFiberPerf = ((null: any): typeof ReactDebugFiberPerf); + +if (__DEV__) { + const { + HostRoot, + HostComponent, + HostText, + HostPortal, + YieldComponent, + Fragment, + } = require('ReactTypeOfWork'); + + const getComponentName = require('getComponentName'); + + // Prefix measurements so that it's possible to filter them. + // Longer prefixes are hard to read in DevTools. + const reactEmoji = '\u269B'; + const warningEmoji = '\u26D4'; + const supportsUserTiming = + typeof performance !== 'undefined' && + typeof performance.mark === 'function' && + typeof performance.clearMarks === 'function' && + typeof performance.measure === 'function' && + typeof performance.clearMeasures === 'function'; + + // Keep track of current fiber so that we know the path to unwind on pause. + // TODO: this looks the same as nextUnitOfWork in scheduler. Can we unify them? + let currentFiber : Fiber | null = null; + // If we're in the middle of user code, which fiber and method is it? + // Reusing `currentFiber` would be confusing for this because user code fiber + // can change during commit phase too, but we don't need to unwind it (since + // lifecycles in the commit phase don't resemble a tree). + let currentPhase : MeasurementPhase | null = null; + let currentPhaseFiber : Fiber | null = null; + // Did lifecycle hook schedule an update? This is often a performance problem, + // so we will keep track of it, and include it in the report. + // Track commits caused by cascading updates. + let isCommitting : boolean = false; + let hasScheduledUpdateInCurrentCommit : boolean = false; + let hasScheduledUpdateInCurrentPhase : boolean = false; + let commitCountInCurrentWorkLoop : number = 0; + let effectCountInCurrentCommit : number = 0; + // During commits, we only show a measurement once per method name + // to avoid stretch the commit phase with measurement overhead. + const labelsInCurrentCommit : Set = new Set(); + + const formatMarkName = (markName : string) => { + return `${reactEmoji} ${markName}`; + }; + + const formatLabel = (label : string, warning : string | null) => { + const prefix = warning ? `${warningEmoji} ` : `${reactEmoji} `; + const suffix = warning ? ` Warning: ${warning}` : ''; + return `${prefix}${label}${suffix}`; + }; + + const beginMark = (markName : string) => { + performance.mark(formatMarkName(markName)); + }; + + const clearMark = (markName : string) => { + performance.clearMarks(formatMarkName(markName)); + }; + + const endMark = (label : string, markName : string, warning : string | null) => { + const formattedMarkName = formatMarkName(markName); + const formattedLabel = formatLabel(label, warning); + try { + performance.measure(formattedLabel, formattedMarkName); + } catch (err) { + // If previous mark was missing for some reason, this will throw. + // This could only happen if React crashed in an unexpected place earlier. + // Don't pile on with more errors. + } + // Clear marks immediately to avoid growing buffer. + performance.clearMarks(formattedMarkName); + performance.clearMeasures(formattedLabel); + }; + + const getFiberMarkName = (label : string, debugID : number) => { + return `${label} (#${debugID})`; + }; + + const getFiberLabel = ( + componentName : string, + isMounted : boolean, + phase : MeasurementPhase | null, + ) => { + if (phase === null) { + // These are composite component total time measurements. + return `${componentName} [${isMounted ? 'update' : 'mount'}]`; + } else { + // Composite component methods. + return `${componentName}.${phase}`; + } + }; + + const beginFiberMark = ( + fiber : Fiber, + phase : MeasurementPhase | null, + ) : boolean => { + const componentName = getComponentName(fiber) || 'Unknown'; + const debugID = ((fiber._debugID : any) : number); + const isMounted = fiber.alternate !== null; + const label = getFiberLabel(componentName, isMounted, phase); + + if (isCommitting && labelsInCurrentCommit.has(label)) { + // During the commit phase, we don't show duplicate labels because + // there is a fixed overhead for every measurement, and we don't + // want to stretch the commit phase beyond necessary. + return false; + } + labelsInCurrentCommit.add(label); + + const markName = getFiberMarkName(label, debugID); + beginMark(markName); + return true; + }; + + const clearFiberMark = (fiber : Fiber, phase : MeasurementPhase | null) => { + const componentName = getComponentName(fiber) || 'Unknown'; + const debugID = ((fiber._debugID : any) : number); + const isMounted = fiber.alternate !== null; + const label = getFiberLabel(componentName, isMounted, phase); + const markName = getFiberMarkName(label, debugID); + clearMark(markName); + }; + + const endFiberMark = ( + fiber : Fiber, + phase : MeasurementPhase | null, + warning : string | null, + ) => { + const componentName = getComponentName(fiber) || 'Unknown'; + const debugID = ((fiber._debugID : any) : number); + const isMounted = fiber.alternate !== null; + const label = getFiberLabel(componentName, isMounted, phase); + const markName = getFiberMarkName(label, debugID); + endMark(label, markName, warning); + }; + + const shouldIgnoreFiber = (fiber : Fiber) : boolean => { + // Host components should be skipped in the timeline. + // We could check typeof fiber.type, but does this work with RN? + switch (fiber.tag) { + case HostRoot: + case HostComponent: + case HostText: + case HostPortal: + case YieldComponent: + case Fragment: + return true; + default: + return false; + } + }; + + const clearPendingPhaseMeasurement = () => { + if (currentPhase !== null && currentPhaseFiber !== null) { + clearFiberMark(currentPhaseFiber, currentPhase); + } + currentPhaseFiber = null; + currentPhase = null; + hasScheduledUpdateInCurrentPhase = false; + }; + + const pauseTimers = () => { + // Stops all currently active measurements so that they can be resumed + // if we continue in a later deferred loop from the same unit of work. + let fiber = currentFiber; + while (fiber) { + if (fiber._debugIsCurrentlyTiming) { + endFiberMark(fiber, null, null); + } + fiber = fiber.return; + } + }; + + const resumeTimersRecursively = (fiber : Fiber) => { + if (fiber.return !== null) { + resumeTimersRecursively(fiber.return); + } + if (fiber._debugIsCurrentlyTiming) { + beginFiberMark(fiber, null); + } + }; + + const resumeTimers = () => { + // Resumes all measurements that were active during the last deferred loop. + if (currentFiber !== null) { + resumeTimersRecursively(currentFiber); + } + }; + + ReactDebugFiberPerf = { + recordEffect() : void { + effectCountInCurrentCommit++; + }, + + recordScheduleUpdate() : void { + if (isCommitting) { + hasScheduledUpdateInCurrentCommit = true; + } + if ( + currentPhase !== null && + currentPhase !== 'componentWillMount' && + currentPhase !== 'componentWillReceiveProps' + ) { + hasScheduledUpdateInCurrentPhase = true; + } + }, + + startWorkTimer(fiber : Fiber) : void { + if (!supportsUserTiming || shouldIgnoreFiber(fiber)) { + return; + } + // If we pause, this is the fiber to unwind from. + currentFiber = fiber; + if (!beginFiberMark(fiber, null)) { + return; + } + fiber._debugIsCurrentlyTiming = true; + }, + + cancelWorkTimer(fiber : Fiber) : void { + if (!supportsUserTiming || shouldIgnoreFiber(fiber)) { + return; + } + // Remember we shouldn't complete measurement for this fiber. + // Otherwise flamechart will be deep even for small updates. + fiber._debugIsCurrentlyTiming = false; + clearFiberMark(fiber, null); + }, + + stopWorkTimer(fiber : Fiber) : void { + if (!supportsUserTiming || shouldIgnoreFiber(fiber)) { + return; + } + // If we pause, its parent is the fiber to unwind from. + currentFiber = fiber.return; + if (!fiber._debugIsCurrentlyTiming) { + return; + } + fiber._debugIsCurrentlyTiming = false; + endFiberMark(fiber, null, null); + }, + + startPhaseTimer( + fiber : Fiber, + phase : MeasurementPhase, + ) : void { + if (!supportsUserTiming) { + return; + } + clearPendingPhaseMeasurement(); + if (!beginFiberMark(fiber, phase)) { + return; + } + currentPhaseFiber = fiber; + currentPhase = phase; + }, + + stopPhaseTimer() : void { + if (!supportsUserTiming) { + return; + } + if (currentPhase !== null && currentPhaseFiber !== null) { + const warning = hasScheduledUpdateInCurrentPhase ? + 'Scheduled a cascading update' : + null; + endFiberMark(currentPhaseFiber, currentPhase, warning); + } + currentPhase = null; + currentPhaseFiber = null; + }, + + startWorkLoopTimer() : void { + if (!supportsUserTiming) { + return; + } + commitCountInCurrentWorkLoop = 0; + // This is top level call. + // Any other measurements are performed within. + beginMark('(React Tree Reconciliation)'); + // Resume any measurements that were in progress during the last loop. + resumeTimers(); + }, + + stopWorkLoopTimer() : void { + if (!supportsUserTiming) { + return; + } + const warning = commitCountInCurrentWorkLoop > 1 ? + 'There were cascading updates' : + null; + commitCountInCurrentWorkLoop = 0; + // Pause any measurements until the next loop. + pauseTimers(); + endMark( + '(React Tree Reconciliation)', + '(React Tree Reconciliation)', + warning, + ); + }, + + startCommitTimer() : void { + if (!supportsUserTiming) { + return; + } + isCommitting = true; + hasScheduledUpdateInCurrentCommit = false; + labelsInCurrentCommit.clear(); + beginMark('(Committing Changes)'); + }, + + stopCommitTimer() : void { + if (!supportsUserTiming) { + return; + } + + let warning = null; + if (hasScheduledUpdateInCurrentCommit) { + warning = 'Lifecycle hook scheduled a cascading update'; + } else if (commitCountInCurrentWorkLoop > 0) { + warning = 'Caused by a cascading update in earlier commit'; + } + hasScheduledUpdateInCurrentCommit = false; + commitCountInCurrentWorkLoop++; + isCommitting = false; + labelsInCurrentCommit.clear(); + + endMark( + '(Committing Changes)', + '(Committing Changes)', + warning, + ); + }, + + startCommitHostEffectsTimer() : void { + if (!supportsUserTiming) { + return; + } + effectCountInCurrentCommit = 0; + beginMark('(Committing Host Effects)'); + }, + + stopCommitHostEffectsTimer() : void { + if (!supportsUserTiming) { + return; + } + const count = effectCountInCurrentCommit; + effectCountInCurrentCommit = 0; + endMark( + `(Committing Host Effects: ${count} Total)`, + '(Committing Host Effects)', + null, + ); + }, + + startCommitLifeCyclesTimer() : void { + if (!supportsUserTiming) { + return; + } + effectCountInCurrentCommit = 0; + beginMark('(Calling Lifecycle Methods)'); + }, + + stopCommitLifeCyclesTimer() : void { + if (!supportsUserTiming) { + return; + } + const count = effectCountInCurrentCommit; + effectCountInCurrentCommit = 0; + endMark( + `(Calling Lifecycle Methods: ${count} Total)`, + '(Calling Lifecycle Methods)', + null, + ); + }, + }; +} + +module.exports = ReactDebugFiberPerf; diff --git a/src/renderers/shared/fiber/ReactFiber.js b/src/renderers/shared/fiber/ReactFiber.js index bd89a93f82..0b34566378 100644 --- a/src/renderers/shared/fiber/ReactFiber.js +++ b/src/renderers/shared/fiber/ReactFiber.js @@ -60,6 +60,7 @@ export type Fiber = { _debugID ?: DebugID, _debugSource ?: Source | null, _debugOwner ?: Fiber | ReactInstance | null, // Stack compatible + _debugIsCurrentlyTiming ?: boolean, // These first fields are conceptually members of an Instance. This used to // be split into a separate type and intersected with the other Fiber fields, @@ -223,6 +224,7 @@ var createFiber = function(tag : TypeOfWork, key : null | string) : Fiber { fiber._debugID = debugCounter++; fiber._debugSource = null; fiber._debugOwner = null; + fiber._debugIsCurrentlyTiming = false; if (typeof Object.preventExtensions === 'function') { Object.preventExtensions(fiber); } diff --git a/src/renderers/shared/fiber/ReactFiberBeginWork.js b/src/renderers/shared/fiber/ReactFiberBeginWork.js index 1d1d6a75c1..3fa3cfda99 100644 --- a/src/renderers/shared/fiber/ReactFiberBeginWork.js +++ b/src/renderers/shared/fiber/ReactFiberBeginWork.js @@ -66,7 +66,9 @@ var invariant = require('fbjs/lib/invariant'); if (__DEV__) { var ReactDebugCurrentFiber = require('ReactDebugCurrentFiber'); + var {cancelWorkTimer} = require('ReactDebugFiberPerf'); var warning = require('fbjs/lib/warning'); + var warnedAboutStatelessRefs = {}; } @@ -652,6 +654,10 @@ module.exports = function( */ function bailoutOnAlreadyFinishedWork(current, workInProgress : Fiber) : Fiber | null { + if (__DEV__) { + cancelWorkTimer(workInProgress); + } + const priorityLevel = workInProgress.pendingWorkPriority; // TODO: We should ideally be able to bail out early if the children have no // more work to do. However, since we don't have a separation of this @@ -679,6 +685,10 @@ module.exports = function( } function bailoutOnLowPriority(current, workInProgress) { + if (__DEV__) { + cancelWorkTimer(workInProgress); + } + // TODO: Handle HostComponent tags here as well and call pushHostContext()? // See PR 8590 discussion for context switch (workInProgress.tag) { diff --git a/src/renderers/shared/fiber/ReactFiberClassComponent.js b/src/renderers/shared/fiber/ReactFiberClassComponent.js index 5af2249d3b..2b0eb304b5 100644 --- a/src/renderers/shared/fiber/ReactFiberClassComponent.js +++ b/src/renderers/shared/fiber/ReactFiberClassComponent.js @@ -31,15 +31,20 @@ var { beginUpdateQueue, } = require('ReactFiberUpdateQueue'); var { hasContextChanged } = require('ReactFiberContext'); -var { getComponentName, isMounted } = require('ReactFiberTreeReflection'); +var { isMounted } = require('ReactFiberTreeReflection'); var ReactInstanceMap = require('ReactInstanceMap'); var emptyObject = require('fbjs/lib/emptyObject'); +var getComponentName = require('getComponentName'); var shallowEqual = require('fbjs/lib/shallowEqual'); var invariant = require('fbjs/lib/invariant'); const isArray = Array.isArray; if (__DEV__) { + var { + startPhaseTimer, + stopPhaseTimer, + } = require('ReactDebugFiberPerf'); var warning = require('fbjs/lib/warning'); var warnOnInvalidCallback = function(callback : mixed, callerName : string) { warning( @@ -102,14 +107,20 @@ module.exports = function( const instance = workInProgress.stateNode; if (typeof instance.shouldComponentUpdate === 'function') { + if (__DEV__) { + startPhaseTimer(workInProgress, 'shouldComponentUpdate'); + } const shouldUpdate = instance.shouldComponentUpdate(newProps, newState, newContext); + if (__DEV__) { + stopPhaseTimer(); + } if (__DEV__) { warning( shouldUpdate !== undefined, '%s.shouldComponentUpdate(): Returned undefined instead of a ' + 'boolean value. Make sure to return true or false.', - getComponentName(workInProgress) + getComponentName(workInProgress) || 'Unknown' ); } @@ -227,22 +238,6 @@ module.exports = function( } } - - function markUpdate(workInProgress) { - workInProgress.effectTag |= Update; - } - - function markUpdateIfAlreadyInProgress(current: Fiber | null, workInProgress : Fiber) { - // If an update was already in progress, we should schedule an Update - // effect even though we're bailing out, so that cWU/cDU are called. - if (current !== null) { - if (workInProgress.memoizedProps !== current.memoizedProps || - workInProgress.memoizedState !== current.memoizedState) { - markUpdate(workInProgress); - } - } - } - function resetInputPointers(workInProgress : Fiber, instance : any) { instance.props = workInProgress.memoizedProps; instance.state = workInProgress.memoizedState; @@ -276,7 +271,6 @@ module.exports = function( // Invokes the mount life-cycles on a previously never rendered instance. function mountClassInstance(workInProgress : Fiber, priorityLevel : PriorityLevel) : void { - markUpdate(workInProgress); const instance = workInProgress.stateNode; const state = instance.state || null; @@ -295,7 +289,13 @@ module.exports = function( instance.context = getMaskedContext(workInProgress, unmaskedContext); if (typeof instance.componentWillMount === 'function') { + if (__DEV__) { + startPhaseTimer(workInProgress, 'componentWillMount'); + } instance.componentWillMount(); + if (__DEV__) { + stopPhaseTimer(); + } // If we had additional state updates during this life-cycle, let's // process them now. const updateQueue = workInProgress.updateQueue; @@ -310,12 +310,14 @@ module.exports = function( ); } } + if (typeof instance.componentDidMount === 'function') { + workInProgress.effectTag |= Update; + } } // Called on a preexisting class instance. Returns false if a resumed render // could be reused. function resumeMountClassInstance(workInProgress : Fiber, priorityLevel : PriorityLevel) : boolean { - markUpdate(workInProgress); const instance = workInProgress.stateNode; resetInputPointers(workInProgress, instance); @@ -362,7 +364,13 @@ module.exports = function( newInstance.context = newContext; if (typeof newInstance.componentWillMount === 'function') { + if (__DEV__) { + startPhaseTimer(workInProgress, 'componentWillMount'); + } newInstance.componentWillMount(); + if (__DEV__) { + stopPhaseTimer(); + } } // If we had additional state updates, process them now. // They may be from componentWillMount() or from error boundary's setState() @@ -378,6 +386,9 @@ module.exports = function( priorityLevel ); } + if (typeof instance.componentDidMount === 'function') { + workInProgress.effectTag |= Update; + } return true; } @@ -408,7 +419,13 @@ module.exports = function( if (oldProps !== newProps || oldContext !== newContext) { if (typeof instance.componentWillReceiveProps === 'function') { + if (__DEV__) { + startPhaseTimer(workInProgress, 'componentWillReceiveProps'); + } instance.componentWillReceiveProps(newProps, newContext); + if (__DEV__) { + stopPhaseTimer(); + } if (instance.state !== workInProgress.memoizedState) { if (__DEV__) { @@ -447,7 +464,14 @@ module.exports = function( oldState === newState && !hasContextChanged() && !(updateQueue !== null && updateQueue.hasForceUpdate)) { - markUpdateIfAlreadyInProgress(current, workInProgress); + // If an update was already in progress, we should schedule an Update + // effect even though we're bailing out, so that cWU/cDU are called. + if (typeof instance.componentDidUpdate === 'function') { + if (oldProps !== current.memoizedProps || + oldState !== current.memoizedState) { + workInProgress.effectTag |= Update; + } + } return false; } @@ -461,12 +485,27 @@ module.exports = function( ); if (shouldUpdate) { - markUpdate(workInProgress); if (typeof instance.componentWillUpdate === 'function') { + if (__DEV__) { + startPhaseTimer(workInProgress, 'componentWillUpdate'); + } instance.componentWillUpdate(newProps, newState, newContext); + if (__DEV__) { + stopPhaseTimer(); + } + } + if (typeof instance.componentDidUpdate === 'function') { + workInProgress.effectTag |= Update; } } else { - markUpdateIfAlreadyInProgress(current, workInProgress); + // If an update was already in progress, we should schedule an Update + // effect even though we're bailing out, so that cWU/cDU are called. + if (typeof instance.componentDidUpdate === 'function') { + if (oldProps !== current.memoizedProps || + oldState !== current.memoizedState) { + workInProgress.effectTag |= Update; + } + } // If shouldComponentUpdate returned false, we should still update the // memoized props/state to indicate that this work can be reused. diff --git a/src/renderers/shared/fiber/ReactFiberCommitWork.js b/src/renderers/shared/fiber/ReactFiberCommitWork.js index e9676b106a..30a2f4962c 100644 --- a/src/renderers/shared/fiber/ReactFiberCommitWork.js +++ b/src/renderers/shared/fiber/ReactFiberCommitWork.js @@ -37,6 +37,13 @@ var { var invariant = require('fbjs/lib/invariant'); +if (__DEV__) { + var { + startPhaseTimer, + stopPhaseTimer, + } = require('ReactDebugFiberPerf'); +} + module.exports = function( config : HostConfig, captureError : (failedFiber : Fiber, error: Error) => Fiber | null @@ -53,10 +60,24 @@ module.exports = function( getPublicInstance, } = config; + if (__DEV__) { + var callComponentWillUnmountWithTimerInDev = function(current, instance) { + startPhaseTimer(current, 'componentWillUnmount'); + instance.componentWillUnmount(); + stopPhaseTimer(); + }; + } + // Capture errors so they don't interrupt unmounting. function safelyCallComponentWillUnmount(current, instance) { if (__DEV__) { - const unmountError = invokeGuardedCallback(null, instance.componentWillUnmount, instance); + const unmountError = invokeGuardedCallback( + null, + callComponentWillUnmountWithTimerInDev, + null, + current, + instance, + ); if (unmountError) { captureError(current, unmountError); } @@ -87,16 +108,6 @@ module.exports = function( } } - // Only called during update. It's ok to throw. - function detachRefIfNeeded(current : Fiber | null, finishedWork : Fiber) { - if (current) { - const currentRef = current.ref; - if (currentRef !== null && currentRef !== finishedWork.ref) { - currentRef(null); - } - } - } - function getHostParent(fiber : Fiber) : I | C { let parent = fiber.return; while (parent !== null) { @@ -381,7 +392,6 @@ module.exports = function( function commitWork(current : Fiber | null, finishedWork : Fiber) : void { switch (finishedWork.tag) { case ClassComponent: { - detachRefIfNeeded(current, finishedWork); return; } case HostComponent: { @@ -398,7 +408,6 @@ module.exports = function( commitUpdate(instance, updatePayload, type, oldProps, newProps, finishedWork); } } - detachRefIfNeeded(current, finishedWork); return; } case HostText: { @@ -435,14 +444,22 @@ module.exports = function( const instance = finishedWork.stateNode; if (finishedWork.effectTag & Update) { if (current === null) { - if (typeof instance.componentDidMount === 'function') { - instance.componentDidMount(); + if (__DEV__) { + startPhaseTimer(finishedWork, 'componentDidMount'); + } + instance.componentDidMount(); + if (__DEV__) { + stopPhaseTimer(); } } else { - if (typeof instance.componentDidUpdate === 'function') { - const prevProps = current.memoizedProps; - const prevState = current.memoizedState; - instance.componentDidUpdate(prevProps, prevState); + const prevProps = current.memoizedProps; + const prevState = current.memoizedState; + if (__DEV__) { + startPhaseTimer(finishedWork, 'componentDidUpdate'); + } + instance.componentDidUpdate(prevProps, prevState); + if (__DEV__) { + stopPhaseTimer(); } } } @@ -495,10 +512,7 @@ module.exports = function( } } - function commitRef(finishedWork : Fiber) { - if (finishedWork.tag !== ClassComponent && finishedWork.tag !== HostComponent) { - return; - } + function commitAttachRef(finishedWork : Fiber) { const ref = finishedWork.ref; if (ref !== null) { const instance = getPublicInstance(finishedWork.stateNode); @@ -506,12 +520,20 @@ module.exports = function( } } + function commitDetachRef(current : Fiber) { + const currentRef = current.ref; + if (currentRef !== null) { + currentRef(null); + } + } + return { commitPlacement, commitDeletion, commitWork, commitLifeCycles, - commitRef, + commitAttachRef, + commitDetachRef, }; }; diff --git a/src/renderers/shared/fiber/ReactFiberCompleteWork.js b/src/renderers/shared/fiber/ReactFiberCompleteWork.js index 8a04936183..aef565e1d2 100644 --- a/src/renderers/shared/fiber/ReactFiberCompleteWork.js +++ b/src/renderers/shared/fiber/ReactFiberCompleteWork.js @@ -85,6 +85,10 @@ module.exports = function( workInProgress.effectTag |= Update; } + function markRef(workInProgress : Fiber) { + workInProgress.effectTag |= Ref; + } + function appendAllYields(yields : Array, workInProgress : Fiber) { let node = workInProgress.stateNode; if (node) { @@ -231,9 +235,12 @@ module.exports = function( workInProgress.updateQueue = (updatePayload : any); // If the update payload indicates that there is a change or if there // is a new ref we mark this as an update. - if (updatePayload || current.ref !== workInProgress.ref) { + if (updatePayload) { markUpdate(workInProgress); } + if (current.ref !== workInProgress.ref) { + markRef(workInProgress); + } } else { if (!newProps) { invariant( @@ -270,7 +277,7 @@ module.exports = function( workInProgress.stateNode = instance; if (workInProgress.ref !== null) { // If there is a ref on a host node we need to schedule a callback - workInProgress.effectTag |= Ref; + markRef(workInProgress); } } return null; diff --git a/src/renderers/shared/fiber/ReactFiberContext.js b/src/renderers/shared/fiber/ReactFiberContext.js index d921285a6b..81410161d4 100644 --- a/src/renderers/shared/fiber/ReactFiberContext.js +++ b/src/renderers/shared/fiber/ReactFiberContext.js @@ -16,10 +16,10 @@ import type { Fiber } from 'ReactFiber'; import type { StackCursor } from 'ReactFiberStack'; var emptyObject = require('fbjs/lib/emptyObject'); +var getComponentName = require('getComponentName'); var invariant = require('fbjs/lib/invariant'); var warning = require('fbjs/lib/warning'); var { - getComponentName, isFiberMounted, } = require('ReactFiberTreeReflection'); var { @@ -36,6 +36,10 @@ if (__DEV__) { var checkReactTypeSpec = require('checkReactTypeSpec'); var ReactDebugCurrentFrame = require('react/lib/ReactDebugCurrentFrame'); var ReactDebugCurrentFiber = require('ReactDebugCurrentFiber'); + var { + startPhaseTimer, + stopPhaseTimer, + } = require('ReactDebugFiberPerf'); var warnedAboutMissingGetChildContext = {}; } @@ -92,7 +96,7 @@ exports.getMaskedContext = function(workInProgress : Fiber, unmaskedContext : Ob } if (__DEV__) { - const name = getComponentName(workInProgress); + const name = getComponentName(workInProgress) || 'Unknown'; ReactDebugCurrentFrame.current = workInProgress; checkReactTypeSpec(contextTypes, context, 'context', name); ReactDebugCurrentFrame.current = null; @@ -152,7 +156,7 @@ function processChildContext(fiber : Fiber, parentContext : Object, isReconcilin // It has only been added in Fiber to match the (unintentional) behavior in Stack. if (typeof instance.getChildContext !== 'function') { if (__DEV__) { - const componentName = getComponentName(fiber); + const componentName = getComponentName(fiber) || 'Unknown'; if (!warnedAboutMissingGetChildContext[componentName]) { warnedAboutMissingGetChildContext[componentName] = true; @@ -172,7 +176,9 @@ function processChildContext(fiber : Fiber, parentContext : Object, isReconcilin let childContext; if (__DEV__) { ReactDebugCurrentFiber.phase = 'getChildContext'; + startPhaseTimer(fiber, 'getChildContext'); childContext = instance.getChildContext(); + stopPhaseTimer(); ReactDebugCurrentFiber.phase = null; } else { childContext = instance.getChildContext(); @@ -181,12 +187,12 @@ function processChildContext(fiber : Fiber, parentContext : Object, isReconcilin invariant( contextKey in childContextTypes, '%s.getChildContext(): key "%s" is not defined in childContextTypes.', - getComponentName(fiber), + getComponentName(fiber) || 'Unknown', contextKey ); } if (__DEV__) { - const name = getComponentName(fiber); + const name = getComponentName(fiber) || 'Unknown'; // We can only provide accurate element stacks if we pass work-in-progress tree // during the begin or complete phase. However currently this function is also // called from unstable_renderSubtree legacy implementation. In this case it unsafe to diff --git a/src/renderers/shared/fiber/ReactFiberReconciler.js b/src/renderers/shared/fiber/ReactFiberReconciler.js index 8cc4235790..f1ae92f1a3 100644 --- a/src/renderers/shared/fiber/ReactFiberReconciler.js +++ b/src/renderers/shared/fiber/ReactFiberReconciler.js @@ -33,7 +33,7 @@ if (__DEV__) { var warning = require('fbjs/lib/warning'); var ReactFiberInstrumentation = require('ReactFiberInstrumentation'); var ReactDebugCurrentFiber = require('ReactDebugCurrentFiber'); - var { getComponentName } = require('ReactFiberTreeReflection'); + var getComponentName = require('getComponentName'); } var { findCurrentHostFiber } = require('ReactFiberTreeReflection'); @@ -148,14 +148,17 @@ module.exports = function( function scheduleTopLevelUpdate(current : Fiber, element : ReactNodeList, callback : ?Function) { if (__DEV__) { - if (ReactDebugCurrentFiber.current !== null) { + if ( + ReactDebugCurrentFiber.phase === 'render' && + ReactDebugCurrentFiber.current !== null + ) { warning( - ReactDebugCurrentFiber.phase !== 'render', + false, 'Render methods should be a pure function of props and state; ' + 'triggering nested component updates from render is not allowed. ' + 'If necessary, trigger nested updates in componentDidUpdate.\n\n' + 'Check the render method of %s.', - getComponentName(ReactDebugCurrentFiber.current) + getComponentName(ReactDebugCurrentFiber.current) || 'Unknown' ); } } diff --git a/src/renderers/shared/fiber/ReactFiberScheduler.js b/src/renderers/shared/fiber/ReactFiberScheduler.js index cacaacdf83..3f1698d4c8 100644 --- a/src/renderers/shared/fiber/ReactFiberScheduler.js +++ b/src/renderers/shared/fiber/ReactFiberScheduler.js @@ -37,7 +37,7 @@ var { const { reset } = require('ReactFiberStack'); var { getStackAddendumByWorkInProgressFiber, -} = require('react/lib/ReactComponentTreeHook'); +} = require('ReactFiberComponentTreeHook'); var { logCapturedError } = require('ReactFiberErrorLogger'); var { invokeGuardedCallback } = require('ReactErrorUtils'); @@ -95,6 +95,20 @@ if (__DEV__) { var warning = require('fbjs/lib/warning'); var ReactFiberInstrumentation = require('ReactFiberInstrumentation'); var ReactDebugCurrentFiber = require('ReactDebugCurrentFiber'); + var { + recordEffect, + recordScheduleUpdate, + startWorkTimer, + stopWorkTimer, + startWorkLoopTimer, + stopWorkLoopTimer, + startCommitTimer, + stopCommitTimer, + startCommitHostEffectsTimer, + stopCommitHostEffectsTimer, + startCommitLifeCyclesTimer, + stopCommitLifeCyclesTimer, + } = require('ReactDebugFiberPerf'); var warnAboutUpdateOnUnmounted = function(instance : ReactClass) { const ctor = instance.constructor; @@ -146,7 +160,8 @@ module.exports = function(config : HostConfig(config : HostConfig(config : HostConfig(config : HostConfig(config : HostConfig(config : HostConfig(config : HostConfig(config : HostConfig(config : HostConfig(config : HostConfig(config : HostConfig(config : HostConfig(config : HostConfig(config : HostConfig(config : HostConfig(config : HostConfig) : boolea 'never access something that requires stale data from the previous ' + 'render, such as refs. Move this logic to componentDidMount and ' + 'componentDidUpdate instead.', - getComponentName(ownerFiber) + getComponentName(ownerFiber) || 'A component' ); instance._warnedAboutRefsInRender = true; } @@ -266,15 +267,3 @@ exports.findCurrentHostFiber = function(parent : Fiber) : Fiber | null { // eslint-disable-next-line no-unreachable return null; }; - -function getComponentName(fiber: Fiber): string { - const type = fiber.type; - const instance = fiber.stateNode; - const constructor = instance && instance.constructor; - return ( - type.displayName || (constructor && constructor.displayName) || - type.name || (constructor && constructor.name) || - 'A Component' - ); -} -exports.getComponentName = getComponentName; diff --git a/src/renderers/shared/fiber/__tests__/ReactIncrementalPerf-test.js b/src/renderers/shared/fiber/__tests__/ReactIncrementalPerf-test.js new file mode 100644 index 0000000000..98780e516c --- /dev/null +++ b/src/renderers/shared/fiber/__tests__/ReactIncrementalPerf-test.js @@ -0,0 +1,487 @@ +/** + * Copyright 2016-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * @emails react-core + */ + +'use strict'; + +describe('ReactDebugFiberPerf', () => { + let React; + let ReactCoroutine; + let ReactFeatureFlags; + let ReactNoop; + let ReactPortal; + + let root; + let activeMeasure; + let knownMarks; + let knownMeasures; + + function resetFlamechart() { + root = { + children: [], + indent: -1, + markName: null, + label: null, + parent: null, + toString() { + return this.children.map(c => c.toString()).join('\n'); + }, + }; + activeMeasure = root; + knownMarks = new Set(); + knownMeasures = new Set(); + } + + function addComment(comment) { + activeMeasure.children.push( + `${' '.repeat(activeMeasure.indent + 1)}// ${comment}` + ); + } + + function getFlameChart() { + // Make sure we unwind the measurement stack every time. + expect(activeMeasure.indent).toBe(-1); + expect(activeMeasure).toBe(root); + // We should always clean them up because browsers + // buffer user timing measurements forever. + expect(knownMarks.size).toBe(0); + expect(knownMeasures.size).toBe(0); + return root.toString(); + } + + function createUserTimingPolyfill() { + // This is not a true polyfill, but it gives us enough + // to capture measurements in a readable tree-like output. + // Reference: https://developer.mozilla.org/en-US/docs/Web/API/User_Timing_API + return { + mark(markName) { + const measure = { + children: [], + indent: activeMeasure.indent + 1, + markName: markName, + // Will be assigned on measure() call: + label: null, + parent: activeMeasure, + toString() { + return [ + ' '.repeat(this.indent) + this.label, + ...this.children.map(c => c.toString()), + ].join('\n') + ( + // Extra newline after each root reconciliation + this.indent === 0 ? '\n' : '' + ); + }, + }; + // Step one level deeper + activeMeasure.children.push(measure); + activeMeasure = measure; + knownMarks.add(markName); + }, + // We don't use the overload with three arguments. + measure(label, markName) { + if (markName !== activeMeasure.markName) { + throw new Error('Unexpected measure() call.'); + } + // Step one level up + activeMeasure.label = label; + activeMeasure = activeMeasure.parent; + knownMeasures.add(label); + }, + clearMarks(markName) { + if (markName === activeMeasure.markName) { + // Step one level up if we're in this measure + activeMeasure = activeMeasure.parent; + activeMeasure.children.length--; + } + knownMarks.delete(markName); + }, + clearMeasures(label) { + knownMeasures.delete(label); + }, + }; + } + + beforeEach(() => { + jest.resetModules(); + resetFlamechart(); + global.performance = createUserTimingPolyfill(); + + // Import after the polyfill is set up: + React = require('React'); + ReactCoroutine = require('ReactCoroutine'); + ReactFeatureFlags = require('ReactFeatureFlags'); + ReactNoop = require('ReactNoop'); + ReactPortal = require('ReactPortal'); + ReactFeatureFlags.disableNewFiberFeatures = false; + }); + + afterEach(() => { + delete global.performance; + }); + + function Parent(props) { + return
{props.children}
; + } + + function Child(props) { + return
{props.children}
; + } + + it('measures a simple reconciliation', () => { + ReactNoop.render(); + addComment('Mount'); + ReactNoop.flush(); + + ReactNoop.render(); + addComment('Update'); + ReactNoop.flush(); + + ReactNoop.render(null); + addComment('Unmount'); + ReactNoop.flush(); + + expect(getFlameChart()).toMatchSnapshot(); + }); + + it('skips parents during setState', () => { + class A extends React.Component { + render() { + return
{this.props.children}
; + } + } + + class B extends React.Component { + render() { + return
{this.props.children}
; + } + } + + let a; + let b; + ReactNoop.render( + + + + a = inst} /> + + + + b = inst} /> + + + ); + ReactNoop.flush(); + resetFlamechart(); + + a.setState({}); + b.setState({}); + addComment('Should include just A and B, no Parents'); + ReactNoop.flush(); + expect(getFlameChart()).toMatchSnapshot(); + }); + + it('warns on cascading renders from setState', () => { + class Cascading extends React.Component { + componentDidMount() { + this.setState({}); + } + render() { + return
{this.props.children}
; + } + } + + ReactNoop.render(); + addComment('Should print a warning'); + ReactNoop.flush(); + expect(getFlameChart()).toMatchSnapshot(); + }); + + it('warns on cascading renders from top-level render', () => { + class Cascading extends React.Component { + componentDidMount() { + ReactNoop.renderToRootWithID(, 'b'); + addComment('Scheduling another root from componentDidMount'); + ReactNoop.flush(); + } + render() { + return
{this.props.children}
; + } + } + + ReactNoop.renderToRootWithID(, 'a'); + addComment('Rendering the first root'); + ReactNoop.flush(); + expect(getFlameChart()).toMatchSnapshot(); + }); + + it('does not treat setState from cWM or cWRP as cascading', () => { + class NotCascading extends React.Component { + componentWillMount() { + this.setState({}); + } + componentWillReceiveProps() { + this.setState({}); + } + render() { + return
{this.props.children}
; + } + } + + ReactNoop.render(); + addComment('Should not print a warning'); + ReactNoop.flush(); + ReactNoop.render(); + addComment('Should not print a warning'); + ReactNoop.flush(); + expect(getFlameChart()).toMatchSnapshot(); + }); + + it('captures all lifecycles', () => { + class AllLifecycles extends React.Component { + static childContextTypes = { + foo: React.PropTypes.any, + }; + shouldComponentUpdate() { + return true; + } + getChildContext() { + return {foo: 42}; + } + componentWillMount() {} + componentDidMount() {} + componentWillReceiveProps() {} + componentWillUpdate() {} + componentDidUpdate() {} + componentWillUnmount() {} + render() { + return
; + } + } + ReactNoop.render(); + addComment('Mount'); + ReactNoop.flush(); + ReactNoop.render(); + addComment('Update'); + ReactNoop.flush(); + ReactNoop.render(null); + addComment('Unmount'); + ReactNoop.flush(); + expect(getFlameChart()).toMatchSnapshot(); + }); + + it('measures deprioritized work', () => { + ReactNoop.performAnimationWork(() => { + ReactNoop.render( + + + + ); + }); + addComment('Flush the parent'); + ReactNoop.flushAnimationPri(); + addComment('Flush the child'); + ReactNoop.flushDeferredPri(); + expect(getFlameChart()).toMatchSnapshot(); + }); + + it('measures deferred work in chunks', () => { + class A extends React.Component { + render() { + return
{this.props.children}
; + } + } + + class B extends React.Component { + render() { + return
{this.props.children}
; + } + } + + ReactNoop.render( + +
+ + + + + + + ); + addComment('Start mounting Parent and A'); + ReactNoop.flushDeferredPri(40); + addComment('Mount B just a little (but not enough to memoize)'); + ReactNoop.flushDeferredPri(10); + addComment('Complete B and Parent'); + ReactNoop.flushDeferredPri(); + expect(getFlameChart()).toMatchSnapshot(); + }); + + it('recovers from fatal errors', () => { + function Baddie() { + throw new Error('Game over'); + } + + ReactNoop.render(); + try { + addComment('Will fatal'); + ReactNoop.flush(); + } catch (err) { + expect(err.message).toBe('Game over'); + } + ReactNoop.render(); + addComment('Will reconcile from a clean state'); + ReactNoop.flush(); + expect(getFlameChart()).toMatchSnapshot(); + }); + + it('recovers from caught errors', () => { + function Baddie() { + throw new Error('Game over'); + } + + function ErrorReport() { + return
; + } + + class Boundary extends React.Component { + state = {error: null}; + unstable_handleError(error) { + this.setState({error}); + } + render() { + if (this.state.error) { + return ; + } + return this.props.children; + } + } + + ReactNoop.render( + + + + + + + + ); + addComment('Stop on Baddie and restart from Boundary'); + ReactNoop.flush(); + expect(getFlameChart()).toMatchSnapshot(); + }); + + it('deduplicates lifecycle names during commit to reduce overhead', () => { + class A extends React.Component { + componentDidUpdate() {} + render() { + return
; + } + } + + class B extends React.Component { + componentDidUpdate(prevProps) { + if (this.props.cascade && !prevProps.cascade) { + this.setState({}); + } + } + render() { + return
; + } + } + + ReactNoop.render( + + + + + + + ); + ReactNoop.flush(); + resetFlamechart(); + + ReactNoop.render( + + + + + + + ); + addComment('The commit phase should mention A and B just once'); + ReactNoop.flush(); + ReactNoop.render( + + + + + + + ); + addComment('Because of deduplication, we don\'t know B was cascading,'); + addComment('but we should still see the warning for the commit phase.'); + ReactNoop.flush(); + expect(getFlameChart()).toMatchSnapshot(); + }); + + it('supports coroutines', () => { + function Continuation({ isSame }) { + return ; + } + + function CoChild({ bar }) { + return ReactCoroutine.createYield({ + props: { + bar: bar, + }, + continuation: Continuation, + }); + } + + function Indirection() { + return [, ]; + } + + function HandleYields(props, yields) { + return yields.map(y => + + ); + } + + function CoParent(props) { + return ReactCoroutine.createCoroutine( + props.children, + HandleYields, + props + ); + } + + function App() { + return
; + } + + ReactNoop.render(); + ReactNoop.flush(); + expect(getFlameChart()).toMatchSnapshot(); + }); + + it('supports portals', () => { + const noopContainer = {children: []}; + ReactNoop.render( + + {ReactPortal.createPortal(, noopContainer, null)} + + ); + ReactNoop.flush(); + expect(getFlameChart()).toMatchSnapshot(); + }); +}); diff --git a/src/renderers/shared/fiber/__tests__/__snapshots__/ReactIncrementalPerf-test.js.snap b/src/renderers/shared/fiber/__tests__/__snapshots__/ReactIncrementalPerf-test.js.snap new file mode 100644 index 0000000000..760598a4ce --- /dev/null +++ b/src/renderers/shared/fiber/__tests__/__snapshots__/ReactIncrementalPerf-test.js.snap @@ -0,0 +1,260 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ReactDebugFiberPerf captures all lifecycles 1`] = ` +"// Mount +⚛ (React Tree Reconciliation) + ⚛ AllLifecycles [mount] + ⚛ AllLifecycles.componentWillMount + ⚛ AllLifecycles.getChildContext + ⚛ (Committing Changes) + ⚛ (Committing Host Effects: 1 Total) + ⚛ (Calling Lifecycle Methods: 1 Total) + ⚛ AllLifecycles.componentDidMount + +// Update +⚛ (React Tree Reconciliation) + ⚛ AllLifecycles [update] + ⚛ AllLifecycles.componentWillReceiveProps + ⚛ AllLifecycles.shouldComponentUpdate + ⚛ AllLifecycles.componentWillUpdate + ⚛ AllLifecycles.getChildContext + ⚛ (Committing Changes) + ⚛ (Committing Host Effects: 2 Total) + ⚛ (Calling Lifecycle Methods: 2 Total) + ⚛ AllLifecycles.componentDidUpdate + +// Unmount +⚛ (React Tree Reconciliation) + ⚛ (Committing Changes) + ⚛ (Committing Host Effects: 1 Total) + ⚛ AllLifecycles.componentWillUnmount + ⚛ (Calling Lifecycle Methods: 0 Total) +" +`; + +exports[`ReactDebugFiberPerf deduplicates lifecycle names during commit to reduce overhead 1`] = ` +"// The commit phase should mention A and B just once +⚛ (React Tree Reconciliation) + ⚛ Parent [update] + ⚛ A [update] + ⚛ B [update] + ⚛ A [update] + ⚛ B [update] + ⚛ (Committing Changes) + ⚛ (Committing Host Effects: 9 Total) + ⚛ (Calling Lifecycle Methods: 9 Total) + ⚛ A.componentDidUpdate + ⚛ B.componentDidUpdate + +// Because of deduplication, we don't know B was cascading, +// but we should still see the warning for the commit phase. +⛔ (React Tree Reconciliation) Warning: There were cascading updates + ⚛ Parent [update] + ⚛ A [update] + ⚛ B [update] + ⚛ A [update] + ⚛ B [update] + ⛔ (Committing Changes) Warning: Lifecycle hook scheduled a cascading update + ⚛ (Committing Host Effects: 9 Total) + ⚛ (Calling Lifecycle Methods: 9 Total) + ⚛ A.componentDidUpdate + ⚛ B.componentDidUpdate + ⚛ B [update] + ⛔ (Committing Changes) Warning: Caused by a cascading update in earlier commit + ⚛ (Committing Host Effects: 3 Total) + ⚛ (Calling Lifecycle Methods: 3 Total) + ⚛ B.componentDidUpdate +" +`; + +exports[`ReactDebugFiberPerf does not treat setState from cWM or cWRP as cascading 1`] = ` +"// Should not print a warning +⚛ (React Tree Reconciliation) + ⚛ Parent [mount] + ⚛ NotCascading [mount] + ⚛ NotCascading.componentWillMount + ⚛ (Committing Changes) + ⚛ (Committing Host Effects: 1 Total) + ⚛ (Calling Lifecycle Methods: 0 Total) + +// Should not print a warning +⚛ (React Tree Reconciliation) + ⚛ Parent [update] + ⚛ NotCascading [update] + ⚛ NotCascading.componentWillReceiveProps + ⚛ (Committing Changes) + ⚛ (Committing Host Effects: 2 Total) + ⚛ (Calling Lifecycle Methods: 2 Total) +" +`; + +exports[`ReactDebugFiberPerf measures a simple reconciliation 1`] = ` +"// Mount +⚛ (React Tree Reconciliation) + ⚛ Parent [mount] + ⚛ Child [mount] + ⚛ (Committing Changes) + ⚛ (Committing Host Effects: 1 Total) + ⚛ (Calling Lifecycle Methods: 0 Total) + +// Update +⚛ (React Tree Reconciliation) + ⚛ Parent [update] + ⚛ Child [update] + ⚛ (Committing Changes) + ⚛ (Committing Host Effects: 2 Total) + ⚛ (Calling Lifecycle Methods: 2 Total) + +// Unmount +⚛ (React Tree Reconciliation) + ⚛ (Committing Changes) + ⚛ (Committing Host Effects: 1 Total) + ⚛ (Calling Lifecycle Methods: 0 Total) +" +`; + +exports[`ReactDebugFiberPerf measures deferred work in chunks 1`] = ` +"// Start mounting Parent and A +⚛ (React Tree Reconciliation) + ⚛ Parent [mount] + ⚛ A [mount] + ⚛ Child [mount] + +// Mount B just a little (but not enough to memoize) +⚛ (React Tree Reconciliation) + ⚛ Parent [mount] + ⚛ B [mount] + +// Complete B and Parent +⚛ (React Tree Reconciliation) + ⚛ Parent [mount] + ⚛ B [mount] + ⚛ Child [mount] + ⚛ (Committing Changes) + ⚛ (Committing Host Effects: 1 Total) + ⚛ (Calling Lifecycle Methods: 0 Total) +" +`; + +exports[`ReactDebugFiberPerf measures deprioritized work 1`] = ` +"// Flush the parent +⚛ (React Tree Reconciliation) + ⚛ Parent [mount] + ⚛ (Committing Changes) + ⚛ (Committing Host Effects: 1 Total) + ⚛ (Calling Lifecycle Methods: 0 Total) + +// Flush the child +⚛ (React Tree Reconciliation) + ⚛ Child [mount] + ⚛ (Committing Changes) + ⚛ (Committing Host Effects: 3 Total) + ⚛ (Calling Lifecycle Methods: 2 Total) +" +`; + +exports[`ReactDebugFiberPerf recovers from caught errors 1`] = ` +"// Stop on Baddie and restart from Boundary +⛔ (React Tree Reconciliation) Warning: There were cascading updates + ⚛ Parent [mount] + ⚛ Boundary [mount] + ⚛ Parent [mount] + ⚛ Baddie [mount] + ⛔ (Committing Changes) Warning: Lifecycle hook scheduled a cascading update + ⚛ (Committing Host Effects: 2 Total) + ⚛ (Calling Lifecycle Methods: 1 Total) + ⚛ Boundary [update] + ⚛ ErrorReport [mount] + ⛔ (Committing Changes) Warning: Caused by a cascading update in earlier commit + ⚛ (Committing Host Effects: 2 Total) + ⚛ (Calling Lifecycle Methods: 1 Total) +" +`; + +exports[`ReactDebugFiberPerf recovers from fatal errors 1`] = ` +"// Will fatal +⚛ (React Tree Reconciliation) + ⚛ Parent [mount] + ⚛ Baddie [mount] + ⚛ (Committing Changes) + ⚛ (Committing Host Effects: 1 Total) + ⚛ (Calling Lifecycle Methods: 1 Total) + +// Will reconcile from a clean state +⚛ (React Tree Reconciliation) + ⚛ Parent [mount] + ⚛ Child [mount] + ⚛ (Committing Changes) + ⚛ (Committing Host Effects: 1 Total) + ⚛ (Calling Lifecycle Methods: 0 Total) +" +`; + +exports[`ReactDebugFiberPerf skips parents during setState 1`] = ` +"// Should include just A and B, no Parents +⚛ (React Tree Reconciliation) + ⚛ A [update] + ⚛ B [update] + ⚛ (Committing Changes) + ⚛ (Committing Host Effects: 6 Total) + ⚛ (Calling Lifecycle Methods: 6 Total) +" +`; + +exports[`ReactDebugFiberPerf supports coroutines 1`] = ` +"⚛ (React Tree Reconciliation) + ⚛ App [mount] + ⚛ CoParent [mount] + ⚛ HandleYields [mount] + ⚛ Indirection [mount] + ⚛ CoChild [mount] + ⚛ CoChild [mount] + ⚛ Continuation [mount] + ⚛ Continuation [mount] + ⚛ (Committing Changes) + ⚛ (Committing Host Effects: 3 Total) + ⚛ (Calling Lifecycle Methods: 0 Total) +" +`; + +exports[`ReactDebugFiberPerf supports portals 1`] = ` +"⚛ (React Tree Reconciliation) + ⚛ Parent [mount] + ⚛ Child [mount] + ⚛ (Committing Changes) + ⚛ (Committing Host Effects: 3 Total) + ⚛ (Calling Lifecycle Methods: 1 Total) +" +`; + +exports[`ReactDebugFiberPerf warns on cascading renders from setState 1`] = ` +"// Should print a warning +⛔ (React Tree Reconciliation) Warning: There were cascading updates + ⚛ Parent [mount] + ⚛ Cascading [mount] + ⛔ (Committing Changes) Warning: Lifecycle hook scheduled a cascading update + ⚛ (Committing Host Effects: 2 Total) + ⚛ (Calling Lifecycle Methods: 1 Total) + ⛔ Cascading.componentDidMount Warning: Scheduled a cascading update + ⚛ Cascading [update] + ⛔ (Committing Changes) Warning: Caused by a cascading update in earlier commit + ⚛ (Committing Host Effects: 2 Total) + ⚛ (Calling Lifecycle Methods: 2 Total) +" +`; + +exports[`ReactDebugFiberPerf warns on cascading renders from top-level render 1`] = ` +"// Rendering the first root +⛔ (React Tree Reconciliation) Warning: There were cascading updates + ⚛ Cascading [mount] + ⛔ (Committing Changes) Warning: Lifecycle hook scheduled a cascading update + ⚛ (Committing Host Effects: 1 Total) + ⚛ (Calling Lifecycle Methods: 1 Total) + ⛔ Cascading.componentDidMount Warning: Scheduled a cascading update + // Scheduling another root from componentDidMount + ⚛ Child [mount] + ⛔ (Committing Changes) Warning: Caused by a cascading update in earlier commit + ⚛ (Committing Host Effects: 1 Total) + ⚛ (Calling Lifecycle Methods: 0 Total) +" +`; diff --git a/src/renderers/shared/shared/event/EventPluginHub.js b/src/renderers/shared/shared/event/EventPluginHub.js index 6c4235ea42..179e89d651 100644 --- a/src/renderers/shared/shared/event/EventPluginHub.js +++ b/src/renderers/shared/shared/event/EventPluginHub.js @@ -126,9 +126,12 @@ var EventPluginHub = { // TODO: shouldPreventMouseEvent is DOM-specific and definitely should not // live here; needs to be moved to a better place soon if (typeof inst.tag === 'number') { - const props = EventPluginUtils.getFiberCurrentPropsFromNode( - inst.stateNode - ); + const stateNode = inst.stateNode; + if (!stateNode) { + // Work in progress (ex: onload events in incremental mode). + return null; + } + const props = EventPluginUtils.getFiberCurrentPropsFromNode(stateNode); if (!props) { // Work in progress. return null; diff --git a/src/renderers/shared/shared/event/EventPluginRegistry.js b/src/renderers/shared/shared/event/EventPluginRegistry.js index fa868dca10..b379ce1e9d 100644 --- a/src/renderers/shared/shared/event/EventPluginRegistry.js +++ b/src/renderers/shared/shared/event/EventPluginRegistry.js @@ -14,7 +14,6 @@ import type { DispatchConfig, - ReactSyntheticEvent, } from 'ReactSyntheticEventType'; import type { @@ -256,80 +255,6 @@ var EventPluginRegistry = { recomputePluginOrdering(); } }, - - /** - * Looks up the plugin for the supplied event. - * - * @param {object} event A synthetic event. - * @return {?object} The plugin that created the supplied event. - * @internal - */ - getPluginModuleForEvent: function( - event: ReactSyntheticEvent, - ): null | PluginModule { - var dispatchConfig = event.dispatchConfig; - if (dispatchConfig.registrationName) { - return EventPluginRegistry.registrationNameModules[ - dispatchConfig.registrationName - ] || null; - } - if (dispatchConfig.phasedRegistrationNames !== undefined) { - // pulling phasedRegistrationNames out of dispatchConfig helps Flow see - // that it is not undefined. - var {phasedRegistrationNames} = dispatchConfig; - for (var phase in phasedRegistrationNames) { - if (!phasedRegistrationNames.hasOwnProperty(phase)) { - continue; - } - var pluginModule = EventPluginRegistry.registrationNameModules[ - phasedRegistrationNames[phase] - ]; - if (pluginModule) { - return pluginModule; - } - } - } - return null; - }, - - /** - * Exposed for unit testing. - * @private - */ - _resetEventPlugins: function(): void { - eventPluginOrder = null; - for (var pluginName in namesToPlugins) { - if (namesToPlugins.hasOwnProperty(pluginName)) { - delete namesToPlugins[pluginName]; - } - } - EventPluginRegistry.plugins.length = 0; - - var eventNameDispatchConfigs = EventPluginRegistry.eventNameDispatchConfigs; - for (var eventName in eventNameDispatchConfigs) { - if (eventNameDispatchConfigs.hasOwnProperty(eventName)) { - delete eventNameDispatchConfigs[eventName]; - } - } - - var registrationNameModules = EventPluginRegistry.registrationNameModules; - for (var registrationName in registrationNameModules) { - if (registrationNameModules.hasOwnProperty(registrationName)) { - delete registrationNameModules[registrationName]; - } - } - - if (__DEV__) { - var possibleRegistrationNames = - EventPluginRegistry.possibleRegistrationNames; - for (var lowerCasedName in possibleRegistrationNames) { - if (possibleRegistrationNames.hasOwnProperty(lowerCasedName)) { - delete possibleRegistrationNames[lowerCasedName]; - } - } - } - }, - }; module.exports = EventPluginRegistry; diff --git a/src/renderers/shared/shared/event/__tests__/EventPluginRegistry-test.js b/src/renderers/shared/shared/event/__tests__/EventPluginRegistry-test.js index 245e7bcc49..01e872abb6 100644 --- a/src/renderers/shared/shared/event/__tests__/EventPluginRegistry-test.js +++ b/src/renderers/shared/shared/event/__tests__/EventPluginRegistry-test.js @@ -17,8 +17,8 @@ describe('EventPluginRegistry', () => { var createPlugin; beforeEach(() => { + jest.resetModuleRegistry(); EventPluginRegistry = require('EventPluginRegistry'); - EventPluginRegistry._resetEventPlugins(); createPlugin = function(properties) { return Object.assign({extractEvents: function() {}}, properties); @@ -225,40 +225,4 @@ describe('EventPluginRegistry', () => { '`one`.' ); }); - - it('should be able to get the plugin from synthetic events', () => { - var clickDispatchConfig = { - registrationName: 'onClick', - }; - var magicDispatchConfig = { - phasedRegistrationNames: { - bubbled: 'onMagicBubble', - captured: 'onMagicCapture', - }, - }; - - var OnePlugin = createPlugin({ - eventTypes: { - click: clickDispatchConfig, - magic: magicDispatchConfig, - }, - }); - - var clickEvent = {dispatchConfig: clickDispatchConfig}; - var magicEvent = {dispatchConfig: magicDispatchConfig}; - - expect(EventPluginRegistry.getPluginModuleForEvent(clickEvent)).toBe(null); - expect(EventPluginRegistry.getPluginModuleForEvent(magicEvent)).toBe(null); - - EventPluginRegistry.injectEventPluginsByName({one: OnePlugin}); - EventPluginRegistry.injectEventPluginOrder(['one']); - - expect( - EventPluginRegistry.getPluginModuleForEvent(clickEvent) - ).toBe(OnePlugin); - expect( - EventPluginRegistry.getPluginModuleForEvent(magicEvent) - ).toBe(OnePlugin); - }); - }); diff --git a/src/renderers/shared/stack/reconciler/ReactUpdateQueue.js b/src/renderers/shared/stack/reconciler/ReactUpdateQueue.js index 19aee0d985..8e77b3346e 100644 --- a/src/renderers/shared/stack/reconciler/ReactUpdateQueue.js +++ b/src/renderers/shared/stack/reconciler/ReactUpdateQueue.js @@ -134,8 +134,8 @@ var ReactUpdateQueue = { return; } - if (callback) { - callback = callback === undefined ? null : callback; + callback = callback === undefined ? null : callback; + if (callback !== null) { if (__DEV__) { warnOnInvalidCallback(callback, callerName); } @@ -174,8 +174,8 @@ var ReactUpdateQueue = { internalInstance._pendingStateQueue = [completeState]; internalInstance._pendingReplaceState = true; - if (callback) { - callback = callback === undefined ? null : callback; + callback = callback === undefined ? null : callback; + if (callback !== null) { if (__DEV__) { warnOnInvalidCallback(callback, callerName); } @@ -222,8 +222,8 @@ var ReactUpdateQueue = { (internalInstance._pendingStateQueue = []); queue.push(partialState); - if (callback) { - callback = callback === undefined ? null : callback; + callback = callback === undefined ? null : callback; + if (callback !== null) { if (__DEV__) { warnOnInvalidCallback(callback, callerName); } diff --git a/src/renderers/shared/utils/ReactErrorUtils.js b/src/renderers/shared/utils/ReactErrorUtils.js index c2cd671767..f53bdddc5a 100644 --- a/src/renderers/shared/utils/ReactErrorUtils.js +++ b/src/renderers/shared/utils/ReactErrorUtils.js @@ -26,7 +26,7 @@ let caughtError = null; const ReactErrorUtils = { invokeGuardedCallback: function( name: string | null, - func: (A, B, C, D, E, F) => void, + func: (a: A, b: B, c: C, d: D, e: E, f: F) => void, context: Context, a: A, b: B, @@ -55,7 +55,7 @@ const ReactErrorUtils = { */ invokeGuardedCallbackAndCatchFirstError: function( name: string | null, - func: (A, B, C, D, E, F) => void, + func: (a: A, b: B, c: C, d: D, e: E, f: F) => void, context: Context, a: A, b: B, diff --git a/src/renderers/shared/utils/__tests__/ReactErrorUtils-test.js b/src/renderers/shared/utils/__tests__/ReactErrorUtils-test.js index 74069e5900..c540a2bb1e 100644 --- a/src/renderers/shared/utils/__tests__/ReactErrorUtils-test.js +++ b/src/renderers/shared/utils/__tests__/ReactErrorUtils-test.js @@ -106,5 +106,30 @@ describe('ReactErrorUtils', () => { expect(err3).toBe(null); // Returns null because inner error was already captured expect(err2).toBe(err1); }); + + it(`can be shimmed (${environment})`, () => { + const ops = []; + // Override the original invokeGuardedCallback + ReactErrorUtils.invokeGuardedCallback = function(name, func, context, a) { + ops.push(a); + try { + func.call(context, a); + } catch (error) { + return error; + } + return null; + }; + + var err = new Error('foo'); + var callback = function() { + throw err; + }; + ReactErrorUtils.invokeGuardedCallbackAndCatchFirstError('foo', callback, null, 'somearg'); + expect(() => ReactErrorUtils.rethrowCaughtError()).toThrow(err); + // invokeGuardedCallbackAndCatchFirstError and rethrowCaughtError close + // over ReactErrorUtils.invokeGuardedCallback so should use the + // shimmed version. + expect(ops).toEqual(['somearg']); + }); } }); diff --git a/src/shared/ReactFiberComponentTreeHook.js b/src/shared/ReactFiberComponentTreeHook.js new file mode 100644 index 0000000000..df24a78b0d --- /dev/null +++ b/src/shared/ReactFiberComponentTreeHook.js @@ -0,0 +1,73 @@ +/** + * Copyright 2016-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * @flow + * @providesModule ReactFiberComponentTreeHook + */ + +'use strict'; + +var ReactTypeOfWork = require('ReactTypeOfWork'); +var { + IndeterminateComponent, + FunctionalComponent, + ClassComponent, + HostComponent, +} = ReactTypeOfWork; +var getComponentName = require('getComponentName'); + +import type { Fiber } from 'ReactFiber'; + +function describeComponentFrame(name, source: any, ownerName) { + return '\n in ' + (name || 'Unknown') + ( + source ? + ' (at ' + source.fileName.replace(/^.*[\\\/]/, '') + ':' + + source.lineNumber + ')' : + ownerName ? + ' (created by ' + ownerName + ')' : + '' + ); +} + +function describeFiber(fiber : Fiber) : string { + switch (fiber.tag) { + case IndeterminateComponent: + case FunctionalComponent: + case ClassComponent: + case HostComponent: + var owner = fiber._debugOwner; + var source = fiber._debugSource; + var name = getComponentName(fiber); + var ownerName = null; + if (owner) { + ownerName = getComponentName(owner); + } + return describeComponentFrame(name, source, ownerName); + default: + return ''; + } +} + +// This function can only be called with a work-in-progress fiber and +// only during begin or complete phase. Do not call it under any other +// circumstances. +function getStackAddendumByWorkInProgressFiber(workInProgress : Fiber) : string { + var info = ''; + var node = workInProgress; + do { + info += describeFiber(node); + // Otherwise this return pointer might point to the wrong tree: + node = node.return; + } while (node); + return info; +} + +module.exports = { + getStackAddendumByWorkInProgressFiber, + describeComponentFrame, +};