From 5331dfd1a509aeaffb33d9096941c02c11d9eb58 Mon Sep 17 00:00:00 2001 From: jinmmd Date: Tue, 27 Sep 2016 01:39:03 +0800 Subject: [PATCH 001/271] =?UTF-8?q?Change=20[API=E5=8F=82=E8=80=83]=20from?= =?UTF-8?q?=20"top-level-api.html"=20to=20"top-level-api-zh-CN.html"=20(#7?= =?UTF-8?q?790)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/docs/tutorial.zh-CN.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/docs/tutorial.zh-CN.md b/docs/docs/tutorial.zh-CN.md index 15ac1ced8a33..f76633fd6c9e 100755 --- a/docs/docs/tutorial.zh-CN.md +++ b/docs/docs/tutorial.zh-CN.md @@ -783,4 +783,4 @@ var CommentBox = React.createClass({ ### 祝贺! -你刚刚通过几个简单的步骤建立了一个评论框。学习更多关于[为什么使用 React](/react/docs/why-react-zh-CN.html), 或者深入 [API 参考](/react/docs/top-level-api.html) 开始钻研!祝你好运! +你刚刚通过几个简单的步骤建立了一个评论框。学习更多关于[为什么使用 React](/react/docs/why-react-zh-CN.html), 或者深入 [API 参考](/react/docs/top-level-api-zh-CN.html) 开始钻研!祝你好运! From d9957ac0759a6d1ff5e44b6356e91fe8ed83fafb Mon Sep 17 00:00:00 2001 From: jinmmd Date: Tue, 27 Sep 2016 01:39:34 +0800 Subject: [PATCH 002/271] =?UTF-8?q?Change=20[=E5=85=A5=E9=97=A8=E6=95=99?= =?UTF-8?q?=E7=A8=8B]=20from=20"tutorial.html"=20to=20"tutorial-zh-CN.html?= =?UTF-8?q?"=20(#7789)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/docs/getting-started.zh-CN.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/docs/getting-started.zh-CN.md b/docs/docs/getting-started.zh-CN.md index 54c80ba377f3..da006ff20eaf 100644 --- a/docs/docs/getting-started.zh-CN.md +++ b/docs/docs/getting-started.zh-CN.md @@ -161,7 +161,7 @@ ReactDOM.render( ## 下一步 -去看看[入门教程](/react/docs/tutorial.html) 和入门教程包 `examples` 目录下的其它例子学习更多。 +去看看[入门教程](/react/docs/tutorial-zh-CN.html) 和入门教程包 `examples` 目录下的其它例子学习更多。 我们还有一个社区开发者共建的 Wiki:[workflows, UI-components, routing, data management etc.](https://github.com/facebook/react/wiki/Complementary-Tools) From dbd9c4b205321b9d881966256f9b7203fc794277 Mon Sep 17 00:00:00 2001 From: Christopher Chedeau Date: Mon, 26 Sep 2016 12:58:51 -0700 Subject: [PATCH 003/271] Introduce facts-tracker (#7747) * Introduce facts-tracker We want to be able to track various things like number of files that are flowified, number of tests passing with Fiber... This introduces a tool that lets you do that. The API is very simple, you execute the script with a list of tuples [key value] and it's going to create a `facts` branch and put a txt file for each fact and values over time. ``` node scripts/facts-tracker/index.js \ "flow-files" "$COUNT_WITH_FLOW/$COUNT_ALL_FILES" ``` Test Plan: This is tricky to test because Travis only exposes the private variables (github token) when it processes a committed file and not on branches. The reason is that otherwise anyone could send a pull requests that does `echo $GITHUB_TOKEN` and steal your token. Given this constraint, I did all the work using two of my repos: - https://github.com/vjeux/facts-tracker - https://github.com/vjeux/facts-tracker-test and am sending this pull request that should work as is /fingers crossed/, but we won't be able to test it out until it is committed. Note that once this lands, I'm going to kill those two repos. * Update with all the suggested changes * Branch on a flow type in travis.yml * Use $GITHUB_TOKEN * properly escape it --- .travis.yml | 10 ++ scripts/facts-tracker/index.js | 178 +++++++++++++++++++++++++++++++++ 2 files changed, 188 insertions(+) create mode 100644 scripts/facts-tracker/index.js diff --git a/.travis.yml b/.travis.yml index af0f57abe068..f5725a14cb98 100644 --- a/.travis.yml +++ b/.travis.yml @@ -79,6 +79,16 @@ script: ./node_modules/.bin/grunt jest:normal git checkout -- src/renderers/dom/shared/ReactDOMFeatureFlags.js ./node_modules/.bin/gulp react:extract-errors + elif [ "$TEST_TYPE" = flow ]; then + set -e + ./node_modules/.bin/grunt flow + + ALL_FILES=`find src -name '*.js' | grep -v umd/ | grep -v __tests__ | grep -v __mocks__` + COUNT_ALL_FILES=`echo $ALL_FILES | wc -l` + COUNT_WITH_FLOW=`grep '@flow' $ALL_FILES | perl -pe 's/:.+//' | wc -l` + node scripts/facts-tracker/index.js \ + "flow-files" "$COUNT_WITH_FLOW/$COUNT_ALL_FILES" + else ./node_modules/.bin/grunt $TEST_TYPE fi diff --git a/scripts/facts-tracker/index.js b/scripts/facts-tracker/index.js new file mode 100644 index 000000000000..70fa9e20061d --- /dev/null +++ b/scripts/facts-tracker/index.js @@ -0,0 +1,178 @@ +#!/usr/bin/env node + +/** + * Copyright (c) 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. + */ + +'use strict'; + +var fs = require('fs'); +var path = require('path'); +var execSync = require('child_process').execSync; + +function escape(value) { + return '\'' + value.replace(/'/g, "'\\''") + '\''; +} + +var cwd = null; +function exec(command) { + console.error('>', command.replace(process.env.GITHUB_TOKEN, '************')); + return execSync(command, cwd ? {cwd: cwd} : undefined).toString(); +} + +var isInsideOfTravis = !!process.env.TRAVIS_REPO_SLUG; + +if (isInsideOfTravis) { + if (process.env.TRAVIS_BRANCH !== 'master') { + console.error('facts-tracker: Branch is not master, exiting...'); + process.exit(0); + } + + if (process.env.TRAVIS_PULL_REQUEST !== 'false') { + console.error('facts-tracker: This is a pull request, exiting...'); + process.exit(0); + } + + if (!process.env.GITHUB_USER || !process.env.GITHUB_TOKEN) { + console.error( + 'In order to use facts-tracker, you need to configure a ' + + 'few environment variables in order to be able to commit to the ' + + 'repository. Follow those steps to get you setup:\n' + + '\n' + + 'Go to https://github.com/settings/tokens/new\n' + + ' - Fill "Token description" with "facts-tracker for ' + + process.env.TRAVIS_REPO_SLUG + '"\n' + + ' - Check "public_repo"\n' + + ' - Press "Generate Token"\n' + + '\n' + + 'In a different tab, go to https://travis-ci.org/' + + process.env.TRAVIS_REPO_SLUG + '/settings\n' + + ' - Make sure "Build only if .travis.yml is present" is ON\n' + + ' - Fill "Name" with "GITHUB_TOKEN" and "Value" with the token you ' + + 'just generated. Press "Add"\n' + + ' - Fill "Name" with "GITHUB_USER" and "Value" with the name of the ' + + 'account you generated the token with. Press "Add"\n' + + '\n' + + 'Once this is done, commit anything to the repository to restart ' + + 'Travis and it should work :)' + ); + process.exit(1); + } + + exec( + 'echo "machine github.com login $GITHUB_USER password $GITHUB_TOKEN"' + + '> ~/.netrc' + ); + exec( + 'git config --global user.name ' + + escape(process.env.GITHUB_USER_NAME || 'facts-tracker') + ); + exec( + 'git config --global user.email ' + + escape(process.env.GITHUB_USER_EMAIL || 'facts-tracker@no-reply.github.com') + ); +} + +if (process.argv.length <= 2) { + console.error('Usage: facts-tracker ...'); + process.exit(1); +} + +function getRepoSlug() { + if (isInsideOfTravis) { + return process.env.TRAVIS_REPO_SLUG; + } + + var remotes = exec('git remote -v').split('\n'); + for (var i = 0; i < remotes.length; ++i) { + var match = remotes[i].match(/^origin\t[^:]+:([^\.]+).+\(fetch\)/); + if (match) { + return match[1]; + } + } + + console.error('Cannot find repository slug, sorry.'); + process.exit(1); +} + +var repoSlug = getRepoSlug(); +var currentCommitHash = exec('git rev-parse HEAD').trim(); +var currentTimestamp = new Date().toISOString() + .replace('T', ' ') + .replace(/\..+/, ''); + +function checkoutFactsFolder() { + var factsFolder = '../' + repoSlug.split('/')[1] + '-facts'; + if (!fs.existsSync(factsFolder)) { + var escapedRepoURL; + if (isInsideOfTravis) { + escapedRepoURL = 'https://$GITHUB_TOKEN@github.com/' + escape(repoSlug) + '.git'; + } else { + escapedRepoURL = escape('git@github.com:' + repoSlug + '.git'); + } + + exec( + 'git clone ' + + '--branch facts ' + + '--depth=5 ' + + escapedRepoURL + ' ' + + escape(factsFolder) + ); + } + + cwd = path.resolve(factsFolder); + exec('git fetch'); + if (exec('git status --porcelain')) { + console.error('facts-tracker: `git status` is not clean, aborting.'); + process.exit(1); + } + exec('git rebase origin/facts'); +} +checkoutFactsFolder(); + +for (var i = 2; i < process.argv.length; i += 2) { + var name = process.argv[i].trim(); + var value = process.argv[i + 1]; + if (value.indexOf('\n') !== -1) { + console.error( + 'facts-tracker: skipping', name, + 'as the value contains new lines:', value + ); + continue; + } + + var filename = name + '.txt'; + try { + var lastLine = exec('tail -n 1 ' + escape(filename)); + } catch (e) { + // ignore error + } + var lastValue = lastLine && lastLine + .replace(/^[^\t]+\t[^\t]+\t/, '') // commit hash \t timestamp \t + .slice(0, -1); // trailing \n + + if (value !== lastValue) { + fs.appendFileSync( + path.resolve(cwd, filename), + currentCommitHash + '\t' + currentTimestamp + '\t' + value + '\n' + ); + } + + console.log(name); + console.log(lastValue); + console.log(value); +} + +if (exec('git status --porcelain')) { + exec('git add --all'); + exec('git commit -m ' + escape('Adding facts for ' + currentCommitHash)); + exec('git push origin facts'); +} else { + console.error('facts-tracker: nothing to update'); +} +cwd = null; From 603249155513668bdb65b712d52bc258a932762a Mon Sep 17 00:00:00 2001 From: imjanghyuk Date: Tue, 27 Sep 2016 06:02:33 +0900 Subject: [PATCH 004/271] Update 07-forms.ko-KR.md (#7809) fix spelling --- docs/docs/07-forms.ko-KR.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/docs/07-forms.ko-KR.md b/docs/docs/07-forms.ko-KR.md index 08d210498a3e..674a7ef5212f 100644 --- a/docs/docs/07-forms.ko-KR.md +++ b/docs/docs/07-forms.ko-KR.md @@ -67,9 +67,9 @@ HTML에서는 ` -``` - -Per l'HTML, questo approccio permette agli sviluppatori di fornire facilmente valori su più righe. Tuttavia, dal momento che React è JavaScript, non abbiamo limitazioni sulle stringhe e possiamo usare `\n` se desideriamo andare a capo. In un mondo in cui abbiamo `value` e `defaultValue`, il ruolo giocato dal nodo figlio è ambiguo. Per questa ragione, non dovresti utilizzare il nodo figlio quando imposti il valore delle ` -``` - -HTMLでは、開発者は簡単に複数行に渡る値を提供できます。しかし、ReactがJavaScriptであるので、文字列の制限を私たちは持っておらず、改行をしたい場合は `\n` を使えます。 `value` と `defaultValue` を私たちが持っている世界では、子要素が果たす役割は曖昧になっています。この理由から、以下のように ` -``` - -HTML에서는 이렇게 하면 여러 줄의 값을 쉽게 개발자가 넣을 수 있게 합니다. 하지만, React는 JavaScript기 때문에, 우리는 문자열 제한이 없고 개행이 필요하면 `\n`을 사용할 수 있습니다. 이 곳에서는 `value`와 `defaultValue`가 있고, 그것이 자식들의 역할을 모호하게 합니다. 이런 이유로, ` -``` - -For HTML, this easily allows developers to supply multiline values. However, since React is JavaScript, we do not have string limitations and can use `\n` if we want newlines. In a world where we have `value` and `defaultValue`, it is ambiguous what role children play. For this reason, you should not use children when setting ` -``` - -对 HTML 而言,让开发者设置多行的值很容易。但是,由于 React 是 JavaScript,没有字符串限制,可以使用 `\n` 实现换行。简言之,React 已经有 `value`、`defaultValue` 属性,` +``` + +For HTML, this easily allows developers to supply multiline values. However, since React is JavaScript, we do not have string limitations and can use `\n` if we want newlines. In a world where we have `value` and `defaultValue`, it is ambiguous what role children play. For this reason, you should not use children when setting ` ``` @@ -220,16 +253,16 @@ To make an uncontrolled component, `defaultValue` is used instead. > > You can pass an array into the `value` attribute, allowing you to select multiple options in a `select` tag: ` - + ); } } -ReactDOM.render(
, document.getElementById('root')); +ReactDOM.render( + , + document.getElementById('root') +); ``` -[Try this on CodePen.](https://codepen.io/ericnakagawa/pen/pExQbL?editors=0010) +[Try it out on CodePen.](https://codepen.io/gaearon/pen/qawrbr?editors=0010) -### Basic Radio Button +### Uncontrolled Radio Button -```javascript +```javascript{25,34,35,44} class Form extends React.Component { constructor(props) { super(props); @@ -365,38 +414,53 @@ class Form extends React.Component { return (


-

- +
+
+
); } } -ReactDOM.render(, document.getElementById('root')); +ReactDOM.render( + , + document.getElementById('root') +); ``` -[Try this on CodePen.](https://codepen.io/ericnakagawa/pen/WGaYVg?editors=0010) +[Try it out on CodePen.](https://codepen.io/gaearon/pen/ozOPLJ?editors=0010) +### Uncontrolled Checkbox -### Basic Uncontrolled Checkbox - -```javascript +```javascript{37,45,46,54} class Form extends React.Component { constructor(props) { super(props); @@ -406,10 +470,16 @@ class Form extends React.Component { } handleChange(event) { - let value = event.target.value; - let checked = this.state.checked; // copy - if (!checked[value]) { checked[value] = true; } else { checked[value] = false; } - this.setState({checked: checked}) + const value = event.target.value; + // Copy the object so we don't mutate the old state. + // (This requires an Object.assign polyfill): + const checked = Object.assign({}, this.state.checked) + if (!checked[value]) { + checked[value] = true; + } else { + checked[value] = false; + }; + this.setState({checked}); } handleSubmit(event) { @@ -424,36 +494,43 @@ class Form extends React.Component { return (


- -

- +
+
+
); } } -ReactDOM.render(, document.getElementById('root')); -``` - -[Try it on CodePen.](https://codepen.io/ericnakagawa/pen/kkAzPO?editors=0010) - -### Form Events - -Event names: +ReactDOM.render( + , + document.getElementById('root') +); ``` -onChange onInput onSubmit -``` + +[Try it on CodePen.](https://codepen.io/gaearon/pen/rrbkWz?editors=0010) diff --git a/docs/docs/reference-events.md b/docs/docs/reference-events.md index b68e65c84bfc..6e275db9b583 100644 --- a/docs/docs/reference-events.md +++ b/docs/docs/reference-events.md @@ -35,7 +35,7 @@ string type > > As of v0.14, returning `false` from an event handler will no longer stop event propagation. Instead, `e.stopPropagation()` or `e.preventDefault()` should be triggered manually, as appropriate. -### Event pooling +### Event Pooling The `SyntheticEvent` is pooled. This means that the `SyntheticEvent` object will be reused and all properties will be nullified after the event callback has been invoked. This is for performance reasons. From 59c94648c485b60035e150fc5e79232c4d25960f Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Wed, 26 Oct 2016 16:56:46 -0700 Subject: [PATCH 159/271] PureComponent in Fiber Passes existing PureComponent tests --- .../shared/fiber/ReactFiberClassComponent.js | 47 ++++++++++++++----- 1 file changed, 35 insertions(+), 12 deletions(-) diff --git a/src/renderers/shared/fiber/ReactFiberClassComponent.js b/src/renderers/shared/fiber/ReactFiberClassComponent.js index 4a8c27308b04..2211dead1876 100644 --- a/src/renderers/shared/fiber/ReactFiberClassComponent.js +++ b/src/renderers/shared/fiber/ReactFiberClassComponent.js @@ -25,6 +25,7 @@ var { } = require('ReactFiberUpdateQueue'); var { isMounted } = require('ReactFiberTreeReflection'); var ReactInstanceMap = require('ReactInstanceMap'); +var shallowEqual = require('shallowEqual'); module.exports = function(scheduleUpdate : (fiber: Fiber, priorityLevel : PriorityLevel) => void) { @@ -73,6 +74,28 @@ module.exports = function(scheduleUpdate : (fiber: Fiber, priorityLevel : Priori }, }; + function checkShouldComponentUpdate(workInProgress, oldProps, newProps, newState) { + const updateQueue = workInProgress.updateQueue; + if (oldProps === null || (updateQueue && updateQueue.isForced)) { + return true; + } + + const instance = workInProgress.stateNode; + if (typeof instance.shouldComponentUpdate === 'function') { + return instance.shouldComponentUpdate(newProps, newState); + } + + const type = workInProgress.type; + if (type.prototype && type.prototype.isPureReactComponent) { + return ( + !shallowEqual(oldProps, newProps) || + !shallowEqual(instance.state, newState) + ); + } + + return true; + } + function adoptClassInstance(workInProgress : Fiber, instance : any) : void { instance.updater = updater; workInProgress.stateNode = instance; @@ -116,7 +139,6 @@ module.exports = function(scheduleUpdate : (fiber: Fiber, priorityLevel : Priori // Called on a preexisting class instance. Returns false if a resumed render // could be reused. function resumeMountClassInstance(workInProgress : Fiber) : boolean { - const instance = workInProgress.stateNode; let newState = workInProgress.memoizedState; let newProps = workInProgress.pendingProps; if (!newProps) { @@ -132,13 +154,12 @@ module.exports = function(scheduleUpdate : (fiber: Fiber, priorityLevel : Priori // componentWillMount and before this componentWillMount? Probably // unsupported anyway. - const updateQueue = workInProgress.updateQueue; - - // If this completed, we might be able to just reuse this instance. - if (typeof instance.shouldComponentUpdate === 'function' && - !(updateQueue && updateQueue.isForced) && - workInProgress.memoizedProps !== null && - !instance.shouldComponentUpdate(newProps, newState)) { + if (!checkShouldComponentUpdate( + workInProgress, + workInProgress.memoizedProps, + newProps, + newState + )) { return false; } @@ -196,10 +217,12 @@ module.exports = function(scheduleUpdate : (fiber: Fiber, priorityLevel : Priori newState = previousState; } - if (typeof instance.shouldComponentUpdate === 'function' && - !(updateQueue && updateQueue.isForced) && - oldProps !== null && - !instance.shouldComponentUpdate(newProps, newState)) { + if (!checkShouldComponentUpdate( + workInProgress, + oldProps, + newProps, + newState + )) { // TODO: Should this get the new props/state updated regardless? return false; } From e3688d1fa3a69a2a893b79759eb759ac9a51a8a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E6=99=93=E5=8B=87?= Date: Thu, 27 Oct 2016 17:26:31 +0800 Subject: [PATCH 160/271] Update forms.md (#8121) --- docs/docs/forms.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/docs/forms.md b/docs/docs/forms.md index b9d8c0c658eb..df1c099745e2 100644 --- a/docs/docs/forms.md +++ b/docs/docs/forms.md @@ -98,7 +98,7 @@ Controlled components also let us reset inputs to arbitrary values by setting th ### Potential Issues With Checkboxes and Radio Buttons -Be aware that, in an attempt to normalize change handling for checkbox and radio inputs, React listens to a `click` browser event to implement the `onChange` event. +Be aware that, in an attempt to normalize change handling for checkboxes and radio inputs, React listens to a `click` browser event to implement the `onChange` event. For the most part this behaves as expected, except when calling `preventDefault` in a `change` handler. `preventDefault` stops the browser from visually updating the input, even if `checked` gets toggled. This can be worked around either by removing the call to `preventDefault`, or putting the toggle of `checked` in a `setTimeout`. From 6eebed0535a5e28effb7200783cede7a4bdab7ec Mon Sep 17 00:00:00 2001 From: Josh Perez Date: Thu, 27 Oct 2016 02:31:10 -0700 Subject: [PATCH 161/271] Shares debugID information across modules (#8097) Prior to this, React was using a nextDebugID variable that was locally scoped to both `instantiateReactComponent` and `ReactShallowRenderer`. This caused problems when the debugIDs would collide, the `itemMap` in `ReactComponentTreeHook` would be overwritten and tests would fail with the message "Expected onBeforeMountComponent() parent and onSetChildren() to be consistent". This change shares the debugID with both modules thus preventing any collisions in the future. --- .../reconciler/instantiateReactComponent.js | 5 ++--- src/shared/utils/getNextDebugID.js | 21 +++++++++++++++++++ src/test/ReactShallowRenderer.js | 7 +++---- 3 files changed, 26 insertions(+), 7 deletions(-) create mode 100644 src/shared/utils/getNextDebugID.js diff --git a/src/renderers/shared/stack/reconciler/instantiateReactComponent.js b/src/renderers/shared/stack/reconciler/instantiateReactComponent.js index 6a8ed0a54ffb..202768d8bba1 100644 --- a/src/renderers/shared/stack/reconciler/instantiateReactComponent.js +++ b/src/renderers/shared/stack/reconciler/instantiateReactComponent.js @@ -15,6 +15,7 @@ var ReactCompositeComponent = require('ReactCompositeComponent'); var ReactEmptyComponent = require('ReactEmptyComponent'); var ReactHostComponent = require('ReactHostComponent'); +var getNextDebugID = require('getNextDebugID'); var invariant = require('invariant'); var warning = require('warning'); @@ -56,8 +57,6 @@ function isInternalComponentType(type) { ); } -var nextDebugID = 1; - /** * Given a ReactNode, create an instance that will actually be mounted. * @@ -125,7 +124,7 @@ function instantiateReactComponent(node, shouldHaveDebugID) { instance._mountImage = null; if (__DEV__) { - instance._debugID = shouldHaveDebugID ? nextDebugID++ : 0; + instance._debugID = shouldHaveDebugID ? getNextDebugID() : 0; } // Internal instances should fully constructed at this point, so they should diff --git a/src/shared/utils/getNextDebugID.js b/src/shared/utils/getNextDebugID.js new file mode 100644 index 000000000000..6e7ecb43b84a --- /dev/null +++ b/src/shared/utils/getNextDebugID.js @@ -0,0 +1,21 @@ +/** + * 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 getNextDebugID + * @flow + */ + +'use strict'; + +var nextDebugID = 1; + +function getNextDebugID(): number { + return nextDebugID++; +} + +module.exports = getNextDebugID; diff --git a/src/test/ReactShallowRenderer.js b/src/test/ReactShallowRenderer.js index 1517d49cff82..ee50d9d6efc6 100644 --- a/src/test/ReactShallowRenderer.js +++ b/src/test/ReactShallowRenderer.js @@ -20,17 +20,16 @@ var ReactReconciler = require('ReactReconciler'); var ReactUpdates = require('ReactUpdates'); var emptyObject = require('emptyObject'); +var getNextDebugID = require('getNextDebugID'); var invariant = require('invariant'); -var nextDebugID = 1; - class NoopInternalComponent { constructor(element) { this._renderedOutput = element; this._currentElement = element; if (__DEV__) { - this._debugID = nextDebugID++; + this._debugID = getNextDebugID(); } } mountComponent() {} @@ -50,7 +49,7 @@ class NoopInternalComponent { var ShallowComponentWrapper = function(element) { // TODO: Consolidate with instantiateReactComponent if (__DEV__) { - this._debugID = nextDebugID++; + this._debugID = getNextDebugID(); } this.construct(element); From dba8f258549c936657d86e599310e92e6afd7f66 Mon Sep 17 00:00:00 2001 From: Lewis Blackwood Date: Thu, 27 Oct 2016 13:01:04 +0100 Subject: [PATCH 162/271] Correct usage of formatName() function in docs (#8122) The code section above these changes defines a `formatName` function that expects a parameter `user`. The code section containing these changes incorrectly called `formatName(user.name)`. For those following along with CodePen, this section should correctly call `formatName(user)`. --- docs/docs/introducing-jsx.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/docs/introducing-jsx.md b/docs/docs/introducing-jsx.md index d05f1575edc9..56e7b98e8582 100644 --- a/docs/docs/introducing-jsx.md +++ b/docs/docs/introducing-jsx.md @@ -59,7 +59,7 @@ This means that you can use JSX inside of `if` statements and `for` loops, assig ```js{3,5} function getGreeting(user) { if (user) { - return

Hello, {formatName(user.name)}!

; + return

Hello, {formatName(user)}!

; } else { return

Hello, Stranger.

; } From 603b7194dc21c332f59e2c820e68ec7377f1caf1 Mon Sep 17 00:00:00 2001 From: Varun Bhuvanendran Date: Thu, 27 Oct 2016 17:31:51 +0530 Subject: [PATCH 163/271] added word break (#8120) --- docs/css/react.scss | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/css/react.scss b/docs/css/react.scss index 087407173674..3177f057e4c0 100644 --- a/docs/css/react.scss +++ b/docs/css/react.scss @@ -924,6 +924,7 @@ p a code { float: right; padding: 15px 20px; width: $columnWidth; + word-wrap: break-word; } .playgroundError { @@ -938,6 +939,7 @@ p a code { .MarkdownEditor .content { white-space: pre-wrap; + word-break: break-word; } .hll { From 6fec6507ed7d316551ad78975d5037f1138b6f9c Mon Sep 17 00:00:00 2001 From: Ivan Zotov Date: Thu, 27 Oct 2016 15:03:32 +0300 Subject: [PATCH 164/271] Fix lint errors (#8113) --- src/renderers/dom/fiber/ReactDOMFiber.js | 6 +++++- src/renderers/noop/ReactNoop.js | 11 +++++++++-- .../shared/fiber/ReactFiberBeginWork.js | 16 +++++++++++----- 3 files changed, 25 insertions(+), 8 deletions(-) diff --git a/src/renderers/dom/fiber/ReactDOMFiber.js b/src/renderers/dom/fiber/ReactDOMFiber.js index 375a443f3e9c..d12b1aca9d39 100644 --- a/src/renderers/dom/fiber/ReactDOMFiber.js +++ b/src/renderers/dom/fiber/ReactDOMFiber.js @@ -96,7 +96,11 @@ var DOMRenderer = ReactFiberReconciler({ parentInstance.appendChild(child); }, - insertBefore(parentInstance : Instance, child : Instance | TextInstance, beforeChild : Instance | TextInstance) : void { + insertBefore( + parentInstance : Instance, + child : Instance | TextInstance, + beforeChild : Instance | TextInstance + ) : void { parentInstance.insertBefore(child, beforeChild); }, diff --git a/src/renderers/noop/ReactNoop.js b/src/renderers/noop/ReactNoop.js index 3e299a3aa376..e6ec61cf4fa4 100644 --- a/src/renderers/noop/ReactNoop.js +++ b/src/renderers/noop/ReactNoop.js @@ -41,7 +41,10 @@ type TextInstance = { tag: 98, text: string }; var instanceCounter = 0; -function recursivelyAppendChildren(flatArray : Array, child : HostChildren) { +function recursivelyAppendChildren( + flatArray : Array, + child : HostChildren +) { if (!child) { return; } @@ -108,7 +111,11 @@ var NoopRenderer = ReactFiberReconciler({ parentInstance.children.push(child); }, - insertBefore(parentInstance : Instance, child : Instance | TextInstance, beforeChild : Instance | TextInstance) : void { + insertBefore( + parentInstance : Instance, + child : Instance | TextInstance, + beforeChild : Instance | TextInstance + ) : void { const index = parentInstance.children.indexOf(child); if (index !== -1) { parentInstance.children.splice(index, 1); diff --git a/src/renderers/shared/fiber/ReactFiberBeginWork.js b/src/renderers/shared/fiber/ReactFiberBeginWork.js index 7112101d3083..bcc8958e5422 100644 --- a/src/renderers/shared/fiber/ReactFiberBeginWork.js +++ b/src/renderers/shared/fiber/ReactFiberBeginWork.js @@ -46,7 +46,10 @@ var { } = require('ReactTypeOfSideEffect'); var ReactFiberClassComponent = require('ReactFiberClassComponent'); -module.exports = function(config : HostConfig, scheduleUpdate : (fiber: Fiber, priorityLevel : PriorityLevel) => void) { +module.exports = function( + config : HostConfig, + scheduleUpdate : (fiber: Fiber, priorityLevel : PriorityLevel) => void +) { const { adoptClassInstance, @@ -152,11 +155,13 @@ module.exports = function(config : HostConfig, s } } + var nextChildren; + if (__DEV__) { ReactCurrentOwner.current = workInProgress; - var nextChildren = fn(props); + nextChildren = fn(props); } else { - var nextChildren = fn(props); + nextChildren = fn(props); } reconcileChildren(current, workInProgress, nextChildren); return workInProgress.child; @@ -244,12 +249,13 @@ module.exports = function(config : HostConfig, s } var fn = workInProgress.type; var props = workInProgress.pendingProps; + var value; if (__DEV__) { ReactCurrentOwner.current = workInProgress; - var value = fn(props); + value = fn(props); } else { - var value = fn(props); + value = fn(props); } if (typeof value === 'object' && value && typeof value.render === 'function') { From a4b9abff425eee51f7456e2eaea7e2c0622ebb61 Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Thu, 27 Oct 2016 13:06:09 +0100 Subject: [PATCH 165/271] Consistent CodePen links in docs --- docs/docs/conditional-rendering.md | 6 +++--- docs/docs/forms.md | 8 ++++---- docs/docs/lists-and-keys.md | 10 +++++----- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/docs/docs/conditional-rendering.md b/docs/docs/conditional-rendering.md index 865f124abadf..cb120a894771 100644 --- a/docs/docs/conditional-rendering.md +++ b/docs/docs/conditional-rendering.md @@ -42,7 +42,7 @@ ReactDOM.render( ); ``` -[Try it out on CodePen.](https://codepen.io/gaearon/pen/ZpVxNq?editors=0011) +[Try it on CodePen.](https://codepen.io/gaearon/pen/ZpVxNq?editors=0011) This example renders a different greeting depending on the value of `isLoggedIn` prop. @@ -116,7 +116,7 @@ ReactDOM.render( ); ``` -[Try it out on CodePen.](https://codepen.io/gaearon/pen/QKzAgB?editors=0010) +[Try it on CodePen.](https://codepen.io/gaearon/pen/QKzAgB?editors=0010) While declaring a variable and using an `if` statement is a fine way to conditionally render a component, sometimes you might want to use a shorter syntax. There are a few ways to inline conditions in JSX, explained below. @@ -238,4 +238,4 @@ ReactDOM.render( ); ``` -[Try it out on CodePen.](https://codepen.io/gaearon/pen/Xjoqwm?editors=0010) +[Try it on CodePen.](https://codepen.io/gaearon/pen/Xjoqwm?editors=0010) diff --git a/docs/docs/forms.md b/docs/docs/forms.md index df1c099745e2..0d92ec6d2214 100644 --- a/docs/docs/forms.md +++ b/docs/docs/forms.md @@ -302,7 +302,7 @@ ReactDOM.render( ); ``` -[Try it out on CodePen.](https://codepen.io/gaearon/pen/JRVaYB?editors=0010) +[Try it on CodePen.](https://codepen.io/gaearon/pen/JRVaYB?editors=0010) ### Controlled Textarea @@ -346,7 +346,7 @@ ReactDOM.render( ); ``` -[Try it out on CodePen.](https://codepen.io/gaearon/pen/NRmLxN?editors=0010) +[Try it on CodePen.](https://codepen.io/gaearon/pen/NRmLxN?editors=0010) ### Controlled Select @@ -389,7 +389,7 @@ ReactDOM.render( ); ``` -[Try it out on CodePen.](https://codepen.io/gaearon/pen/qawrbr?editors=0010) +[Try it on CodePen.](https://codepen.io/gaearon/pen/qawrbr?editors=0010) ### Uncontrolled Radio Button @@ -456,7 +456,7 @@ ReactDOM.render( ); ``` -[Try it out on CodePen.](https://codepen.io/gaearon/pen/ozOPLJ?editors=0010) +[Try it on CodePen.](https://codepen.io/gaearon/pen/ozOPLJ?editors=0010) ### Uncontrolled Checkbox diff --git a/docs/docs/lists-and-keys.md b/docs/docs/lists-and-keys.md index 14a346f61778..c2b15270873d 100644 --- a/docs/docs/lists-and-keys.md +++ b/docs/docs/lists-and-keys.md @@ -42,7 +42,7 @@ ReactDOM.render( ); ``` -[Try it out on CodePen.](https://codepen.io/gaearon/pen/GjPyQr?editors=0011) +[Try it on CodePen.](https://codepen.io/gaearon/pen/GjPyQr?editors=0011) This code displays a bullet list of numbers between 1 and 5. @@ -94,7 +94,7 @@ ReactDOM.render( ); ``` -[Try it out on CodePen.](https://codepen.io/gaearon/pen/jrXYRR?editors=0011) +[Try it on CodePen.](https://codepen.io/gaearon/pen/jrXYRR?editors=0011) ## Keys @@ -200,7 +200,7 @@ ReactDOM.render( ); ``` -[Try it out on CodePen.](https://codepen.io/rthor/pen/QKzJKG?editors=0010) +[Try it on CodePen.](https://codepen.io/rthor/pen/QKzJKG?editors=0010) A good rule of thumb is that elements inside the `map()` call need keys. @@ -244,7 +244,7 @@ ReactDOM.render( ); ``` -[Try it out on CodePen.](https://codepen.io/gaearon/pen/NRZYGN?editors=0010) +[Try it on CodePen.](https://codepen.io/gaearon/pen/NRZYGN?editors=0010) Keys serve as a hint to React but they don't get passed to your components. If you need the same value in your component, pass it explicitly as a prop with a different name: @@ -294,6 +294,6 @@ function NumberList(props) { } ``` -[Try it out on CodePen.](https://codepen.io/gaearon/pen/BLvYrB?editors=0010) +[Try it on CodePen.](https://codepen.io/gaearon/pen/BLvYrB?editors=0010) Sometimes this results in clearer code, but this style can also be abused. Like in JavaScript, it is up to you to decide whether it is worth extracting a variable for readability. Keep in mind that if the `map()` body is too nested, it might be a good time to [extract a component](/react/docs/components-and-props.html#extracting-components). From 1e126c29dd03073c82883ee666fd0a2c3172cdbd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20M=C3=B6ller?= Date: Mon, 24 Oct 2016 09:07:10 +0200 Subject: [PATCH 166/271] Fix: Remove unneeded else branches from documentation examples --- docs/docs/conditional-rendering.md | 3 +-- docs/docs/introducing-jsx.md | 3 +-- docs/docs/lifting-state-up.md | 3 +-- 3 files changed, 3 insertions(+), 6 deletions(-) diff --git a/docs/docs/conditional-rendering.md b/docs/docs/conditional-rendering.md index cb120a894771..c73720629563 100644 --- a/docs/docs/conditional-rendering.md +++ b/docs/docs/conditional-rendering.md @@ -30,9 +30,8 @@ function Greeting(props) { const isLoggedIn = props.isLoggedIn; if (isLoggedIn) { return ; - } else { - return ; } + return ; } ReactDOM.render( diff --git a/docs/docs/introducing-jsx.md b/docs/docs/introducing-jsx.md index 56e7b98e8582..75b9a5622d4a 100644 --- a/docs/docs/introducing-jsx.md +++ b/docs/docs/introducing-jsx.md @@ -60,9 +60,8 @@ This means that you can use JSX inside of `if` statements and `for` loops, assig function getGreeting(user) { if (user) { return

Hello, {formatName(user)}!

; - } else { - return

Hello, Stranger.

; } + return

Hello, Stranger.

; } ``` diff --git a/docs/docs/lifting-state-up.md b/docs/docs/lifting-state-up.md index 912ee89cdbc9..6b75c4ce7fae 100644 --- a/docs/docs/lifting-state-up.md +++ b/docs/docs/lifting-state-up.md @@ -16,9 +16,8 @@ We will start with a component called `BoilingVerdict`. It accepts the `celsius` function BoilingVerdict(props) { if (props.celsius >= 100) { return

The water would boil.

; - } else { - return

The water would not boil.

; } + return

The water would not boil.

; } ``` From b3b13919c7f86fc42eeb20693294e031754c0d30 Mon Sep 17 00:00:00 2001 From: Ivan Zotov Date: Thu, 27 Oct 2016 15:54:18 +0300 Subject: [PATCH 167/271] Improve devtools image size for the tutorial (#8114) --- docs/img/tutorial/devtools.png | Bin 51403 -> 24215 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/docs/img/tutorial/devtools.png b/docs/img/tutorial/devtools.png index 7dcf3c6a9dbee0f94759f152a2703ff34f1e7afd..c0482c093ac386a970fb012f3efff7bf29d22011 100644 GIT binary patch literal 24215 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 G|Gxk-Tw_uI literal 51403 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 F{s(wPl(hf= From e1b140225a5881d831e3ac7e9128106a27039fec Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Thu, 27 Oct 2016 14:21:34 +0100 Subject: [PATCH 168/271] Clarify how transition props work (#8124) --- docs/docs/addons-animation.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/docs/addons-animation.md b/docs/docs/addons-animation.md index 9b69d9136eea..abae33397e5d 100644 --- a/docs/docs/addons-animation.md +++ b/docs/docs/addons-animation.md @@ -105,7 +105,9 @@ render() { + transitionAppearTimeout={500} + transitionEnter={false} + transitionLeave={false}>

Fading at Initial Mount

); @@ -130,6 +132,8 @@ At the initial mount, all children of the `ReactCSSTransitionGroup` will `appear > Note: > > The prop `transitionAppear` was added to `ReactCSSTransitionGroup` in version `0.13`. To maintain backwards compatibility, the default value is set to `false`. +> +> However, the default values of `transitionEnter` and `transitionLeave` are `true` so you must specify `transitionEnterTimeout` and `transitionLeaveTimeout` by default. If you don't need either enter or leave animations, pass `transitionEnter={false}` or `transitionLeave={false}`. ### Custom Classes From 2633a4284f40eb4eecace2f443177233e583a081 Mon Sep 17 00:00:00 2001 From: Sebastian Markbage Date: Tue, 25 Oct 2016 14:53:54 -0700 Subject: [PATCH 169/271] Move more files into stack reconciler CallbackQueue and Transaction are specific to the stack reconciler. --- src/renderers/shared/{utils => stack/reconciler}/CallbackQueue.js | 0 src/renderers/shared/{utils => stack/reconciler}/Transaction.js | 0 .../{utils => stack/reconciler}/__tests__/Transaction-test.js | 0 3 files changed, 0 insertions(+), 0 deletions(-) rename src/renderers/shared/{utils => stack/reconciler}/CallbackQueue.js (100%) rename src/renderers/shared/{utils => stack/reconciler}/Transaction.js (100%) rename src/renderers/shared/{utils => stack/reconciler}/__tests__/Transaction-test.js (100%) diff --git a/src/renderers/shared/utils/CallbackQueue.js b/src/renderers/shared/stack/reconciler/CallbackQueue.js similarity index 100% rename from src/renderers/shared/utils/CallbackQueue.js rename to src/renderers/shared/stack/reconciler/CallbackQueue.js diff --git a/src/renderers/shared/utils/Transaction.js b/src/renderers/shared/stack/reconciler/Transaction.js similarity index 100% rename from src/renderers/shared/utils/Transaction.js rename to src/renderers/shared/stack/reconciler/Transaction.js diff --git a/src/renderers/shared/utils/__tests__/Transaction-test.js b/src/renderers/shared/stack/reconciler/__tests__/Transaction-test.js similarity index 100% rename from src/renderers/shared/utils/__tests__/Transaction-test.js rename to src/renderers/shared/stack/reconciler/__tests__/Transaction-test.js From 346dcaacfa38f62593a47f10d1b9de100ffb2f1b Mon Sep 17 00:00:00 2001 From: Rick Beerendonk Date: Thu, 27 Oct 2016 13:25:14 -0500 Subject: [PATCH 170/271] Add React Remote Conf 2016. (#8094) Add video links to some conferences. --- docs/community/conferences.md | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/docs/community/conferences.md b/docs/community/conferences.md index 388b5fa6cf5c..39ac533c8a44 100644 --- a/docs/community/conferences.md +++ b/docs/community/conferences.md @@ -37,7 +37,7 @@ April 16 in Amsterdam, The Netherlands ### ReactEurope 2016 June 2 & 3 in Paris, France -[Website](http://www.react-europe.org/) - [Schedule](http://www.react-europe.org/#schedule) +[Website](http://www.react-europe.org/) - [Schedule](http://www.react-europe.org/#schedule) - [Videos](https://www.youtube.com/channel/UCorlLn2oZfgOJ-FUcF2eZ1A/playlists) ### ReactRally 2016 August 25-26 in Salt Lake City, UT @@ -47,10 +47,10 @@ August 25-26 in Salt Lake City, UT ### ReactNext 2016 September 15 in Tel Aviv, Israel -[Website](http://react-next.com/) - [Schedule](http://react-next.com/#schedule) +[Website](http://react-next.com/) - [Schedule](http://react-next.com/#schedule) - [Videos](https://www.youtube.com/channel/UC3BT8hh3yTTYxbLQy_wbk2w) ### ReactNL 2016 -October 13 in Amsterdam, The Netherlands +October 13 in Amsterdam, The Netherlands - [Schedule](http://reactnl.org/#program) [Website](http://reactnl.org/) @@ -58,3 +58,8 @@ October 13 in Amsterdam, The Netherlands October 26-28 in Bratislava, Slovakia [Website](https://reactiveconf.com/) + +### React Remote Conf 2016 +October 26-28 online + +[Website](https://allremoteconfs.com/react-2016) - [Schedule](https://allremoteconfs.com/react-2016#schedule) \ No newline at end of file From 0e23880d22cf105a7e34869af132d9647bc2a277 Mon Sep 17 00:00:00 2001 From: Eugene Date: Thu, 27 Oct 2016 22:25:43 +0400 Subject: [PATCH 171/271] Update reference-react-component.md (#8126) line 320: For example, this code ensures that the `color` prop is a string --- docs/docs/reference-react-component.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/docs/reference-react-component.md b/docs/docs/reference-react-component.md index 7e5d6d4a9055..fa8b5de573a9 100644 --- a/docs/docs/reference-react-component.md +++ b/docs/docs/reference-react-component.md @@ -325,7 +325,7 @@ class CustomButton extends React.Component { } CustomButton.propTypes = { - name: React.PropTypes.string + color: React.PropTypes.string }; ``` From 2f8d1689db30a769188f08b79480a1301b736d6f Mon Sep 17 00:00:00 2001 From: Sebastian Markbage Date: Thu, 27 Oct 2016 18:54:15 -0700 Subject: [PATCH 172/271] Refactor ResponderEventPlugin to not rely on _rootNodeID Instead of relying on IDs, we now use instances for everything so this should be reflected by the test. This still has a _rootNodeID to store the listeners which I will remove next. --- .../eventPlugins/ResponderEventPlugin.js | 4 +- .../__tests__/ResponderEventPlugin-test.js | 259 ++++++------------ 2 files changed, 85 insertions(+), 178 deletions(-) diff --git a/src/renderers/shared/shared/event/eventPlugins/ResponderEventPlugin.js b/src/renderers/shared/shared/event/eventPlugins/ResponderEventPlugin.js index 2f26f4f24c09..1973592d8a56 100644 --- a/src/renderers/shared/shared/event/eventPlugins/ResponderEventPlugin.js +++ b/src/renderers/shared/shared/event/eventPlugins/ResponderEventPlugin.js @@ -466,8 +466,8 @@ function noResponderTouches(nativeEvent) { var ResponderEventPlugin = { /* For unit testing only */ - _getResponderID: function() { - return responderInst ? responderInst._rootNodeID : null; + _getResponder: function() { + return responderInst; }, eventTypes: eventTypes, diff --git a/src/renderers/shared/shared/event/eventPlugins/__tests__/ResponderEventPlugin-test.js b/src/renderers/shared/shared/event/eventPlugins/__tests__/ResponderEventPlugin-test.js index 4c95a3e31600..557049d31cc2 100644 --- a/src/renderers/shared/shared/event/eventPlugins/__tests__/ResponderEventPlugin-test.js +++ b/src/renderers/shared/shared/event/eventPlugins/__tests__/ResponderEventPlugin-test.js @@ -80,7 +80,7 @@ var _touchConfig = function( changedTouchObjects ), topLevelType: topType, - targetInst: idToInstance[targetNodeHandle], + targetInst: getInstanceFromNode(targetNodeHandle), }; }; @@ -233,7 +233,7 @@ var registerTestHandlers = function(eventTestConfig, readableIDToID) { } return config.returnVal; }.bind(null, readableID, nodeConfig); - EventPluginHub.putListener(idToInstance[id], registrationName, handler); + EventPluginHub.putListener(getInstanceFromNode(id), registrationName, handler); } }; for (var eventName in eventTestConfig) { @@ -304,146 +304,53 @@ var run = function(config, hierarchyConfig, nativeEventConfig) { ); // +1 for extra ++ }; -var GRANDPARENT_ID = '.0'; -var PARENT_ID = '.0.0'; -var CHILD_ID = '.0.0.0'; -var CHILD_ID2 = '.0.0.1'; +var GRANDPARENT_HOST_NODE = { }; +var PARENT_HOST_NODE = { }; +var CHILD_HOST_NODE = { }; +var CHILD_HOST_NODE2 = { }; -var idToInstance = {}; -[GRANDPARENT_ID, PARENT_ID, CHILD_ID, CHILD_ID2].forEach(function(id) { - idToInstance[id] = {_rootNodeID: id}; -}); +var GRANDPARENT_INST = { _hostParent: null, _rootNodeID: '1', _hostNode: GRANDPARENT_HOST_NODE }; +var PARENT_INST = { _hostParent: GRANDPARENT_INST, _rootNodeID: '2', _hostNode: PARENT_HOST_NODE }; +var CHILD_INST = { _hostParent: PARENT_INST, _rootNodeID: '3', _hostNode: CHILD_HOST_NODE }; +var CHILD_INST2 = { _hostParent: PARENT_INST, _rootNodeID: '4', _hostNode: CHILD_HOST_NODE2 }; + +GRANDPARENT_HOST_NODE._reactInstance = GRANDPARENT_INST; +PARENT_HOST_NODE._reactInstance = PARENT_INST; +CHILD_HOST_NODE._reactInstance = CHILD_INST; +CHILD_HOST_NODE2._reactInstance = CHILD_INST2; var three = { - grandParent: GRANDPARENT_ID, - parent: PARENT_ID, - child: CHILD_ID, + grandParent: GRANDPARENT_HOST_NODE, + parent: PARENT_HOST_NODE, + child: CHILD_HOST_NODE, }; var siblings = { - parent: PARENT_ID, - childOne: CHILD_ID, - childTwo: CHILD_ID2, + parent: PARENT_HOST_NODE, + childOne: CHILD_HOST_NODE, + childTwo: CHILD_HOST_NODE2, }; -describe('ResponderEventPlugin', () => { - - // This test is written against React IDs such as 'root.a.b[10]' but we - // removed those. In order to make those tests still pass with minimum - // surgery, we are inlining the implementation of those IDs here as it - // is the only remaining call site. - - var SEPARATOR = '.'; - - function isBoundary(id, index) { - return id.charAt(index) === SEPARATOR || index === id.length; - } - - function isAncestorIDOf(ancestorID, descendantID) { - return ( - descendantID.indexOf(ancestorID) === 0 && - isBoundary(descendantID, ancestorID.length) - ); - } - - function getParentID(id) { - return id ? id.substr(0, id.lastIndexOf(SEPARATOR)) : ''; - } - - function getNextDescendantID(ancestorID, destinationID) { - if (ancestorID === destinationID) { - return ancestorID; - } - // Skip over the ancestor and the immediate separator. Traverse until we hit - // another separator or we reach the end of `destinationID`. - var start = ancestorID.length + SEPARATOR.length; - var i; - for (i = start; i < destinationID.length; i++) { - if (isBoundary(destinationID, i)) { - break; - } - } - return destinationID.substr(0, i); - } - - function getFirstCommonAncestorID(oneID, twoID) { - var minLength = Math.min(oneID.length, twoID.length); - if (minLength === 0) { - return ''; - } - var lastCommonMarkerIndex = 0; - // Use `<=` to traverse until the "EOL" of the shorter string. - for (var i = 0; i <= minLength; i++) { - if (isBoundary(oneID, i) && isBoundary(twoID, i)) { - lastCommonMarkerIndex = i; - } else if (oneID.charAt(i) !== twoID.charAt(i)) { - break; - } - } - return oneID.substr(0, lastCommonMarkerIndex); - } - - function traverseParentPath(start, stop, cb, arg, skipFirst, skipLast) { - var traverseUp = isAncestorIDOf(stop, start); - var traverse = traverseUp ? getParentID : getNextDescendantID; - for (var id = start; /* until break */; id = traverse(id, stop)) { - var ret; - if ((!skipFirst || id !== start) && (!skipLast || id !== stop)) { - ret = cb(id, traverseUp ? 'bubbled': 'captured', arg); - } - if (ret === false || id === stop) { - // Only break //after// visiting `stop`. - break; - } - } - } +function getInstanceFromNode(node) { + return node._reactInstance; +} - function traverseTwoPhase(targetID, cb, arg) { - traverseParentPath('', targetID, cb, arg, true, false); - traverseParentPath(targetID, '', cb, arg, false, true); - } +function getNodeFromInstance(inst) { + return inst._hostNode; +} - // -- end of the React ID implementation +describe('ResponderEventPlugin', () => { beforeEach(() => { jest.resetModuleRegistry(); - // Inline mock - const ReactTreeTraversal = require('ReactTreeTraversal'); - Object.assign(ReactTreeTraversal, { - isAncestor: function(a, b) { - return isAncestorIDOf(a._rootNodeID, b._rootNodeID); - }, - getLowestCommonAncestor: function(a, b) { - var commonID = getFirstCommonAncestorID(a._rootNodeID, b._rootNodeID); - return idToInstance[commonID] || null; - }, - getParentInstance: function(inst) { - var id = inst._rootNodeID; - var parentID = id.substr(0, id.lastIndexOf(SEPARATOR)); - return idToInstance[parentID] || null; - }, - traverseTwoPhase: function(target, fn, arg) { - traverseTwoPhase( - target._rootNodeID, - function(id, phase) { - fn(idToInstance[id], phase, arg); - } - ); - }, - }); - EventPluginHub = require('EventPluginHub'); EventPluginUtils = require('EventPluginUtils'); ResponderEventPlugin = require('ResponderEventPlugin'); EventPluginUtils.injection.injectComponentTree({ - getInstanceFromNode: function(id) { - return idToInstance[id]; - }, - getNodeFromInstance: function(inst) { - return inst._rootNodeID; - }, + getInstanceFromNode, + getNodeFromInstance, }); }); @@ -456,12 +363,12 @@ describe('ResponderEventPlugin', () => { config.startShouldSetResponder.bubbled.parent = {order: 4, returnVal: false}; config.startShouldSetResponder.bubbled.grandParent = {order: 5, returnVal: false}; run(config, three, startConfig(three.child, [three.child], [0])); - expect(ResponderEventPlugin._getResponderID()).toBe(null); + expect(ResponderEventPlugin._getResponder()).toBe(null); // Now no handlers should be called on `touchEnd`. config = oneEventLoopTestConfig(three); run(config, three, endConfig(three.child, [three.child], [0])); - expect(ResponderEventPlugin._getResponderID()).toBe(null); + expect(ResponderEventPlugin._getResponder()).toBe(null); }); @@ -477,13 +384,13 @@ describe('ResponderEventPlugin', () => { config.responderGrant.grandParent = {order: 1}; config.responderStart.grandParent = {order: 2}; run(config, three, startConfig(three.child, [three.child], [0])); - expect(ResponderEventPlugin._getResponderID()).toBe(three.grandParent); + expect(ResponderEventPlugin._getResponder()).toBe(getInstanceFromNode(three.grandParent)); config = oneEventLoopTestConfig(three); config.responderEnd.grandParent = {order: 0}; config.responderRelease.grandParent = {order: 1}; run(config, three, endConfig(three.child, [three.child], [0])); - expect(ResponderEventPlugin._getResponderID()).toBe(null); + expect(ResponderEventPlugin._getResponder()).toBe(null); }); it('should grant responder parent while capturing', () => { @@ -493,13 +400,13 @@ describe('ResponderEventPlugin', () => { config.responderGrant.parent = {order: 2}; config.responderStart.parent = {order: 3}; run(config, three, startConfig(three.child, [three.child], [0])); - expect(ResponderEventPlugin._getResponderID()).toBe(three.parent); + expect(ResponderEventPlugin._getResponder()).toBe(getInstanceFromNode(three.parent)); config = oneEventLoopTestConfig(three); config.responderEnd.parent = {order: 0}; config.responderRelease.parent = {order: 1}; run(config, three, endConfig(three.child, [three.child], [0])); - expect(ResponderEventPlugin._getResponderID()).toBe(null); + expect(ResponderEventPlugin._getResponder()).toBe(null); }); it('should grant responder child while capturing', () => { @@ -510,13 +417,13 @@ describe('ResponderEventPlugin', () => { config.responderGrant.child = {order: 3}; config.responderStart.child = {order: 4}; run(config, three, startConfig(three.child, [three.child], [0])); - expect(ResponderEventPlugin._getResponderID()).toBe(three.child); + expect(ResponderEventPlugin._getResponder()).toBe(getInstanceFromNode(three.child)); config = oneEventLoopTestConfig(three); config.responderEnd.child = {order: 0}; config.responderRelease.child = {order: 1}; run(config, three, endConfig(three.child, [three.child], [0])); - expect(ResponderEventPlugin._getResponderID()).toBe(null); + expect(ResponderEventPlugin._getResponder()).toBe(null); }); it('should grant responder child while bubbling', () => { @@ -528,13 +435,13 @@ describe('ResponderEventPlugin', () => { config.responderGrant.child = {order: 4}; config.responderStart.child = {order: 5}; run(config, three, startConfig(three.child, [three.child], [0])); - expect(ResponderEventPlugin._getResponderID()).toBe(three.child); + expect(ResponderEventPlugin._getResponder()).toBe(getInstanceFromNode(three.child)); config = oneEventLoopTestConfig(three); config.responderEnd.child = {order: 0}; config.responderRelease.child = {order: 1}; run(config, three, endConfig(three.child, [three.child], [0])); - expect(ResponderEventPlugin._getResponderID()).toBe(null); + expect(ResponderEventPlugin._getResponder()).toBe(null); }); it('should grant responder parent while bubbling', () => { @@ -547,13 +454,13 @@ describe('ResponderEventPlugin', () => { config.responderGrant.parent = {order: 5}; config.responderStart.parent = {order: 6}; run(config, three, startConfig(three.child, [three.child], [0])); - expect(ResponderEventPlugin._getResponderID()).toBe(three.parent); + expect(ResponderEventPlugin._getResponder()).toBe(getInstanceFromNode(three.parent)); config = oneEventLoopTestConfig(three); config.responderEnd.parent = {order: 0}; config.responderRelease.parent = {order: 1}; run(config, three, endConfig(three.child, [three.child], [0])); - expect(ResponderEventPlugin._getResponderID()).toBe(null); + expect(ResponderEventPlugin._getResponder()).toBe(null); }); it('should grant responder grandParent while bubbling', () => { @@ -567,13 +474,13 @@ describe('ResponderEventPlugin', () => { config.responderGrant.grandParent = {order: 6}; config.responderStart.grandParent = {order: 7}; run(config, three, startConfig(three.child, [three.child], [0])); - expect(ResponderEventPlugin._getResponderID()).toBe(three.grandParent); + expect(ResponderEventPlugin._getResponder()).toBe(getInstanceFromNode(three.grandParent)); config = oneEventLoopTestConfig(three); config.responderEnd.grandParent = {order: 0}; config.responderRelease.grandParent = {order: 1}; run(config, three, endConfig(three.child, [three.child], [0])); - expect(ResponderEventPlugin._getResponderID()).toBe(null); + expect(ResponderEventPlugin._getResponder()).toBe(null); }); @@ -599,13 +506,13 @@ describe('ResponderEventPlugin', () => { config.responderGrant.grandParent = {order: 1}; config.responderMove.grandParent = {order: 2}; run(config, three, moveConfig(three.child, [three.child], [0])); - expect(ResponderEventPlugin._getResponderID()).toBe(three.grandParent); + expect(ResponderEventPlugin._getResponder()).toBe(getInstanceFromNode(three.grandParent)); config = oneEventLoopTestConfig(three); config.responderEnd.grandParent = {order: 0}; config.responderRelease.grandParent = {order: 1}; run(config, three, endConfig(three.child, [three.child], [0])); - expect(ResponderEventPlugin._getResponderID()).toBe(null); + expect(ResponderEventPlugin._getResponder()).toBe(null); }); it('should grant responder parent while capturing move', () => { @@ -625,13 +532,13 @@ describe('ResponderEventPlugin', () => { config.responderGrant.parent = {order: 2}; config.responderMove.parent = {order: 3}; run(config, three, moveConfig(three.child, [three.child], [0])); - expect(ResponderEventPlugin._getResponderID()).toBe(three.parent); + expect(ResponderEventPlugin._getResponder()).toBe(getInstanceFromNode(three.parent)); config = oneEventLoopTestConfig(three); config.responderEnd.parent = {order: 0}; config.responderRelease.parent = {order: 1}; run(config, three, endConfig(three.child, [three.child], [0])); - expect(ResponderEventPlugin._getResponderID()).toBe(null); + expect(ResponderEventPlugin._getResponder()).toBe(null); }); it('should grant responder child while capturing move', () => { @@ -652,13 +559,13 @@ describe('ResponderEventPlugin', () => { config.responderGrant.child = {order: 3}; config.responderMove.child = {order: 4}; run(config, three, moveConfig(three.child, [three.child], [0])); - expect(ResponderEventPlugin._getResponderID()).toBe(three.child); + expect(ResponderEventPlugin._getResponder()).toBe(getInstanceFromNode(three.child)); config = oneEventLoopTestConfig(three); config.responderEnd.child = {order: 0}; config.responderRelease.child = {order: 1}; run(config, three, endConfig(three.child, [three.child], [0])); - expect(ResponderEventPlugin._getResponderID()).toBe(null); + expect(ResponderEventPlugin._getResponder()).toBe(null); }); it('should grant responder child while bubbling move', () => { @@ -680,13 +587,13 @@ describe('ResponderEventPlugin', () => { config.responderGrant.child = {order: 4}; config.responderMove.child = {order: 5}; run(config, three, moveConfig(three.child, [three.child], [0])); - expect(ResponderEventPlugin._getResponderID()).toBe(three.child); + expect(ResponderEventPlugin._getResponder()).toBe(getInstanceFromNode(three.child)); config = oneEventLoopTestConfig(three); config.responderEnd.child = {order: 0}; config.responderRelease.child = {order: 1}; run(config, three, endConfig(three.child, [three.child], [0])); - expect(ResponderEventPlugin._getResponderID()).toBe(null); + expect(ResponderEventPlugin._getResponder()).toBe(null); }); it('should grant responder parent while bubbling move', () => { @@ -709,13 +616,13 @@ describe('ResponderEventPlugin', () => { config.responderGrant.parent = {order: 5}; config.responderMove.parent = {order: 6}; run(config, three, moveConfig(three.child, [three.child], [0])); - expect(ResponderEventPlugin._getResponderID()).toBe(three.parent); + expect(ResponderEventPlugin._getResponder()).toBe(getInstanceFromNode(three.parent)); config = oneEventLoopTestConfig(three); config.responderEnd.parent = {order: 0}; config.responderRelease.parent = {order: 1}; run(config, three, endConfig(three.child, [three.child], [0])); - expect(ResponderEventPlugin._getResponderID()).toBe(null); + expect(ResponderEventPlugin._getResponder()).toBe(null); }); it('should grant responder grandParent while bubbling move', () => { @@ -739,13 +646,13 @@ describe('ResponderEventPlugin', () => { config.responderGrant.grandParent = {order: 6}; config.responderMove.grandParent = {order: 7}; run(config, three, moveConfig(three.child, [three.child], [0])); - expect(ResponderEventPlugin._getResponderID()).toBe(three.grandParent); + expect(ResponderEventPlugin._getResponder()).toBe(getInstanceFromNode(three.grandParent)); config = oneEventLoopTestConfig(three); config.responderEnd.grandParent = {order: 0}; config.responderRelease.grandParent = {order: 1}; run(config, three, endConfig(three.child, [three.child], [0])); - expect(ResponderEventPlugin._getResponderID()).toBe(null); + expect(ResponderEventPlugin._getResponder()).toBe(null); }); @@ -761,7 +668,7 @@ describe('ResponderEventPlugin', () => { config.responderGrant.parent = {order: 2}; config.responderStart.parent = {order: 3}; run(config, three, startConfig(three.child, [three.child], [0])); - expect(ResponderEventPlugin._getResponderID()).toBe(three.parent); + expect(ResponderEventPlugin._getResponder()).toBe(getInstanceFromNode(three.parent)); // While `parent` is still responder, we create new handlers that verify // the ordering of propagation, restarting the count at `0`. @@ -771,13 +678,13 @@ describe('ResponderEventPlugin', () => { config.startShouldSetResponder.bubbled.grandParent = {order: 1, returnVal: false}; config.responderStart.parent = {order: 2}; run(config, three, startConfig(three.child, [three.child], [0])); - expect(ResponderEventPlugin._getResponderID()).toBe(three.parent); + expect(ResponderEventPlugin._getResponder()).toBe(getInstanceFromNode(three.parent)); config = oneEventLoopTestConfig(three); config.responderEnd.parent = {order: 0}; config.responderRelease.parent = {order: 1}; run(config, three, endConfig(three.child, [three.child], [0])); - expect(ResponderEventPlugin._getResponderID()).toBe(null); + expect(ResponderEventPlugin._getResponder()).toBe(null); }); it('should bubble negotiation to first common ancestor of responder then transfer', () => { @@ -787,7 +694,7 @@ describe('ResponderEventPlugin', () => { config.responderGrant.parent = {order: 2}; config.responderStart.parent = {order: 3}; run(config, three, startConfig(three.child, [three.child], [0])); - expect(ResponderEventPlugin._getResponderID()).toBe(three.parent); + expect(ResponderEventPlugin._getResponder()).toBe(getInstanceFromNode(three.parent)); config = oneEventLoopTestConfig(three); @@ -798,19 +705,19 @@ describe('ResponderEventPlugin', () => { config.responderTerminate.parent = {order: 3}; config.responderStart.grandParent = {order: 4}; run(config, three, startConfig(three.child, [three.child, three.child], [1])); - expect(ResponderEventPlugin._getResponderID()).toBe(three.grandParent); + expect(ResponderEventPlugin._getResponder()).toBe(getInstanceFromNode(three.grandParent)); config = oneEventLoopTestConfig(three); config.responderEnd.grandParent = {order: 0}; // one remains\ /one ended \ run(config, three, endConfig(three.child, [three.child, three.child], [1])); - expect(ResponderEventPlugin._getResponderID()).toBe(three.grandParent); + expect(ResponderEventPlugin._getResponder()).toBe(getInstanceFromNode(three.grandParent)); config = oneEventLoopTestConfig(three); config.responderEnd.grandParent = {order: 0}; config.responderRelease.grandParent = {order: 1}; run(config, three, endConfig(three.child, [three.child], [0])); - expect(ResponderEventPlugin._getResponderID()).toBe(null); + expect(ResponderEventPlugin._getResponder()).toBe(null); }); /** @@ -826,7 +733,7 @@ describe('ResponderEventPlugin', () => { config.startShouldSetResponder.bubbled.grandParent = {order: 3, returnVal: false}; run(config, three, startConfig(three.parent, [three.parent], [0])); - expect(ResponderEventPlugin._getResponderID()).toBe(null); + expect(ResponderEventPlugin._getResponder()).toBe(null); config = oneEventLoopTestConfig(three); @@ -842,7 +749,7 @@ describe('ResponderEventPlugin', () => { config.responderStart.child = {order: 5}; // / Two active touches \ /one of them new\ run(config, three, startConfig(three.child, [three.parent, three.child], [1])); - expect(ResponderEventPlugin._getResponderID()).toBe(three.child); + expect(ResponderEventPlugin._getResponder()).toBe(getInstanceFromNode(three.child)); // Now we remove the original first touch, keeping the second touch that @@ -852,7 +759,7 @@ describe('ResponderEventPlugin', () => { config.responderEnd.child = {order: 0}; // / one ended\ /one remains \ run(config, three, endConfig(three.child, [three.parent, three.child], [0])); - expect(ResponderEventPlugin._getResponderID()).toBe(three.child); + expect(ResponderEventPlugin._getResponder()).toBe(getInstanceFromNode(three.child)); // Okay, now let's add back that first touch (nothing should change) and // then we'll try peeling back the touches in the opposite order to make @@ -868,7 +775,7 @@ describe('ResponderEventPlugin', () => { config.responderStart.child = {order: 4}; // / Two active touches \ /one of them new\ run(config, three, startConfig(three.parent, [three.child, three.parent], [1])); - expect(ResponderEventPlugin._getResponderID()).toBe(three.child); + expect(ResponderEventPlugin._getResponder()).toBe(getInstanceFromNode(three.child)); // Now, move that new touch that had no effect, and did not start within @@ -883,7 +790,7 @@ describe('ResponderEventPlugin', () => { config.responderMove.child = {order: 4}; // / Two active touches \ /one of them moved\ run(config, three, moveConfig(three.parent, [three.child, three.parent], [1])); - expect(ResponderEventPlugin._getResponderID()).toBe(three.child); + expect(ResponderEventPlugin._getResponder()).toBe(getInstanceFromNode(three.child)); config = oneEventLoopTestConfig(three); @@ -891,7 +798,7 @@ describe('ResponderEventPlugin', () => { config.responderRelease.child = {order: 1}; // /child end \ /parent remain\ run(config, three, endConfig(three.child, [three.child, three.parent], [0])); - expect(ResponderEventPlugin._getResponderID()).toBe(null); + expect(ResponderEventPlugin._getResponder()).toBe(null); }); @@ -909,7 +816,7 @@ describe('ResponderEventPlugin', () => { config.responderStart.childOne = {order: 4}; run(config, siblings, startConfig(siblings.childOne, [siblings.childOne], [0])); - expect(ResponderEventPlugin._getResponderID()).toBe(siblings.childOne); + expect(ResponderEventPlugin._getResponder()).toBe(getInstanceFromNode(siblings.childOne)); // If the touch target is the sibling item, the negotiation should only // propagate to first common ancestor of current responder and sibling (so @@ -922,7 +829,7 @@ describe('ResponderEventPlugin', () => { var touchConfig = startConfig(siblings.childTwo, [siblings.childOne, siblings.childTwo], [1]); run(config, siblings, touchConfig); - expect(ResponderEventPlugin._getResponderID()).toBe(siblings.childOne); + expect(ResponderEventPlugin._getResponder()).toBe(getInstanceFromNode(siblings.childOne)); // move childOne @@ -931,7 +838,7 @@ describe('ResponderEventPlugin', () => { config.moveShouldSetResponder.bubbled.parent = {order: 1, returnVal: false}; config.responderMove.childOne = {order: 2}; run(config, siblings, moveConfig(siblings.childOne, [siblings.childOne, siblings.childTwo], [0])); - expect(ResponderEventPlugin._getResponderID()).toBe(siblings.childOne); + expect(ResponderEventPlugin._getResponder()).toBe(getInstanceFromNode(siblings.childOne)); // move childTwo: Only negotiates to `parent`. config = oneEventLoopTestConfig(siblings); @@ -939,7 +846,7 @@ describe('ResponderEventPlugin', () => { config.moveShouldSetResponder.bubbled.parent = {order: 1, returnVal: false}; config.responderMove.childOne = {order: 2}; run(config, siblings, moveConfig(siblings.childTwo, [siblings.childOne, siblings.childTwo], [1])); - expect(ResponderEventPlugin._getResponderID()).toBe(siblings.childOne); + expect(ResponderEventPlugin._getResponder()).toBe(getInstanceFromNode(siblings.childOne)); }); @@ -955,7 +862,7 @@ describe('ResponderEventPlugin', () => { config.responderStart.child = {order: 5}; run(config, three, startConfig(three.child, [three.child], [0])); - expect(ResponderEventPlugin._getResponderID()).toBe(three.child); + expect(ResponderEventPlugin._getResponder()).toBe(getInstanceFromNode(three.child)); // Suppose parent wants to become responder on move, and is rejected config = oneEventLoopTestConfig(three); @@ -971,7 +878,7 @@ describe('ResponderEventPlugin', () => { var touchConfig = moveConfig(three.child, [three.child], [0]); run(config, three, touchConfig); - expect(ResponderEventPlugin._getResponderID()).toBe(three.child); + expect(ResponderEventPlugin._getResponder()).toBe(getInstanceFromNode(three.child)); config = oneEventLoopTestConfig(three); config.startShouldSetResponder.captured.grandParent = {order: 0, returnVal: false}; @@ -986,7 +893,7 @@ describe('ResponderEventPlugin', () => { touchConfig = startConfig(three.child, [three.child, three.child], [1]); run(config, three, touchConfig); - expect(ResponderEventPlugin._getResponderID()).toBe(three.child); + expect(ResponderEventPlugin._getResponder()).toBe(getInstanceFromNode(three.child)); }); @@ -1002,7 +909,7 @@ describe('ResponderEventPlugin', () => { config.responderStart.child = {order: 5}; run(config, three, startConfig(three.child, [three.child], [0])); - expect(ResponderEventPlugin._getResponderID()).toBe(three.child); + expect(ResponderEventPlugin._getResponder()).toBe(getInstanceFromNode(three.child)); // If the touch target is the sibling item, the negotiation should only // propagate to first common ancestor of current responder and sibling (so @@ -1017,10 +924,10 @@ describe('ResponderEventPlugin', () => { run(config, three, { topLevelType: 'topScroll', - targetInst: idToInstance[three.parent], + targetInst: getInstanceFromNode(three.parent), nativeEvent: {}, }); - expect(ResponderEventPlugin._getResponderID()).toBe(three.child); + expect(ResponderEventPlugin._getResponder()).toBe(getInstanceFromNode(three.child)); // Now lets let the scroll take control this time. @@ -1034,10 +941,10 @@ describe('ResponderEventPlugin', () => { run(config, three, { topLevelType: 'topScroll', - targetInst: idToInstance[three.parent], + targetInst: getInstanceFromNode(three.parent), nativeEvent: {}, }); - expect(ResponderEventPlugin._getResponderID()).toBe(three.parent); + expect(ResponderEventPlugin._getResponder()).toBe(getInstanceFromNode(three.parent)); }); @@ -1053,7 +960,7 @@ describe('ResponderEventPlugin', () => { config.responderStart.child = {order: 5}; run(config, three, startConfig(three.child, [three.child], [0])); - expect(ResponderEventPlugin._getResponderID()).toBe(three.child); + expect(ResponderEventPlugin._getResponder()).toBe(getInstanceFromNode(three.child)); config = oneEventLoopTestConfig(three); config.responderEnd.child = {order: 0}; @@ -1066,6 +973,6 @@ describe('ResponderEventPlugin', () => { [0] ); run(config, three, nativeEvent); - expect(ResponderEventPlugin._getResponderID()).toBe(null); + expect(ResponderEventPlugin._getResponder()).toBe(null); }); }); From ea47782aaac8b405c21f0192b7a23b9820e5ace7 Mon Sep 17 00:00:00 2001 From: Sebastian Markbage Date: Thu, 27 Oct 2016 18:57:30 -0700 Subject: [PATCH 173/271] Create ReactGenericBatching injection wrapper around BatchedUpdates This moves calls that don't know if they're in a Fiber or Stack context to use ReactGenericBatching.batchedUpdates. The corresponding one will be injected from either the stack reconciler and/or the fiber reconciler if they're loaded at the same time. This lets them share the event system when they're both used at once. This can also be useful for libraries that call unstable_batchedUpdates today but don't know which renderer to use. --- .../dom/shared/ReactEventListener.js | 4 +- .../shared/eventPlugins/ChangeEventPlugin.js | 4 +- .../stack/client/ReactDOMStackInjection.js | 5 ++ .../native/ReactNativeEventEmitter.js | 4 +- .../native/ReactNativeStackInjection.js | 5 ++ .../shared/event/ReactGenericBatching.js | 53 +++++++++++++++++++ 6 files changed, 69 insertions(+), 6 deletions(-) create mode 100644 src/renderers/shared/shared/event/ReactGenericBatching.js diff --git a/src/renderers/dom/shared/ReactEventListener.js b/src/renderers/dom/shared/ReactEventListener.js index 8a329e9599c6..950cbe22630d 100644 --- a/src/renderers/dom/shared/ReactEventListener.js +++ b/src/renderers/dom/shared/ReactEventListener.js @@ -15,7 +15,7 @@ var EventListener = require('EventListener'); var ExecutionEnvironment = require('ExecutionEnvironment'); var PooledClass = require('PooledClass'); var ReactDOMComponentTree = require('ReactDOMComponentTree'); -var ReactUpdates = require('ReactUpdates'); +var ReactGenericBatching = require('ReactGenericBatching'); var getEventTarget = require('getEventTarget'); var getUnboundedScrollPosition = require('getUnboundedScrollPosition'); @@ -165,7 +165,7 @@ var ReactEventListener = { try { // Event queue being processed in the same cycle allows // `preventDefault`. - ReactUpdates.batchedUpdates(handleTopLevelImpl, bookKeeping); + ReactGenericBatching.batchedUpdates(handleTopLevelImpl, bookKeeping); } finally { TopLevelCallbackBookKeeping.release(bookKeeping); } diff --git a/src/renderers/dom/shared/eventPlugins/ChangeEventPlugin.js b/src/renderers/dom/shared/eventPlugins/ChangeEventPlugin.js index db8a78aa704d..06d153f077af 100644 --- a/src/renderers/dom/shared/eventPlugins/ChangeEventPlugin.js +++ b/src/renderers/dom/shared/eventPlugins/ChangeEventPlugin.js @@ -15,7 +15,7 @@ var EventPluginHub = require('EventPluginHub'); var EventPropagators = require('EventPropagators'); var ExecutionEnvironment = require('ExecutionEnvironment'); var ReactDOMComponentTree = require('ReactDOMComponentTree'); -var ReactUpdates = require('ReactUpdates'); +var ReactGenericBatching = require('ReactGenericBatching'); var SyntheticEvent = require('SyntheticEvent'); var inputValueTracking = require('inputValueTracking'); @@ -98,7 +98,7 @@ function manualDispatchChangeEvent(nativeEvent) { // components don't work properly in conjunction with event bubbling because // the component is rerendered and the value reverted before all the event // handlers can run. See https://github.com/facebook/react/issues/708. - ReactUpdates.batchedUpdates(runEventInBatch, event); + ReactGenericBatching.batchedUpdates(runEventInBatch, event); } function runEventInBatch(event) { diff --git a/src/renderers/dom/stack/client/ReactDOMStackInjection.js b/src/renderers/dom/stack/client/ReactDOMStackInjection.js index 7da0618673cf..6bf56262ac9d 100644 --- a/src/renderers/dom/stack/client/ReactDOMStackInjection.js +++ b/src/renderers/dom/stack/client/ReactDOMStackInjection.js @@ -19,6 +19,7 @@ var ReactDOMEmptyComponent = require('ReactDOMEmptyComponent'); var ReactDOMTextComponent = require('ReactDOMTextComponent'); var ReactDefaultBatchingStrategy = require('ReactDefaultBatchingStrategy'); var ReactEmptyComponent = require('ReactEmptyComponent'); +var ReactGenericBatching = require('ReactGenericBatching'); var ReactHostComponent = require('ReactHostComponent'); var ReactReconcileTransaction = require('ReactReconcileTransaction'); var ReactUpdates = require('ReactUpdates'); @@ -34,6 +35,10 @@ function inject() { } alreadyInjected = true; + ReactGenericBatching.injection.injectStackBatchedUpdates( + ReactUpdates.batchedUpdates + ); + ReactHostComponent.injection.injectGenericComponentClass( ReactDOMComponent ); diff --git a/src/renderers/native/ReactNativeEventEmitter.js b/src/renderers/native/ReactNativeEventEmitter.js index 0c4c3a7da1d3..c476bc166f37 100644 --- a/src/renderers/native/ReactNativeEventEmitter.js +++ b/src/renderers/native/ReactNativeEventEmitter.js @@ -16,7 +16,7 @@ var EventPluginRegistry = require('EventPluginRegistry'); var ReactEventEmitterMixin = require('ReactEventEmitterMixin'); var ReactNativeComponentTree = require('ReactNativeComponentTree'); var ReactNativeTagHandles = require('ReactNativeTagHandles'); -var ReactUpdates = require('ReactUpdates'); +var ReactGenericBatching = require('ReactGenericBatching'); var warning = require('warning'); @@ -123,7 +123,7 @@ var ReactNativeEventEmitter = { // any events. return; } - ReactUpdates.batchedUpdates(function() { + ReactGenericBatching.batchedUpdates(function() { ReactNativeEventEmitter.handleTopLevel( topLevelType, inst, diff --git a/src/renderers/native/ReactNativeStackInjection.js b/src/renderers/native/ReactNativeStackInjection.js index 01181a65f81d..83a5bcd4d4b6 100644 --- a/src/renderers/native/ReactNativeStackInjection.js +++ b/src/renderers/native/ReactNativeStackInjection.js @@ -23,6 +23,7 @@ var React = require('React'); var ReactComponentEnvironment = require('ReactComponentEnvironment'); var ReactDefaultBatchingStrategy = require('ReactDefaultBatchingStrategy'); var ReactEmptyComponent = require('ReactEmptyComponent'); +var ReactGenericBatching = require('ReactGenericBatching'); var ReactHostComponent = require('ReactHostComponent'); var ReactNativeComponentEnvironment = require('ReactNativeComponentEnvironment'); var ReactNativeTextComponent = require('ReactNativeTextComponent'); @@ -32,6 +33,10 @@ var ReactUpdates = require('ReactUpdates'); var invariant = require('invariant'); function inject() { + ReactGenericBatching.injection.injectStackBatchedUpdates( + ReactUpdates.batchedUpdates + ); + ReactUpdates.injection.injectReconcileTransaction( ReactNativeComponentEnvironment.ReactReconcileTransaction ); diff --git a/src/renderers/shared/shared/event/ReactGenericBatching.js b/src/renderers/shared/shared/event/ReactGenericBatching.js new file mode 100644 index 000000000000..6461ae973dbb --- /dev/null +++ b/src/renderers/shared/shared/event/ReactGenericBatching.js @@ -0,0 +1,53 @@ +/** + * 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 ReactGenericBatching + */ + +'use strict'; + +// Used as a way to call batchedUpdates when we don't know if we're in a Fiber +// or Stack context. Such as when we're dispatching events or if third party +// libraries need to call batchedUpdates. Eventually, this API will go away when +// everything is batched by default. We'll then have a similar API to opt-out of +// scheduled work and instead do synchronous work. + +// Defaults +var stackBatchedUpdates = function(fn, a, b, c, d, e) { + fn(a, b, c, d, e); +}; +var fiberPerformSynchronousWork = function(fn, bookkeeping) { + fn(bookkeeping); +}; + +function performFiberBatchedUpdates(fn, bookkeeping) { + // If we have Fiber loaded, we need to wrap this in a batching call so that + // Fiber can apply its default priority for this call. + fiberPerformSynchronousWork(fn, bookkeeping); +} +function batchedUpdates(fn, bookkeeping) { + // We first perform work with the stack batching strategy, by passing our + // indirection to it. + stackBatchedUpdates(performFiberBatchedUpdates, fn, bookkeeping); +} + +var ReactGenericBatchingInjection = { + injectStackBatchedUpdates: function(_batchedUpdates) { + stackBatchedUpdates = _batchedUpdates; + }, + injectFiberPerformSynchronousWork: function(_performSynchronousWork) { + fiberPerformSynchronousWork = _performSynchronousWork; + }, +}; + +var ReactGenericBatching = { + batchedUpdates, + injection: ReactGenericBatchingInjection, +}; + +module.exports = ReactGenericBatching; From 3c6abbfff7c0bf0e46da714e3e38fa04aca8bba2 Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Fri, 28 Oct 2016 10:29:14 +0100 Subject: [PATCH 174/271] [Fiber] Error Boundaries (#8095) --- .../shared/fiber/ReactFiberCommitWork.js | 109 +++- .../shared/fiber/ReactFiberErrorBoundary.js | 53 ++ .../shared/fiber/ReactFiberScheduler.js | 148 ++++- .../__tests__/ReactErrorBoundaries-test.js | 518 ++++++++++++------ 4 files changed, 631 insertions(+), 197 deletions(-) create mode 100644 src/renderers/shared/fiber/ReactFiberErrorBoundary.js diff --git a/src/renderers/shared/fiber/ReactFiberCommitWork.js b/src/renderers/shared/fiber/ReactFiberCommitWork.js index 2503e0d02b59..db844453c8ef 100644 --- a/src/renderers/shared/fiber/ReactFiberCommitWork.js +++ b/src/renderers/shared/fiber/ReactFiberCommitWork.js @@ -12,6 +12,7 @@ 'use strict'; +import type { TrappedError } from 'ReactFiberErrorBoundary'; import type { Fiber } from 'ReactFiber'; import type { FiberRoot } from 'ReactFiberRoot'; import type { HostConfig } from 'ReactFiberReconciler'; @@ -23,6 +24,7 @@ var { HostComponent, HostText, } = ReactTypeOfWork; +var { trapError } = require('ReactFiberErrorBoundary'); var { callCallbacks } = require('ReactFiberUpdateQueue'); var { @@ -155,7 +157,10 @@ module.exports = function(config : HostConfig) { } } - function commitNestedUnmounts(root : Fiber) { + function commitNestedUnmounts(root : Fiber): Array | null { + // Since errors are rare, we allocate this array on demand. + let trappedErrors = null; + // While we're inside a removed host node we don't want to call // removeChild on the inner nodes because they're removed by the top // call anyway. We also want to call componentWillUnmount on all @@ -163,39 +168,58 @@ module.exports = function(config : HostConfig) { // we do an inner loop while we're still inside the host node. let node : Fiber = root; while (true) { - commitUnmount(node); + const error = commitUnmount(node); + if (error) { + trappedErrors = trappedErrors || []; + trappedErrors.push(error); + } if (node.child) { // TODO: Coroutines need to visit the stateNode. node = node.child; continue; } if (node === root) { - return; + return trappedErrors; } while (!node.sibling) { if (!node.return || node.return === root) { - return; + return trappedErrors; } node = node.return; } node = node.sibling; } + return trappedErrors; } - function unmountHostComponents(parent, current) { + function unmountHostComponents(parent, current): Array | null { + // Since errors are rare, we allocate this array on demand. + let trappedErrors = null; + // We only have the top Fiber that was inserted but we need recurse down its // children to find all the terminal nodes. let node : Fiber = current; while (true) { if (node.tag === HostComponent || node.tag === HostText) { - commitNestedUnmounts(node); + const errors = commitNestedUnmounts(node); + if (errors) { + if (!trappedErrors) { + trappedErrors = errors; + } else { + trappedErrors.push.apply(trappedErrors, errors); + } + } // After all the children have unmounted, it is now safe to remove the // node from the tree. if (parent) { removeChild(parent, node.stateNode); } } else { - commitUnmount(node); + const error = commitUnmount(node); + if (error) { + trappedErrors = trappedErrors || []; + trappedErrors.push(error); + } if (node.child) { // TODO: Coroutines need to visit the stateNode. node = node.child; @@ -203,24 +227,24 @@ module.exports = function(config : HostConfig) { } } if (node === current) { - return; + return trappedErrors; } while (!node.sibling) { if (!node.return || node.return === current) { - return; + return trappedErrors; } node = node.return; } node = node.sibling; } + return trappedErrors; } - function commitDeletion(current : Fiber) : void { + function commitDeletion(current : Fiber) : Array | null { // Recursively delete all host nodes from the parent. - // TODO: Error handling. const parent = getHostParent(current); - - unmountHostComponents(parent, current); + // Detach refs and call componentWillUnmount() on the whole subtree. + const trappedErrors = unmountHostComponents(parent, current); // Cut off the return pointers to disconnect it from the tree. Ideally, we // should clear the child pointer of the parent alternate to let this @@ -233,21 +257,29 @@ module.exports = function(config : HostConfig) { current.alternate.child = null; current.alternate.return = null; } + + return trappedErrors; } - function commitUnmount(current : Fiber) : void { + function commitUnmount(current : Fiber) : TrappedError | null { switch (current.tag) { case ClassComponent: { detachRef(current); const instance = current.stateNode; if (typeof instance.componentWillUnmount === 'function') { - instance.componentWillUnmount(); + const error = tryCallComponentWillUnmount(instance); + if (error) { + return trapError(current, error); + } } - return; + return null; } case HostComponent: { detachRef(current); - return; + return null; + } + default: { + return null; } } } @@ -292,19 +324,20 @@ module.exports = function(config : HostConfig) { } } - function commitLifeCycles(current : ?Fiber, finishedWork : Fiber) : void { + function commitLifeCycles(current : ?Fiber, finishedWork : Fiber) : TrappedError | null { switch (finishedWork.tag) { case ClassComponent: { const instance = finishedWork.stateNode; + let error = null; if (!current) { if (typeof instance.componentDidMount === 'function') { - instance.componentDidMount(); + error = tryCallComponentDidMount(instance); } } else { if (typeof instance.componentDidUpdate === 'function') { const prevProps = current.memoizedProps; const prevState = current.memoizedState; - instance.componentDidUpdate(prevProps, prevState); + error = tryCallComponentDidUpdate(instance, prevProps, prevState); } } // Clear updates from current fiber. This must go before the callbacks @@ -320,7 +353,10 @@ module.exports = function(config : HostConfig) { callCallbacks(callbackList, instance); } attachRef(current, finishedWork, instance); - return; + if (error) { + return trapError(finishedWork, error); + } + return null; } case HostContainer: { const instance = finishedWork.stateNode; @@ -333,17 +369,44 @@ module.exports = function(config : HostConfig) { case HostComponent: { const instance : I = finishedWork.stateNode; attachRef(current, finishedWork, instance); - return; + return null; } case HostText: { // We have no life-cycles associated with text. - return; + return null; } default: throw new Error('This unit of work tag should not have side-effects.'); } } + function tryCallComponentDidMount(instance) { + try { + instance.componentDidMount(); + return null; + } catch (error) { + return error; + } + } + + function tryCallComponentDidUpdate(instance, prevProps, prevState) { + try { + instance.componentDidUpdate(prevProps, prevState); + return null; + } catch (error) { + return error; + } + } + + function tryCallComponentWillUnmount(instance) { + try { + instance.componentWillUnmount(); + return null; + } catch (error) { + return error; + } + } + return { commitInsertion, commitDeletion, diff --git a/src/renderers/shared/fiber/ReactFiberErrorBoundary.js b/src/renderers/shared/fiber/ReactFiberErrorBoundary.js new file mode 100644 index 000000000000..726888215596 --- /dev/null +++ b/src/renderers/shared/fiber/ReactFiberErrorBoundary.js @@ -0,0 +1,53 @@ +/** + * 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 ReactFiberErrorBoundary + * @flow + */ + +'use strict'; + +import type { Fiber } from 'ReactFiber'; + +var { + ClassComponent, +} = require('ReactTypeOfWork'); + +export type TrappedError = { + boundary: Fiber | null, + error: any, +}; + +function findClosestErrorBoundary(fiber : Fiber): Fiber | null { + let maybeErrorBoundary = fiber.return; + while (maybeErrorBoundary) { + if (maybeErrorBoundary.tag === ClassComponent) { + const instance = maybeErrorBoundary.stateNode; + if (typeof instance.unstable_handleError === 'function') { + return maybeErrorBoundary; + } + } + maybeErrorBoundary = maybeErrorBoundary.return; + } + return null; +} + +function trapError(fiber : Fiber, error : any) : TrappedError { + return { + boundary: findClosestErrorBoundary(fiber), + error, + }; +} + +function acknowledgeErrorInBoundary(boundary : Fiber, error : any) { + const instance = boundary.stateNode; + instance.unstable_handleError(error); +} + +exports.trapError = trapError; +exports.acknowledgeErrorInBoundary = acknowledgeErrorInBoundary; diff --git a/src/renderers/shared/fiber/ReactFiberScheduler.js b/src/renderers/shared/fiber/ReactFiberScheduler.js index 6ea78627da81..54ca2ef2cbb7 100644 --- a/src/renderers/shared/fiber/ReactFiberScheduler.js +++ b/src/renderers/shared/fiber/ReactFiberScheduler.js @@ -23,6 +23,7 @@ var ReactFiberCommitWork = require('ReactFiberCommitWork'); var ReactCurrentOwner = require('ReactCurrentOwner'); var { cloneFiber } = require('ReactFiber'); +var { trapError, acknowledgeErrorInBoundary } = require('ReactFiberErrorBoundary'); var { NoWork, @@ -109,9 +110,14 @@ module.exports = function(config : HostConfig) { return null; } - function commitAllWork(finishedWork : Fiber) { + function commitAllWork(finishedWork : Fiber, ignoreUnmountingErrors : boolean) { // Commit all the side-effects within a tree. - // TODO: Error handling. + + // Commit phase is meant to be atomic and non-interruptible. + // Any errors raised in it should be handled after it is over + // so that we don't end up in an inconsistent state due to user code. + // We'll keep track of all caught errors and handle them later. + let allTrappedErrors = null; // First, we'll perform all the host insertions, updates, deletions and // ref unmounts. @@ -129,7 +135,7 @@ module.exports = function(config : HostConfig) { commitInsertion(effectfulFiber); const current = effectfulFiber.alternate; commitWork(current, effectfulFiber); - // Clear the effect tag so that we know that this is inserted, before + // Clear the "placement" from effect tag so that we know that this is inserted, before // any life-cycles like componentDidMount gets called. effectfulFiber.effectTag = Update; break; @@ -140,7 +146,20 @@ module.exports = function(config : HostConfig) { break; } case Deletion: { - commitDeletion(effectfulFiber); + // Deletion might cause an error in componentWillUnmount(). + // We will continue nevertheless and handle those later on. + const trappedErrors = commitDeletion(effectfulFiber); + // There is a special case where we completely ignore errors. + // It happens when we already caught an error earlier, and the update + // is caused by an error boundary trying to render an error message. + // In this case, we want to blow away the tree without catching errors. + if (trappedErrors && !ignoreUnmountingErrors) { + if (!allTrappedErrors) { + allTrappedErrors = trappedErrors; + } else { + allTrappedErrors.push.apply(allTrappedErrors, trappedErrors); + } + } break; } } @@ -155,7 +174,11 @@ module.exports = function(config : HostConfig) { if (effectfulFiber.effectTag === Update || effectfulFiber.effectTag === PlacementAndUpdate) { const current = effectfulFiber.alternate; - commitLifeCycles(current, effectfulFiber); + const trappedError = commitLifeCycles(current, effectfulFiber); + if (trappedError) { + allTrappedErrors = allTrappedErrors || []; + allTrappedErrors.push(trappedError); + } } const next = effectfulFiber.nextEffect; // Ensure that we clean these up so that we don't accidentally keep them. @@ -173,7 +196,17 @@ module.exports = function(config : HostConfig) { if (finishedWork.effectTag !== NoEffect) { const current = finishedWork.alternate; commitWork(current, finishedWork); - commitLifeCycles(current, finishedWork); + const trappedError = commitLifeCycles(current, finishedWork); + if (trappedError) { + allTrappedErrors = allTrappedErrors || []; + allTrappedErrors.push(trappedError); + } + } + + // Now that the tree has been committed, we can handle errors. + if (allTrappedErrors) { + // TODO: handle multiple errors with distinct boundaries. + handleError(allTrappedErrors[0]); } } @@ -195,7 +228,7 @@ module.exports = function(config : HostConfig) { workInProgress.pendingWorkPriority = newPriority; } - function completeUnitOfWork(workInProgress : Fiber) : ?Fiber { + function completeUnitOfWork(workInProgress : Fiber, ignoreUnmountingErrors : boolean) : ?Fiber { while (true) { // The current, flushed, state of this fiber is the alternate. // Ideally nothing should rely on this, but relying on it here @@ -267,7 +300,7 @@ module.exports = function(config : HostConfig) { // "next" scheduled work since we've already scanned passed. That // also ensures that work scheduled during reconciliation gets deferred. // const hasMoreWork = workInProgress.pendingWorkPriority !== NoWork; - commitAllWork(workInProgress); + commitAllWork(workInProgress, ignoreUnmountingErrors); const nextWork = findNextUnitOfWork(); // if (!nextWork && hasMoreWork) { // TODO: This can happen when some deep work completes and we don't @@ -281,7 +314,7 @@ module.exports = function(config : HostConfig) { } } - function performUnitOfWork(workInProgress : Fiber) : ?Fiber { + function performUnitOfWork(workInProgress : Fiber, ignoreUnmountingErrors : boolean) : ?Fiber { // The current, flushed, state of this fiber is the alternate. // Ideally nothing should rely on this, but relying on it here // means that we don't need an additional field on the work in @@ -302,7 +335,7 @@ module.exports = function(config : HostConfig) { ReactFiberInstrumentation.debugTool.onWillCompleteWork(workInProgress); } // If this doesn't spawn new work, complete the current work. - next = completeUnitOfWork(workInProgress); + next = completeUnitOfWork(workInProgress, ignoreUnmountingErrors); if (__DEV__ && ReactFiberInstrumentation.debugTool) { ReactFiberInstrumentation.debugTool.onDidCompleteWork(workInProgress); } @@ -313,13 +346,13 @@ module.exports = function(config : HostConfig) { return next; } - function performDeferredWork(deadline) { + function performDeferredWorkUnsafe(deadline) { if (!nextUnitOfWork) { nextUnitOfWork = findNextUnitOfWork(); } while (nextUnitOfWork) { if (deadline.timeRemaining() > timeHeuristicForUnitOfWork) { - nextUnitOfWork = performUnitOfWork(nextUnitOfWork); + nextUnitOfWork = performUnitOfWork(nextUnitOfWork, false); if (!nextUnitOfWork) { // Find more work. We might have time to complete some more. nextUnitOfWork = findNextUnitOfWork(); @@ -331,6 +364,23 @@ module.exports = function(config : HostConfig) { } } + function performDeferredWork(deadline) { + try { + performDeferredWorkUnsafe(deadline); + } catch (error) { + const failedUnitOfWork = nextUnitOfWork; + // Reset because it points to the error boundary: + nextUnitOfWork = null; + if (!failedUnitOfWork) { + // We shouldn't end up here because nextUnitOfWork + // should always be set while work is being performed. + throw error; + } + const trappedError = trapError(failedUnitOfWork, error); + handleError(trappedError); + } + } + function scheduleDeferredWork(root : FiberRoot, priority : PriorityLevel) { // We must reset the current unit of work pointer so that we restart the // search from the root during the next tick, in case there is now higher @@ -362,12 +412,12 @@ module.exports = function(config : HostConfig) { } } - function performAnimationWork() { + function performAnimationWorkUnsafe() { // Always start from the root nextUnitOfWork = findNextUnitOfWork(); while (nextUnitOfWork && nextPriorityLevel !== NoWork) { - nextUnitOfWork = performUnitOfWork(nextUnitOfWork); + nextUnitOfWork = performUnitOfWork(nextUnitOfWork, false); if (!nextUnitOfWork) { // Keep searching for animation work until there's no more left nextUnitOfWork = findNextUnitOfWork(); @@ -380,6 +430,23 @@ module.exports = function(config : HostConfig) { } } + function performAnimationWork() { + try { + performAnimationWorkUnsafe(); + } catch (error) { + const failedUnitOfWork = nextUnitOfWork; + // Reset because it points to the error boundary: + nextUnitOfWork = null; + if (!failedUnitOfWork) { + // We shouldn't end up here because nextUnitOfWork + // should always be set while work is being performed. + throw error; + } + const trappedError = trapError(failedUnitOfWork, error); + handleError(trappedError); + } + } + function scheduleAnimationWork(root: FiberRoot, priorityLevel : PriorityLevel) { // Set the priority on the root, without deprioritizing if (root.current.pendingWorkPriority === NoWork || @@ -404,6 +471,59 @@ module.exports = function(config : HostConfig) { } } + function handleError(trappedError) { + const boundary = trappedError.boundary; + const error = trappedError.error; + if (!boundary) { + throw error; + } + + try { + // Give error boundary a chance to update its state + acknowledgeErrorInBoundary(boundary, error); + + // We will process an update caused by an error boundary with synchronous priority. + // This leaves us free to not keep track of whether a boundary has errored. + // If it errors again, we will just catch the error and synchronously propagate it higher. + + // First, traverse upwards and set pending synchronous priority on the whole tree. + let fiber = boundary; + while (fiber) { + fiber.pendingWorkPriority = SynchronousPriority; + if (fiber.alternate) { + fiber.alternate.pendingWorkPriority = SynchronousPriority; + } + if (!fiber.return) { + if (fiber.tag === HostContainer) { + // We found the root. + // Now go to the second phase and update it synchronously. + break; + } else { + throw new Error('Invalid root'); + } + } + fiber = fiber.return; + } + + if (!fiber) { + throw new Error('Could not find an error boundary root.'); + } + + // Find the work in progress tree. + const root : FiberRoot = (fiber.stateNode : any); + fiber = root.current.alternate; + + // Perform all the work synchronously. + while (fiber) { + fiber = performUnitOfWork(fiber, true); + } + } catch (nextError) { + // Propagate error to the next boundary or rethrow. + const nextTrappedError = trapError(boundary, nextError); + handleError(nextTrappedError); + } + } + function scheduleWork(root : FiberRoot) { if (defaultPriority === SynchronousPriority) { throw new Error('Not implemented yet'); diff --git a/src/renderers/shared/stack/reconciler/__tests__/ReactErrorBoundaries-test.js b/src/renderers/shared/stack/reconciler/__tests__/ReactErrorBoundaries-test.js index ac5e410cad91..78fa5fe6ce8c 100644 --- a/src/renderers/shared/stack/reconciler/__tests__/ReactErrorBoundaries-test.js +++ b/src/renderers/shared/stack/reconciler/__tests__/ReactErrorBoundaries-test.js @@ -11,6 +11,8 @@ 'use strict'; +var ReactDOMFeatureFlags = require('ReactDOMFeatureFlags'); + var React; var ReactDOM; @@ -33,6 +35,9 @@ describe('ReactErrorBoundaries', () => { var Normal; beforeEach(() => { + // TODO: Fiber isn't error resilient and one test can bring down them all. + jest.resetModuleRegistry(); + ReactDOM = require('ReactDOM'); React = require('React'); @@ -46,7 +51,7 @@ describe('ReactErrorBoundaries', () => { } render() { log.push('BrokenConstructor render'); - return
; + return
{this.props.children}
; } componentWillMount() { log.push('BrokenConstructor componentWillMount'); @@ -75,7 +80,7 @@ describe('ReactErrorBoundaries', () => { } render() { log.push('BrokenComponentWillMount render'); - return
; + return
{this.props.children}
; } componentWillMount() { log.push('BrokenComponentWillMount componentWillMount [!]'); @@ -105,7 +110,7 @@ describe('ReactErrorBoundaries', () => { } render() { log.push('BrokenComponentDidMount render'); - return
; + return
{this.props.children}
; } componentWillMount() { log.push('BrokenComponentDidMount componentWillMount'); @@ -135,7 +140,7 @@ describe('ReactErrorBoundaries', () => { } render() { log.push('BrokenComponentWillReceiveProps render'); - return
; + return
{this.props.children}
; } componentWillMount() { log.push('BrokenComponentWillReceiveProps componentWillMount'); @@ -165,7 +170,7 @@ describe('ReactErrorBoundaries', () => { } render() { log.push('BrokenComponentWillUpdate render'); - return
; + return
{this.props.children}
; } componentWillMount() { log.push('BrokenComponentWillUpdate componentWillMount'); @@ -195,7 +200,7 @@ describe('ReactErrorBoundaries', () => { } render() { log.push('BrokenComponentDidUpdate render'); - return
; + return
{this.props.children}
; } componentWillMount() { log.push('BrokenComponentDidUpdate componentWillMount'); @@ -225,7 +230,7 @@ describe('ReactErrorBoundaries', () => { } render() { log.push('BrokenComponentWillUnmount render'); - return
; + return
{this.props.children}
; } componentWillMount() { log.push('BrokenComponentWillUnmount componentWillMount'); @@ -459,52 +464,52 @@ describe('ReactErrorBoundaries', () => { }; }); - // Known limitation: error boundary only "sees" errors caused by updates - // flowing through it. This might be easier to fix in Fiber. - it('currently does not catch errors originating downstream', () => { - var fail = false; - class Stateful extends React.Component { - state = {shouldThrow: false}; - - render() { - if (fail) { - log.push('Stateful render [!]'); - throw new Error('Hello'); + if (ReactDOMFeatureFlags.useFiber) { + // This test implements a new feature in Fiber. + it('catches errors originating downstream', () => { + var fail = false; + class Stateful extends React.Component { + state = {shouldThrow: false}; + + render() { + if (fail) { + log.push('Stateful render [!]'); + throw new Error('Hello'); + } + return
{this.props.children}
; } - return
; } - } - var statefulInst; - var container = document.createElement('div'); - ReactDOM.render( - - statefulInst = inst} /> - , - container - ); - - log.length = 0; - expect(() => { - fail = true; - statefulInst.forceUpdate(); - }).toThrow(); - - expect(log).toEqual([ - 'Stateful render [!]', - // FIXME: uncomment when downstream errors get caught. - // Catch and render an error message - // 'ErrorBoundary unstable_handleError', - // 'ErrorBoundary render error', - // 'ErrorBoundary componentDidUpdate', - ]); + var statefulInst; + var container = document.createElement('div'); + ReactDOM.render( + + statefulInst = inst} /> + , + container + ); - log.length = 0; - ReactDOM.unmountComponentAtNode(container); - expect(log).toEqual([ - 'ErrorBoundary componentWillUnmount', - ]); - }); + log.length = 0; + expect(() => { + fail = true; + statefulInst.forceUpdate(); + }).not.toThrow(); + + expect(log).toEqual([ + 'Stateful render [!]', + 'ErrorBoundary unstable_handleError', + 'ErrorBoundary componentWillUpdate', + 'ErrorBoundary render error', + 'ErrorBoundary componentDidUpdate', + ]); + + log.length = 0; + ReactDOM.unmountComponentAtNode(container); + expect(log).toEqual([ + 'ErrorBoundary componentWillUnmount', + ]); + }); + } it('renders an error state if child throws in render', () => { var container = document.createElement('div'); @@ -526,6 +531,12 @@ describe('ReactErrorBoundaries', () => { 'BrokenRender render [!]', // Catch and render an error message 'ErrorBoundary unstable_handleError', + ...(ReactDOMFeatureFlags.useFiber ? [ + // The initial render was aborted, so + // Fiber retries from the root. + 'ErrorBoundary constructor', + 'ErrorBoundary componentWillMount', + ] : []), 'ErrorBoundary render error', 'ErrorBoundary componentDidMount', ]); @@ -553,6 +564,12 @@ describe('ReactErrorBoundaries', () => { 'BrokenConstructor constructor [!]', // Catch and render an error message 'ErrorBoundary unstable_handleError', + ...(ReactDOMFeatureFlags.useFiber ? [ + // The initial render was aborted, so + // Fiber retries from the root. + 'ErrorBoundary constructor', + 'ErrorBoundary componentWillMount', + ] : []), 'ErrorBoundary render error', 'ErrorBoundary componentDidMount', ]); @@ -581,6 +598,12 @@ describe('ReactErrorBoundaries', () => { 'BrokenComponentWillMount componentWillMount [!]', // Catch and render an error message 'ErrorBoundary unstable_handleError', + ...(ReactDOMFeatureFlags.useFiber ? [ + // The initial render was aborted, so + // Fiber retries from the root. + 'ErrorBoundary constructor', + 'ErrorBoundary componentWillMount', + ] : []), 'ErrorBoundary render error', 'ErrorBoundary componentDidMount', ]); @@ -615,6 +638,12 @@ describe('ReactErrorBoundaries', () => { 'BrokenRender render [!]', // Handle the error: 'ErrorBoundary unstable_handleError', + ...(ReactDOMFeatureFlags.useFiber ? [ + // The initial render was aborted, so + // Fiber retries from the root. + 'ErrorBoundary constructor', + 'ErrorBoundary componentWillMount', + ] : []), 'ErrorBoundary render error', // Mount the error message: 'ErrorMessage constructor', @@ -632,40 +661,64 @@ describe('ReactErrorBoundaries', () => { ]); }); - // Known limitation because componentDidMount() does not occur on the stack. - // We could either hardcode searching for parent boundary, or wait for Fiber. - it('currently does not catch errors in componentDidMount', () => { - var container = document.createElement('div'); - expect(() => { + if (ReactDOMFeatureFlags.useFiber) { + // This test implements a new feature in Fiber. + it('catches errors in componentDidMount', () => { + var container = document.createElement('div'); ReactDOM.render( + + + + , container ); - }).toThrow(); - expect(log).toEqual([ - 'ErrorBoundary constructor', - 'ErrorBoundary componentWillMount', - 'ErrorBoundary render success', - 'BrokenComponentDidMount constructor', - 'BrokenComponentDidMount componentWillMount', - 'BrokenComponentDidMount render', - 'BrokenComponentDidMount componentDidMount [!]', - // FIXME: uncomment when componentDidMount() gets caught. - // Catch and render an error message - // 'ErrorBoundary unstable_handleError', - // 'ErrorBoundary render error', - // 'ErrorBoundary componentDidMount', - ]); - - log.length = 0; - ReactDOM.unmountComponentAtNode(container); - expect(log).toEqual([ - 'ErrorBoundary componentWillUnmount', - 'BrokenComponentDidMount componentWillUnmount', - ]); - }); + expect(log).toEqual([ + 'ErrorBoundary constructor', + 'ErrorBoundary componentWillMount', + 'ErrorBoundary render success', + 'BrokenComponentWillUnmount constructor', + 'BrokenComponentWillUnmount componentWillMount', + 'BrokenComponentWillUnmount render', + 'Normal constructor', + 'Normal componentWillMount', + 'Normal render', + 'BrokenComponentDidMount constructor', + 'BrokenComponentDidMount componentWillMount', + 'BrokenComponentDidMount render', + 'LastChild constructor', + 'LastChild componentWillMount', + 'LastChild render', + // Start flushing didMount queue + 'Normal componentDidMount', + 'BrokenComponentWillUnmount componentDidMount', + 'BrokenComponentDidMount componentDidMount [!]', + // Continue despite the error + 'LastChild componentDidMount', + 'ErrorBoundary componentDidMount', + // Now we are ready to handle the error + 'ErrorBoundary unstable_handleError', + 'ErrorBoundary componentWillUpdate', + 'ErrorBoundary render error', + // Safely unmount every child + 'BrokenComponentWillUnmount componentWillUnmount [!]', + // Continue unmounting safely despite any errors + 'Normal componentWillUnmount', + 'BrokenComponentDidMount componentWillUnmount', + 'LastChild componentWillUnmount', + // The update has finished + 'ErrorBoundary componentDidUpdate', + ]); + + log.length = 0; + ReactDOM.unmountComponentAtNode(container); + expect(log).toEqual([ + 'ErrorBoundary componentWillUnmount', + ]); + }); + } it('propagates errors on retry on mounting', () => { var container = document.createElement('div'); @@ -688,15 +741,30 @@ describe('ReactErrorBoundaries', () => { 'BrokenRender constructor', 'BrokenRender componentWillMount', 'BrokenRender render [!]', - // The first error boundary catches the error - // However, it doesn't adjust its state so next render also fails + // The first error boundary catches the error. + // However, it doesn't adjust its state so next render will also fail. 'NoopErrorBoundary unstable_handleError', + ...(ReactDOMFeatureFlags.useFiber ? [ + // The initial render was aborted, so + // Fiber retries from the root. + 'ErrorBoundary constructor', + 'ErrorBoundary componentWillMount', + 'ErrorBoundary render success', + 'NoopErrorBoundary constructor', + 'NoopErrorBoundary componentWillMount', + ] : []), 'NoopErrorBoundary render', 'BrokenRender constructor', 'BrokenRender componentWillMount', 'BrokenRender render [!]', // This time, the error propagates to the higher boundary 'ErrorBoundary unstable_handleError', + ...(ReactDOMFeatureFlags.useFiber ? [ + // The initial render was aborted, so + // Fiber retries from the root. + 'ErrorBoundary constructor', + 'ErrorBoundary componentWillMount', + ] : []), // Render the error 'ErrorBoundary render error', 'ErrorBoundary componentDidMount', @@ -726,6 +794,12 @@ describe('ReactErrorBoundaries', () => { 'BrokenComponentWillMountErrorBoundary componentWillMount [!]', // The error propagates to the higher boundary 'ErrorBoundary unstable_handleError', + ...(ReactDOMFeatureFlags.useFiber ? [ + // The initial render was aborted, so + // Fiber retries from the root. + 'ErrorBoundary constructor', + 'ErrorBoundary componentWillMount', + ] : []), // Render the error 'ErrorBoundary render error', 'ErrorBoundary componentDidMount', @@ -762,9 +836,24 @@ describe('ReactErrorBoundaries', () => { // The first error boundary catches the error // It adjusts state but throws displaying the message 'BrokenRenderErrorBoundary unstable_handleError', + ...(ReactDOMFeatureFlags.useFiber ? [ + // The initial render was aborted, so + // Fiber retries from the root. + 'ErrorBoundary constructor', + 'ErrorBoundary componentWillMount', + 'ErrorBoundary render success', + 'BrokenRenderErrorBoundary constructor', + 'BrokenRenderErrorBoundary componentWillMount', + ] : []), 'BrokenRenderErrorBoundary render error [!]', // The error propagates to the higher boundary 'ErrorBoundary unstable_handleError', + ...(ReactDOMFeatureFlags.useFiber ? [ + // The initial render was aborted, so + // Fiber retries from the root. + 'ErrorBoundary constructor', + 'ErrorBoundary componentWillMount', + ] : []), // Render the error 'ErrorBoundary render error', 'ErrorBoundary componentDidMount', @@ -822,6 +911,12 @@ describe('ReactErrorBoundaries', () => { 'BrokenRender render [!]', // Error boundary catches the error 'ErrorBoundary unstable_handleError', + ...(ReactDOMFeatureFlags.useFiber ? [ + // The initial render was aborted, so + // Fiber retries from the root. + 'ErrorBoundary constructor', + 'ErrorBoundary componentWillMount', + ] : []), // Render the error message 'ErrorBoundary render error', 'ErrorBoundary componentDidMount', @@ -860,8 +955,16 @@ describe('ReactErrorBoundaries', () => { 'BrokenRender render [!]', // Handle error: 'ErrorBoundary unstable_handleError', - // Child ref wasn't (and won't be) set but there's no harm in clearing: - 'Child ref is set to null', + ...(ReactDOMFeatureFlags.useFiber ? [ + // The initial render was aborted, so + // Fiber retries from the root. + 'ErrorBoundary constructor', + 'ErrorBoundary componentWillMount', + ] : [ + // Stack reconciler resets ref on update, as it doesn't know ref was never set. + // This is unnecessary, and Fiber doesn't do it: + 'Child ref is set to null', + ]), 'ErrorBoundary render error', // Ref to error message should get set: 'Error message ref is set to [object HTMLDivElement]', @@ -932,10 +1035,20 @@ describe('ReactErrorBoundaries', () => { // BrokenConstructor will abort rendering: 'BrokenConstructor constructor [!]', 'ErrorBoundary unstable_handleError', - // Unmount the previously mounted components: - 'Normal componentWillUnmount', + ...(ReactDOMFeatureFlags.useFiber ? [ + // The initial render was aborted, so + // Fiber retries from the root. + 'ErrorBoundary componentWillReceiveProps', + 'ErrorBoundary componentWillUpdate', + // Fiber renders first, then unmounts in a batch: + 'ErrorBoundary render error', + 'Normal componentWillUnmount', + ] : [ + // Stack unmounts first, then renders: + 'Normal componentWillUnmount', + 'ErrorBoundary render error', + ]), // Normal2 does not get lifefycle because it was never mounted - 'ErrorBoundary render error', 'ErrorBoundary componentDidUpdate', ]); @@ -980,10 +1093,20 @@ describe('ReactErrorBoundaries', () => { 'BrokenComponentWillMount constructor', 'BrokenComponentWillMount componentWillMount [!]', 'ErrorBoundary unstable_handleError', - // Unmount the previously mounted components: - 'Normal componentWillUnmount', + ...(ReactDOMFeatureFlags.useFiber ? [ + // The initial render was aborted, so + // Fiber retries from the root. + 'ErrorBoundary componentWillReceiveProps', + 'ErrorBoundary componentWillUpdate', + // Fiber renders first, then unmounts in a batch: + 'ErrorBoundary render error', + 'Normal componentWillUnmount', + ] : [ + // Stack unmounts first, then renders: + 'Normal componentWillUnmount', + 'ErrorBoundary render error', + ]), // Normal2 does not get lifefycle because it was never mounted - 'ErrorBoundary render error', 'ErrorBoundary componentDidUpdate', ]); @@ -1023,11 +1146,21 @@ describe('ReactErrorBoundaries', () => { // BrokenComponentWillReceiveProps will abort rendering: 'BrokenComponentWillReceiveProps componentWillReceiveProps [!]', 'ErrorBoundary unstable_handleError', - // Unmount the previously mounted components: - 'Normal componentWillUnmount', - 'BrokenComponentWillReceiveProps componentWillUnmount', - // Render error: - 'ErrorBoundary render error', + ...(ReactDOMFeatureFlags.useFiber ? [ + // The initial render was aborted, so + // Fiber retries from the root. + 'ErrorBoundary componentWillReceiveProps', + 'ErrorBoundary componentWillUpdate', + // Fiber renders first, then unmounts in a batch: + 'ErrorBoundary render error', + 'Normal componentWillUnmount', + 'BrokenComponentWillReceiveProps componentWillUnmount', + ] : [ + // Stack unmounts first, then renders: + 'Normal componentWillUnmount', + 'BrokenComponentWillReceiveProps componentWillUnmount', + 'ErrorBoundary render error', + ]), 'ErrorBoundary componentDidUpdate', ]); @@ -1068,11 +1201,21 @@ describe('ReactErrorBoundaries', () => { 'BrokenComponentWillUpdate componentWillReceiveProps', 'BrokenComponentWillUpdate componentWillUpdate [!]', 'ErrorBoundary unstable_handleError', - // Unmount the previously mounted components: - 'Normal componentWillUnmount', - 'BrokenComponentWillUpdate componentWillUnmount', - // Render error: - 'ErrorBoundary render error', + ...(ReactDOMFeatureFlags.useFiber ? [ + // The initial render was aborted, so + // Fiber retries from the root. + 'ErrorBoundary componentWillReceiveProps', + 'ErrorBoundary componentWillUpdate', + // Fiber renders first, then unmounts in a batch: + 'ErrorBoundary render error', + 'Normal componentWillUnmount', + 'BrokenComponentWillUpdate componentWillUnmount', + ] : [ + // Stack unmounts first, then renders: + 'Normal componentWillUnmount', + 'BrokenComponentWillUpdate componentWillUnmount', + 'ErrorBoundary render error', + ]), 'ErrorBoundary componentDidUpdate', ]); @@ -1118,10 +1261,20 @@ describe('ReactErrorBoundaries', () => { 'BrokenRender componentWillMount', 'BrokenRender render [!]', 'ErrorBoundary unstable_handleError', - // Unmount the previously mounted components: - 'Normal componentWillUnmount', + ...(ReactDOMFeatureFlags.useFiber ? [ + // The initial render was aborted, so + // Fiber retries from the root. + 'ErrorBoundary componentWillReceiveProps', + 'ErrorBoundary componentWillUpdate', + // Fiber renders first, then unmounts in a batch: + 'ErrorBoundary render error', + 'Normal componentWillUnmount', + ] : [ + // Stack unmounts first, then renders: + 'Normal componentWillUnmount', + 'ErrorBoundary render error', + ]), // Normal2 does not get lifefycle because it was never mounted - 'ErrorBoundary render error', 'ErrorBoundary componentDidUpdate', ]); @@ -1177,9 +1330,19 @@ describe('ReactErrorBoundaries', () => { 'BrokenRender componentWillMount', 'BrokenRender render [!]', 'ErrorBoundary unstable_handleError', - // Unmount the previously mounted components: - 'Child1 ref is set to null', - 'ErrorBoundary render error', + ...(ReactDOMFeatureFlags.useFiber ? [ + // The initial render was aborted, so + // Fiber retries from the root. + 'ErrorBoundary componentWillReceiveProps', + 'ErrorBoundary componentWillUpdate', + // Fiber renders first, resets refs later + 'ErrorBoundary render error', + 'Child1 ref is set to null', + ] : [ + // Stack resets ref first, renders later + 'Child1 ref is set to null', + 'ErrorBoundary render error', + ]), 'Error message ref is set to [object HTMLDivElement]', // Child2 ref is never set because its mounting aborted 'ErrorBoundary componentDidUpdate', @@ -1193,48 +1356,49 @@ describe('ReactErrorBoundaries', () => { ]); }); - // Known limitation because componentDidUpdate() does not occur on the stack. - // We could either hardcode searching for parent boundary, or wait for Fiber. - it('currently does not catch errors in componentDidUpdate', () => { - var container = document.createElement('div'); - ReactDOM.render( - - - , - container - ); - - log.length = 0; - expect(() => { + if (ReactDOMFeatureFlags.useFiber) { + // This test implements a new feature in Fiber. + it('catches errors in componentDidUpdate', () => { + var container = document.createElement('div'); ReactDOM.render( , container ); - }).toThrow(); - expect(log).toEqual([ - 'ErrorBoundary componentWillReceiveProps', - 'ErrorBoundary componentWillUpdate', - 'ErrorBoundary render success', - 'BrokenComponentDidUpdate componentWillReceiveProps', - 'BrokenComponentDidUpdate componentWillUpdate', - 'BrokenComponentDidUpdate render', - 'BrokenComponentDidUpdate componentDidUpdate [!]', - // FIXME: uncomment when componentDidUpdate() gets caught. - // Catch and render an error message - // 'ErrorBoundary unstable_handleError', - // 'ErrorBoundary render error', - // 'ErrorBoundary componentDidUpdate', - ]); - log.length = 0; - ReactDOM.unmountComponentAtNode(container); - expect(log).toEqual([ - 'ErrorBoundary componentWillUnmount', - 'BrokenComponentDidUpdate componentWillUnmount', - ]); - }); + log.length = 0; + ReactDOM.render( + + + , + container + ); + expect(log).toEqual([ + 'ErrorBoundary componentWillReceiveProps', + 'ErrorBoundary componentWillUpdate', + 'ErrorBoundary render success', + 'BrokenComponentDidUpdate componentWillReceiveProps', + 'BrokenComponentDidUpdate componentWillUpdate', + 'BrokenComponentDidUpdate render', + // All lifecycles run + 'BrokenComponentDidUpdate componentDidUpdate [!]', + 'ErrorBoundary componentDidUpdate', + // Then, error is handled + 'ErrorBoundary unstable_handleError', + 'ErrorBoundary componentWillUpdate', + 'ErrorBoundary render error', + 'BrokenComponentDidUpdate componentWillUnmount', + 'ErrorBoundary componentDidUpdate', + ]); + + log.length = 0; + ReactDOM.unmountComponentAtNode(container); + expect(log).toEqual([ + 'ErrorBoundary componentWillUnmount', + ]); + }); + } it('recovers from componentWillUnmount errors on update', () => { var container = document.createElement('div'); @@ -1242,7 +1406,7 @@ describe('ReactErrorBoundaries', () => { - + , container ); @@ -1251,7 +1415,6 @@ describe('ReactErrorBoundaries', () => { ReactDOM.render( - , container ); @@ -1260,23 +1423,39 @@ describe('ReactErrorBoundaries', () => { 'ErrorBoundary componentWillReceiveProps', 'ErrorBoundary componentWillUpdate', 'ErrorBoundary render success', - // Update existing children: - 'BrokenComponentWillUnmount componentWillReceiveProps', - 'BrokenComponentWillUnmount componentWillUpdate', - 'BrokenComponentWillUnmount render', + // Update existing child: 'BrokenComponentWillUnmount componentWillReceiveProps', 'BrokenComponentWillUnmount componentWillUpdate', 'BrokenComponentWillUnmount render', // Unmounting throws: 'BrokenComponentWillUnmount componentWillUnmount [!]', - 'ErrorBoundary unstable_handleError', - // Attempt to unmount previous children: - 'BrokenComponentWillUnmount componentWillUnmount [!]', - 'BrokenComponentWillUnmount componentWillUnmount [!]', - // Render error: - 'ErrorBoundary render error', - 'ErrorBoundary componentDidUpdate', - // Children don't get componentDidUpdate() since update was aborted + ...(ReactDOMFeatureFlags.useFiber ? [ + // Fiber proceeds with lifecycles despite errors + 'Normal componentWillUnmount', + // The components have updated in this phase + 'BrokenComponentWillUnmount componentDidUpdate', + 'ErrorBoundary componentDidUpdate', + // Now that commit phase is done, Fiber handles errors + 'ErrorBoundary unstable_handleError', + // The initial render was aborted, so + // Fiber retries from the root. + 'ErrorBoundary componentWillUpdate', + // Render an error now (stack will do it later) + 'ErrorBoundary render error', + // Attempt to unmount previous child: + 'BrokenComponentWillUnmount componentWillUnmount [!]', + // Done + 'ErrorBoundary componentDidUpdate', + ] : [ + // Stack will handle error immediately + 'ErrorBoundary unstable_handleError', + // Attempt to unmount previous children: + 'BrokenComponentWillUnmount componentWillUnmount [!]', + 'Normal componentWillUnmount', + // Render an error now (Fiber will do it earlier) + 'ErrorBoundary render error', + 'ErrorBoundary componentDidUpdate', + ]), ]); log.length = 0; @@ -1321,13 +1500,32 @@ describe('ReactErrorBoundaries', () => { 'BrokenComponentWillUnmount render', // Unmounting throws: 'BrokenComponentWillUnmount componentWillUnmount [!]', - 'ErrorBoundary unstable_handleError', - // Attempt to unmount previous children: - 'Normal componentWillUnmount', - 'BrokenComponentWillUnmount componentWillUnmount [!]', - // Render error: - 'ErrorBoundary render error', - 'ErrorBoundary componentDidUpdate', + ...(ReactDOMFeatureFlags.useFiber ? [ + // Fiber proceeds with lifecycles despite errors + 'BrokenComponentWillUnmount componentDidUpdate', + 'Normal componentDidUpdate', + 'ErrorBoundary componentDidUpdate', + // Now that commit phase is done, Fiber handles errors + 'ErrorBoundary unstable_handleError', + // The initial render was aborted, so + // Fiber retries from the root. + 'ErrorBoundary componentWillUpdate', + // Render an error now (stack will do it later) + 'ErrorBoundary render error', + // Attempt to unmount previous child: + 'Normal componentWillUnmount', + 'BrokenComponentWillUnmount componentWillUnmount [!]', + // Done + 'ErrorBoundary componentDidUpdate', + ] : [ + 'ErrorBoundary unstable_handleError', + // Attempt to unmount previous children: + 'Normal componentWillUnmount', + 'BrokenComponentWillUnmount componentWillUnmount [!]', + // Stack calls lifecycles first, then renders. + 'ErrorBoundary render error', + 'ErrorBoundary componentDidUpdate', + ]), ]); log.length = 0; @@ -1474,7 +1672,7 @@ describe('ReactErrorBoundaries', () => { if (fail) { throw new Error('Hello'); } - return
; + return
{this.props.children}
; } } From 4bca777131fc122c3dff68ae1568de83205e7cef Mon Sep 17 00:00:00 2001 From: mfijas Date: Fri, 28 Oct 2016 19:28:44 +0200 Subject: [PATCH 175/271] Updated tutorial.md (#8054) Fixed method name (renderStep -> renderSquare). Removed
  • 's key to present the warning described below the code fragment. From 6825773882010d760507b31ab36b56d9831ca17c Mon Sep 17 00:00:00 2001 From: Gant Laborde Date: Fri, 28 Oct 2016 13:21:21 -0500 Subject: [PATCH 176/271] Organize and add confs (#8129) Upcoming proximity followed by past chronological. --- docs/community/conferences.md | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/docs/community/conferences.md b/docs/community/conferences.md index 39ac533c8a44..0fcf2e6c8b99 100644 --- a/docs/community/conferences.md +++ b/docs/community/conferences.md @@ -7,6 +7,26 @@ permalink: community/conferences.html redirect_from: "docs/conferences.html" --- +## Upcoming Conferences + +### ReactEurope 2017 +May 18th & 19th in Paris, France + +[Website](http://www.react-europe.org/) - [Schedule](http://www.react-europe.org/#schedule) + +### Chain React 2017 +Summer 2017, Portland, Oregon USA + +[Website](https://infinite.red/ChainReactConf) - [Twitter](https://twitter.com/chainreactconf) + +### React Native EU 2017 +Fall 2017, Poland + +[Website](http://react-native.eu/) + + +## Past Conferences + ### React.js Conf 2015 January 28 & 29 in Facebook HQ, CA @@ -62,4 +82,4 @@ October 26-28 in Bratislava, Slovakia ### React Remote Conf 2016 October 26-28 online -[Website](https://allremoteconfs.com/react-2016) - [Schedule](https://allremoteconfs.com/react-2016#schedule) \ No newline at end of file +[Website](https://allremoteconfs.com/react-2016) - [Schedule](https://allremoteconfs.com/react-2016#schedule) From bd3c90ac6f9b601e0c0b53ae38b0e8d4bbc5ac03 Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Fri, 28 Oct 2016 15:29:19 +0100 Subject: [PATCH 177/271] Add more tests for error boundaries --- .../__tests__/ReactErrorBoundaries-test.js | 461 ++++++++++++------ 1 file changed, 299 insertions(+), 162 deletions(-) diff --git a/src/renderers/shared/stack/reconciler/__tests__/ReactErrorBoundaries-test.js b/src/renderers/shared/stack/reconciler/__tests__/ReactErrorBoundaries-test.js index 78fa5fe6ce8c..63762a5fcbce 100644 --- a/src/renderers/shared/stack/reconciler/__tests__/ReactErrorBoundaries-test.js +++ b/src/renderers/shared/stack/reconciler/__tests__/ReactErrorBoundaries-test.js @@ -28,6 +28,7 @@ describe('ReactErrorBoundaries', () => { var BrokenComponentWillUnmount; var BrokenRenderErrorBoundary; var BrokenComponentWillMountErrorBoundary; + var BrokenComponentDidMountErrorBoundary; var BrokenRender; var ErrorBoundary; var ErrorMessage; @@ -283,6 +284,36 @@ describe('ReactErrorBoundaries', () => { } }; + BrokenComponentDidMountErrorBoundary = class extends React.Component { + constructor(props) { + super(props); + this.state = {error: null}; + log.push('BrokenComponentDidMountErrorBoundary constructor'); + } + render() { + if (this.state.error) { + log.push('BrokenComponentDidMountErrorBoundary render error'); + return
    Caught an error: {this.state.error.message}.
    ; + } + log.push('BrokenComponentDidMountErrorBoundary render success'); + return
    {this.props.children}
    ; + } + componentWillMount() { + log.push('BrokenComponentDidMountErrorBoundary componentWillMount'); + } + componentDidMount() { + log.push('BrokenComponentDidMountErrorBoundary componentDidMount [!]'); + throw new Error('Hello'); + } + componentWillUnmount() { + log.push('BrokenComponentDidMountErrorBoundary componentWillUnmount'); + } + unstable_handleError(error) { + log.push('BrokenComponentDidMountErrorBoundary unstable_handleError'); + this.setState({error}); + } + }; + BrokenRenderErrorBoundary = class extends React.Component { constructor(props) { super(props); @@ -397,43 +428,44 @@ describe('ReactErrorBoundaries', () => { }; ErrorBoundary = class extends React.Component { - constructor() { - super(); + constructor(props) { + super(props); this.state = {error: null}; - log.push('ErrorBoundary constructor'); + log.push(`${this.props.logName} constructor`); } render() { if (this.state.error && !this.props.forceRetry) { - log.push('ErrorBoundary render error'); + log.push(`${this.props.logName} render error`); return this.props.renderError(this.state.error, this.props); } - log.push('ErrorBoundary render success'); + log.push(`${this.props.logName} render success`); return
    {this.props.children}
    ; } unstable_handleError(error) { - log.push('ErrorBoundary unstable_handleError'); + log.push(`${this.props.logName} unstable_handleError`); this.setState({error}); } componentWillMount() { - log.push('ErrorBoundary componentWillMount'); + log.push(`${this.props.logName} componentWillMount`); } componentDidMount() { - log.push('ErrorBoundary componentDidMount'); + log.push(`${this.props.logName} componentDidMount`); } componentWillReceiveProps() { - log.push('ErrorBoundary componentWillReceiveProps'); + log.push(`${this.props.logName} componentWillReceiveProps`); } componentWillUpdate() { - log.push('ErrorBoundary componentWillUpdate'); + log.push(`${this.props.logName} componentWillUpdate`); } componentDidUpdate() { - log.push('ErrorBoundary componentDidUpdate'); + log.push(`${this.props.logName} componentDidUpdate`); } componentWillUnmount() { - log.push('ErrorBoundary componentWillUnmount'); + log.push(`${this.props.logName} componentWillUnmount`); } }; ErrorBoundary.defaultProps = { + logName: 'ErrorBoundary', renderError(error, props) { return (
    @@ -464,53 +496,6 @@ describe('ReactErrorBoundaries', () => { }; }); - if (ReactDOMFeatureFlags.useFiber) { - // This test implements a new feature in Fiber. - it('catches errors originating downstream', () => { - var fail = false; - class Stateful extends React.Component { - state = {shouldThrow: false}; - - render() { - if (fail) { - log.push('Stateful render [!]'); - throw new Error('Hello'); - } - return
    {this.props.children}
    ; - } - } - - var statefulInst; - var container = document.createElement('div'); - ReactDOM.render( - - statefulInst = inst} /> - , - container - ); - - log.length = 0; - expect(() => { - fail = true; - statefulInst.forceUpdate(); - }).not.toThrow(); - - expect(log).toEqual([ - 'Stateful render [!]', - 'ErrorBoundary unstable_handleError', - 'ErrorBoundary componentWillUpdate', - 'ErrorBoundary render error', - 'ErrorBoundary componentDidUpdate', - ]); - - log.length = 0; - ReactDOM.unmountComponentAtNode(container); - expect(log).toEqual([ - 'ErrorBoundary componentWillUnmount', - ]); - }); - } - it('renders an error state if child throws in render', () => { var container = document.createElement('div'); ReactDOM.render( @@ -661,65 +646,6 @@ describe('ReactErrorBoundaries', () => { ]); }); - if (ReactDOMFeatureFlags.useFiber) { - // This test implements a new feature in Fiber. - it('catches errors in componentDidMount', () => { - var container = document.createElement('div'); - ReactDOM.render( - - - - - - - , - container - ); - expect(log).toEqual([ - 'ErrorBoundary constructor', - 'ErrorBoundary componentWillMount', - 'ErrorBoundary render success', - 'BrokenComponentWillUnmount constructor', - 'BrokenComponentWillUnmount componentWillMount', - 'BrokenComponentWillUnmount render', - 'Normal constructor', - 'Normal componentWillMount', - 'Normal render', - 'BrokenComponentDidMount constructor', - 'BrokenComponentDidMount componentWillMount', - 'BrokenComponentDidMount render', - 'LastChild constructor', - 'LastChild componentWillMount', - 'LastChild render', - // Start flushing didMount queue - 'Normal componentDidMount', - 'BrokenComponentWillUnmount componentDidMount', - 'BrokenComponentDidMount componentDidMount [!]', - // Continue despite the error - 'LastChild componentDidMount', - 'ErrorBoundary componentDidMount', - // Now we are ready to handle the error - 'ErrorBoundary unstable_handleError', - 'ErrorBoundary componentWillUpdate', - 'ErrorBoundary render error', - // Safely unmount every child - 'BrokenComponentWillUnmount componentWillUnmount [!]', - // Continue unmounting safely despite any errors - 'Normal componentWillUnmount', - 'BrokenComponentDidMount componentWillUnmount', - 'LastChild componentWillUnmount', - // The update has finished - 'ErrorBoundary componentDidUpdate', - ]); - - log.length = 0; - ReactDOM.unmountComponentAtNode(container); - expect(log).toEqual([ - 'ErrorBoundary componentWillUnmount', - ]); - }); - } - it('propagates errors on retry on mounting', () => { var container = document.createElement('div'); ReactDOM.render( @@ -1356,50 +1282,6 @@ describe('ReactErrorBoundaries', () => { ]); }); - if (ReactDOMFeatureFlags.useFiber) { - // This test implements a new feature in Fiber. - it('catches errors in componentDidUpdate', () => { - var container = document.createElement('div'); - ReactDOM.render( - - - , - container - ); - - log.length = 0; - ReactDOM.render( - - - , - container - ); - expect(log).toEqual([ - 'ErrorBoundary componentWillReceiveProps', - 'ErrorBoundary componentWillUpdate', - 'ErrorBoundary render success', - 'BrokenComponentDidUpdate componentWillReceiveProps', - 'BrokenComponentDidUpdate componentWillUpdate', - 'BrokenComponentDidUpdate render', - // All lifecycles run - 'BrokenComponentDidUpdate componentDidUpdate [!]', - 'ErrorBoundary componentDidUpdate', - // Then, error is handled - 'ErrorBoundary unstable_handleError', - 'ErrorBoundary componentWillUpdate', - 'ErrorBoundary render error', - 'BrokenComponentDidUpdate componentWillUnmount', - 'ErrorBoundary componentDidUpdate', - ]); - - log.length = 0; - ReactDOM.unmountComponentAtNode(container); - expect(log).toEqual([ - 'ErrorBoundary componentWillUnmount', - ]); - }); - } - it('recovers from componentWillUnmount errors on update', () => { var container = document.createElement('div'); ReactDOM.render( @@ -1535,6 +1417,76 @@ describe('ReactErrorBoundaries', () => { ]); }); + it('picks the right boundary when handling unmounting errors', () => { + function renderInnerError(error, props) { + return
    Caught an inner error: {error.message}.
    ; + } + function renderOuterError(error, props) { + return
    Caught an outer error: {error.message}.
    ; + } + + var container = document.createElement('div'); + ReactDOM.render( + + + + + , + container + ); + + log.length = 0; + ReactDOM.render( + + + , + container + ); + expect(container.textContent).toBe('Caught an inner error: Hello.'); + expect(log).toEqual([ + // Update outer boundary + 'OuterErrorBoundary componentWillReceiveProps', + 'OuterErrorBoundary componentWillUpdate', + 'OuterErrorBoundary render success', + // Update inner boundary + 'InnerErrorBoundary componentWillReceiveProps', + 'InnerErrorBoundary componentWillUpdate', + 'InnerErrorBoundary render success', + // Try unmounting child + 'BrokenComponentWillUnmount componentWillUnmount [!]', + ...(ReactDOMFeatureFlags.useFiber ? [ + // Fiber proceeds with lifecycles despite errors + // Inner and outer boundaries have updated in this phase + 'InnerErrorBoundary componentDidUpdate', + 'OuterErrorBoundary componentDidUpdate', + // Now that commit phase is done, Fiber handles errors + // Only inner boundary receives the error: + 'InnerErrorBoundary unstable_handleError', + 'InnerErrorBoundary componentWillUpdate', + // Render an error now + 'InnerErrorBoundary render error', + // In Fiber, this was a local update to the + // inner boundary so only its hook fires + 'InnerErrorBoundary componentDidUpdate', + ] : [ + // Stack will handle error immediately + 'InnerErrorBoundary unstable_handleError', + 'InnerErrorBoundary render error', + // In stack, this was a part of the update to the + // outer boundary so both lifecycles fire + 'InnerErrorBoundary componentDidUpdate', + 'OuterErrorBoundary componentDidUpdate', + ]), + ]); + + log.length = 0; + ReactDOM.unmountComponentAtNode(container); + expect(log).toEqual([ + 'OuterErrorBoundary componentWillUnmount', + 'InnerErrorBoundary componentWillUnmount', + ]); + }); + it('can recover from error state', () => { var container = document.createElement('div'); ReactDOM.render( @@ -1701,4 +1653,189 @@ describe('ReactErrorBoundaries', () => { 'ErrorBoundary componentWillUnmount', ]); }); + + // The tests below implement new features in Fiber. + if (ReactDOMFeatureFlags.useFiber) { + it('catches errors originating downstream', () => { + var fail = false; + class Stateful extends React.Component { + state = {shouldThrow: false}; + + render() { + if (fail) { + log.push('Stateful render [!]'); + throw new Error('Hello'); + } + return
    {this.props.children}
    ; + } + } + + var statefulInst; + var container = document.createElement('div'); + ReactDOM.render( + + statefulInst = inst} /> + , + container + ); + + log.length = 0; + expect(() => { + fail = true; + statefulInst.forceUpdate(); + }).not.toThrow(); + + expect(log).toEqual([ + 'Stateful render [!]', + 'ErrorBoundary unstable_handleError', + 'ErrorBoundary componentWillUpdate', + 'ErrorBoundary render error', + 'ErrorBoundary componentDidUpdate', + ]); + + log.length = 0; + ReactDOM.unmountComponentAtNode(container); + expect(log).toEqual([ + 'ErrorBoundary componentWillUnmount', + ]); + }); + + it('catches errors in componentDidMount', () => { + var container = document.createElement('div'); + ReactDOM.render( + + + + + + + , + container + ); + expect(log).toEqual([ + 'ErrorBoundary constructor', + 'ErrorBoundary componentWillMount', + 'ErrorBoundary render success', + 'BrokenComponentWillUnmount constructor', + 'BrokenComponentWillUnmount componentWillMount', + 'BrokenComponentWillUnmount render', + 'Normal constructor', + 'Normal componentWillMount', + 'Normal render', + 'BrokenComponentDidMount constructor', + 'BrokenComponentDidMount componentWillMount', + 'BrokenComponentDidMount render', + 'LastChild constructor', + 'LastChild componentWillMount', + 'LastChild render', + // Start flushing didMount queue + 'Normal componentDidMount', + 'BrokenComponentWillUnmount componentDidMount', + 'BrokenComponentDidMount componentDidMount [!]', + // Continue despite the error + 'LastChild componentDidMount', + 'ErrorBoundary componentDidMount', + // Now we are ready to handle the error + 'ErrorBoundary unstable_handleError', + 'ErrorBoundary componentWillUpdate', + 'ErrorBoundary render error', + // Safely unmount every child + 'BrokenComponentWillUnmount componentWillUnmount [!]', + // Continue unmounting safely despite any errors + 'Normal componentWillUnmount', + 'BrokenComponentDidMount componentWillUnmount', + 'LastChild componentWillUnmount', + // The update has finished + 'ErrorBoundary componentDidUpdate', + ]); + + log.length = 0; + ReactDOM.unmountComponentAtNode(container); + expect(log).toEqual([ + 'ErrorBoundary componentWillUnmount', + ]); + }); + + it('propagates errors inside boundary during componentDidMount', () => { + var container = document.createElement('div'); + ReactDOM.render( + + ( +
    + We should never catch our own error: {error.message}. +
    + )} /> +
    , + container + ); + expect(container.firstChild.textContent).toBe('Caught an error: Hello.'); + expect(log).toEqual([ + 'ErrorBoundary constructor', + 'ErrorBoundary componentWillMount', + 'ErrorBoundary render success', + 'BrokenComponentDidMountErrorBoundary constructor', + 'BrokenComponentDidMountErrorBoundary componentWillMount', + 'BrokenComponentDidMountErrorBoundary render success', + 'BrokenComponentDidMountErrorBoundary componentDidMount [!]', + // Fiber proceeds with the hooks + 'ErrorBoundary componentDidMount', + // The error propagates to the higher boundary + 'ErrorBoundary unstable_handleError', + // Fiber retries from the root + 'ErrorBoundary componentWillUpdate', + 'ErrorBoundary render error', + 'BrokenComponentDidMountErrorBoundary componentWillUnmount', + 'ErrorBoundary componentDidUpdate', + ]); + + log.length = 0; + ReactDOM.unmountComponentAtNode(container); + expect(log).toEqual([ + 'ErrorBoundary componentWillUnmount', + ]); + }); + + it('catches errors in componentDidUpdate', () => { + var container = document.createElement('div'); + ReactDOM.render( + + + , + container + ); + + log.length = 0; + ReactDOM.render( + + + , + container + ); + expect(log).toEqual([ + 'ErrorBoundary componentWillReceiveProps', + 'ErrorBoundary componentWillUpdate', + 'ErrorBoundary render success', + 'BrokenComponentDidUpdate componentWillReceiveProps', + 'BrokenComponentDidUpdate componentWillUpdate', + 'BrokenComponentDidUpdate render', + // All lifecycles run + 'BrokenComponentDidUpdate componentDidUpdate [!]', + 'ErrorBoundary componentDidUpdate', + // Then, error is handled + 'ErrorBoundary unstable_handleError', + 'ErrorBoundary componentWillUpdate', + 'ErrorBoundary render error', + 'BrokenComponentDidUpdate componentWillUnmount', + 'ErrorBoundary componentDidUpdate', + ]); + + log.length = 0; + ReactDOM.unmountComponentAtNode(container); + expect(log).toEqual([ + 'ErrorBoundary componentWillUnmount', + ]); + }); + } + }); From f51abe0b552ceabfa926957337d182d6f7173344 Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Fri, 28 Oct 2016 15:42:06 +0100 Subject: [PATCH 178/271] Add a failing test for multiple independent boundaries Now that commits are treated as atomic, it is possible that componentDidMount, componentDidUpdate, or componentWillUnmount threw in multiple places during the commit. We need to make sure we notify all affected boundaries of the first errors in them. --- .../__tests__/ReactErrorBoundaries-test.js | 141 +++++++++++++++--- 1 file changed, 121 insertions(+), 20 deletions(-) diff --git a/src/renderers/shared/stack/reconciler/__tests__/ReactErrorBoundaries-test.js b/src/renderers/shared/stack/reconciler/__tests__/ReactErrorBoundaries-test.js index 63762a5fcbce..e7475d5eef69 100644 --- a/src/renderers/shared/stack/reconciler/__tests__/ReactErrorBoundaries-test.js +++ b/src/renderers/shared/stack/reconciler/__tests__/ReactErrorBoundaries-test.js @@ -195,6 +195,9 @@ describe('ReactErrorBoundaries', () => { }; BrokenComponentDidUpdate = class extends React.Component { + static defaultProps = { + errorText: 'Hello' + }; constructor(props) { super(props); log.push('BrokenComponentDidUpdate constructor'); @@ -217,7 +220,7 @@ describe('ReactErrorBoundaries', () => { } componentDidUpdate() { log.push('BrokenComponentDidUpdate componentDidUpdate [!]'); - throw new Error('Hello'); + throw new Error(this.props.errorText); } componentWillUnmount() { log.push('BrokenComponentDidUpdate componentWillUnmount'); @@ -225,6 +228,9 @@ describe('ReactErrorBoundaries', () => { }; BrokenComponentWillUnmount = class extends React.Component { + static defaultProps = { + errorText: 'Hello' + }; constructor(props) { super(props); log.push('BrokenComponentWillUnmount constructor'); @@ -250,7 +256,7 @@ describe('ReactErrorBoundaries', () => { } componentWillUnmount() { log.push('BrokenComponentWillUnmount componentWillUnmount [!]'); - throw new Error('Hello'); + throw new Error(this.props.errorText); } }; @@ -1418,10 +1424,10 @@ describe('ReactErrorBoundaries', () => { }); it('picks the right boundary when handling unmounting errors', () => { - function renderInnerError(error, props) { + function renderInnerError(error) { return
    Caught an inner error: {error.message}.
    ; } - function renderOuterError(error, props) { + function renderOuterError(error) { return
    Caught an outer error: {error.message}.
    ; } @@ -1756,6 +1762,47 @@ describe('ReactErrorBoundaries', () => { ]); }); + it('catches errors in componentDidUpdate', () => { + var container = document.createElement('div'); + ReactDOM.render( + + + , + container + ); + + log.length = 0; + ReactDOM.render( + + + , + container + ); + expect(log).toEqual([ + 'ErrorBoundary componentWillReceiveProps', + 'ErrorBoundary componentWillUpdate', + 'ErrorBoundary render success', + 'BrokenComponentDidUpdate componentWillReceiveProps', + 'BrokenComponentDidUpdate componentWillUpdate', + 'BrokenComponentDidUpdate render', + // All lifecycles run + 'BrokenComponentDidUpdate componentDidUpdate [!]', + 'ErrorBoundary componentDidUpdate', + // Then, error is handled + 'ErrorBoundary unstable_handleError', + 'ErrorBoundary componentWillUpdate', + 'ErrorBoundary render error', + 'BrokenComponentDidUpdate componentWillUnmount', + 'ErrorBoundary componentDidUpdate', + ]); + + log.length = 0; + ReactDOM.unmountComponentAtNode(container); + expect(log).toEqual([ + 'ErrorBoundary componentWillUnmount', + ]); + }); + it('propagates errors inside boundary during componentDidMount', () => { var container = document.createElement('div'); ReactDOM.render( @@ -1796,44 +1843,98 @@ describe('ReactErrorBoundaries', () => { ]); }); - it('catches errors in componentDidUpdate', () => { + it('lets different boundaries catch their own first errors', () => { + function renderUnmountError(error) { + return
    Caught an unmounting error: {error.message}.
    ; + } + function renderUpdateError(error) { + return
    Caught an updating error: {error.message}.
    ; + } + var container = document.createElement('div'); ReactDOM.render( - - + + + + + + + + + , container ); log.length = 0; ReactDOM.render( - - + + + + + + , container ); + + expect(container.firstChild.textContent).toBe( + 'Caught an unmounting error: E1.' + + 'Caught an updating error: E3.' + ); expect(log).toEqual([ - 'ErrorBoundary componentWillReceiveProps', - 'ErrorBoundary componentWillUpdate', - 'ErrorBoundary render success', + // Begin update phase + 'OuterErrorBoundary componentWillReceiveProps', + 'OuterErrorBoundary componentWillUpdate', + 'OuterErrorBoundary render success', + 'InnerUnmountBoundary componentWillReceiveProps', + 'InnerUnmountBoundary componentWillUpdate', + 'InnerUnmountBoundary render success', + 'InnerUpdateBoundary componentWillReceiveProps', + 'InnerUpdateBoundary componentWillUpdate', + 'InnerUpdateBoundary render success', + // First come the updates 'BrokenComponentDidUpdate componentWillReceiveProps', 'BrokenComponentDidUpdate componentWillUpdate', 'BrokenComponentDidUpdate render', - // All lifecycles run + 'BrokenComponentDidUpdate componentWillReceiveProps', + 'BrokenComponentDidUpdate componentWillUpdate', + 'BrokenComponentDidUpdate render', + // We're in commit phase now, deleting + 'BrokenComponentWillUnmount componentWillUnmount [!]', + 'BrokenComponentWillUnmount componentWillUnmount [!]', + // Continue despite errors, handle them after commit is done + 'InnerUnmountBoundary componentDidUpdate', + // We're still in commit phase, now calling update lifecycles 'BrokenComponentDidUpdate componentDidUpdate [!]', - 'ErrorBoundary componentDidUpdate', - // Then, error is handled - 'ErrorBoundary unstable_handleError', - 'ErrorBoundary componentWillUpdate', - 'ErrorBoundary render error', + // Again, continue despite errors, we'll handle them later + 'BrokenComponentDidUpdate componentDidUpdate [!]', + 'InnerUpdateBoundary componentDidUpdate', + 'OuterErrorBoundary componentDidUpdate', + // The interesting part starts now. + // Acknowledge errors independently but don't update yet: + 'InnerUnmountBoundary unstable_handleError', + 'InnerUpdateBoundary unstable_handleError', + // Only two of four errors are acknowledged: one per boundary. + // The rest are likely cascading and we ignore them. + // Now update: + 'InnerUnmountBoundary componentWillUpdate', + 'InnerUnmountBoundary render error', + 'InnerUpdateBoundary componentWillUpdate', + 'InnerUpdateBoundary render error', + // Commit 'BrokenComponentDidUpdate componentWillUnmount', - 'ErrorBoundary componentDidUpdate', + 'BrokenComponentDidUpdate componentWillUnmount', + 'InnerUnmountBoundary componentDidUpdate', + 'InnerUpdateBoundary componentDidUpdate', ]); log.length = 0; ReactDOM.unmountComponentAtNode(container); expect(log).toEqual([ - 'ErrorBoundary componentWillUnmount', + 'OuterErrorBoundary componentWillUnmount', + 'InnerUnmountBoundary componentWillUnmount', + 'InnerUpdateBoundary componentWillUnmount', ]); }); } From c08469a7d6647fd1e4f0480713bdf2d9e49a719f Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Fri, 28 Oct 2016 19:23:35 +0100 Subject: [PATCH 179/271] Add tests verifying we don't swallow exceptions --- .../__tests__/ReactErrorBoundaries-test.js | 88 ++++++++++++++++++- 1 file changed, 86 insertions(+), 2 deletions(-) diff --git a/src/renderers/shared/stack/reconciler/__tests__/ReactErrorBoundaries-test.js b/src/renderers/shared/stack/reconciler/__tests__/ReactErrorBoundaries-test.js index e7475d5eef69..658b76a1deef 100644 --- a/src/renderers/shared/stack/reconciler/__tests__/ReactErrorBoundaries-test.js +++ b/src/renderers/shared/stack/reconciler/__tests__/ReactErrorBoundaries-test.js @@ -196,7 +196,7 @@ describe('ReactErrorBoundaries', () => { BrokenComponentDidUpdate = class extends React.Component { static defaultProps = { - errorText: 'Hello' + errorText: 'Hello', }; constructor(props) { super(props); @@ -229,7 +229,7 @@ describe('ReactErrorBoundaries', () => { BrokenComponentWillUnmount = class extends React.Component { static defaultProps = { - errorText: 'Hello' + errorText: 'Hello', }; constructor(props) { super(props); @@ -502,6 +502,90 @@ describe('ReactErrorBoundaries', () => { }; }); + it('does not swallow exceptions on mounting without boundaries', () => { + var container = document.createElement('div'); + expect(() => { + ReactDOM.render(, container); + }).toThrow('Hello'); + + container = document.createElement('div'); + expect(() => { + ReactDOM.render(, container); + }).toThrow('Hello'); + + container = document.createElement('div'); + expect(() => { + ReactDOM.render(, container); + }).toThrow('Hello'); + }); + + it('does not swallow exceptions on updating without boundaries', () => { + var container = document.createElement('div'); + ReactDOM.render(, container); + expect(() => { + ReactDOM.render(, container); + }).toThrow('Hello'); + + container = document.createElement('div'); + ReactDOM.render(, container); + expect(() => { + ReactDOM.render(, container); + }).toThrow('Hello'); + + container = document.createElement('div'); + ReactDOM.render(, container); + expect(() => { + ReactDOM.render(, container); + }).toThrow('Hello'); + }); + + it('does not swallow exceptions on unmounting without boundaries', () => { + var container = document.createElement('div'); + ReactDOM.render(, container); + expect(() => { + ReactDOM.unmountComponentAtNode(container); + }).toThrow('Hello'); + }); + + it('prevents errors from leaking into other roots', () => { + var container1 = document.createElement('div'); + var container2 = document.createElement('div'); + var container3 = document.createElement('div'); + + ReactDOM.render(Before 1, container1); + expect(() => { + ReactDOM.render(, container2); + }).toThrow('Hello'); + ReactDOM.render( + + + , + container3 + ); + expect(container1.firstChild.textContent).toBe('Before 1'); + expect(container2.firstChild).toBe(null); + expect(container3.firstChild.textContent).toBe('Caught an error: Hello.'); + + ReactDOM.render(After 1, container1); + ReactDOM.render(After 2, container2); + ReactDOM.render( + + After 3 + , + container3 + ); + expect(container1.firstChild.textContent).toBe('After 1'); + expect(container2.firstChild.textContent).toBe('After 2'); + expect(container3.firstChild.textContent).toBe('After 3'); + + ReactDOM.unmountComponentAtNode(container1); + ReactDOM.unmountComponentAtNode(container2); + ReactDOM.unmountComponentAtNode(container3); + expect(container1.firstChild).toBe(null); + expect(container2.firstChild).toBe(null); + expect(container3.firstChild).toBe(null); + }); + it('renders an error state if child throws in render', () => { var container = document.createElement('div'); ReactDOM.render( From d8441cd725bcc3bc4f5be7d21be0f3788fde9590 Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Fri, 28 Oct 2016 21:22:44 +0100 Subject: [PATCH 180/271] Handle multiple errors in boundaries --- src/renderers/dom/fiber/ReactDOMFiber.js | 1 + .../shared/fiber/ReactFiberScheduler.js | 138 ++++++++++++------ 2 files changed, 92 insertions(+), 47 deletions(-) diff --git a/src/renderers/dom/fiber/ReactDOMFiber.js b/src/renderers/dom/fiber/ReactDOMFiber.js index d12b1aca9d39..8294883db55b 100644 --- a/src/renderers/dom/fiber/ReactDOMFiber.js +++ b/src/renderers/dom/fiber/ReactDOMFiber.js @@ -131,6 +131,7 @@ var ReactDOM = { render(element : ReactElement, container : DOMContainerElement, callback: ?Function) { warnAboutUnstableUse(); let root; + if (!container._reactRootContainer) { root = container._reactRootContainer = DOMRenderer.mountContainer(element, container, callback); } else { diff --git a/src/renderers/shared/fiber/ReactFiberScheduler.js b/src/renderers/shared/fiber/ReactFiberScheduler.js index 54ca2ef2cbb7..d861c81cd17d 100644 --- a/src/renderers/shared/fiber/ReactFiberScheduler.js +++ b/src/renderers/shared/fiber/ReactFiberScheduler.js @@ -12,6 +12,7 @@ 'use strict'; +import type { TrappedError } from 'ReactFiberErrorBoundary'; import type { Fiber } from 'ReactFiber'; import type { FiberRoot } from 'ReactFiberRoot'; import type { HostConfig } from 'ReactFiberReconciler'; @@ -205,8 +206,7 @@ module.exports = function(config : HostConfig) { // Now that the tree has been committed, we can handle errors. if (allTrappedErrors) { - // TODO: handle multiple errors with distinct boundaries. - handleError(allTrappedErrors[0]); + handleErrors(allTrappedErrors); } } @@ -377,7 +377,7 @@ module.exports = function(config : HostConfig) { throw error; } const trappedError = trapError(failedUnitOfWork, error); - handleError(trappedError); + handleErrors([trappedError]); } } @@ -408,6 +408,7 @@ module.exports = function(config : HostConfig) { // We're the only work scheduled. nextScheduledRoot = root; lastScheduledRoot = root; + scheduleDeferredCallback(performDeferredWork); } } @@ -443,7 +444,7 @@ module.exports = function(config : HostConfig) { throw error; } const trappedError = trapError(failedUnitOfWork, error); - handleError(trappedError); + handleErrors([trappedError]); } } @@ -471,56 +472,99 @@ module.exports = function(config : HostConfig) { } } - function handleError(trappedError) { - const boundary = trappedError.boundary; - const error = trappedError.error; - if (!boundary) { - throw error; + function scheduleErrorBoundaryWork(boundary : Fiber, priority) : FiberRoot { + let root = null; + let fiber = boundary; + while (fiber) { + fiber.pendingWorkPriority = priority; + if (fiber.alternate) { + fiber.alternate.pendingWorkPriority = priority; + } + if (!fiber.return) { + if (fiber.tag === HostContainer) { + // We found the root. + // Remember it so we can update it. + root = ((fiber.stateNode : any) : FiberRoot); + break; + } else { + throw new Error('Invalid root'); + } + } + fiber = fiber.return; + } + if (!root) { + throw new Error('Could not find root from the boundary.'); } + return root; + } - try { - // Give error boundary a chance to update its state - acknowledgeErrorInBoundary(boundary, error); - - // We will process an update caused by an error boundary with synchronous priority. - // This leaves us free to not keep track of whether a boundary has errored. - // If it errors again, we will just catch the error and synchronously propagate it higher. - - // First, traverse upwards and set pending synchronous priority on the whole tree. - let fiber = boundary; - while (fiber) { - fiber.pendingWorkPriority = SynchronousPriority; - if (fiber.alternate) { - fiber.alternate.pendingWorkPriority = SynchronousPriority; + function handleErrors(initialTrappedErrors : Array) : void { + let nextTrappedErrors = initialTrappedErrors; + let firstUncaughtError = null; + + // In each phase, we will attempt to pass errors to boundaries and re-render them. + // If we get more errors, we propagate them to higher boundaries in the next iterations. + while (nextTrappedErrors) { + const trappedErrors = nextTrappedErrors; + nextTrappedErrors = null; + + // Pass errors to all affected boundaries. + const affectedBoundaries : Set = new Set(); + trappedErrors.forEach(trappedError => { + const boundary = trappedError.boundary; + const error = trappedError.error; + if (!boundary) { + firstUncaughtError = firstUncaughtError || error; + return; } - if (!fiber.return) { - if (fiber.tag === HostContainer) { - // We found the root. - // Now go to the second phase and update it synchronously. - break; - } else { - throw new Error('Invalid root'); + // Don't visit boundaries twice. + if (affectedBoundaries.has(boundary)) { + return; + } + // Give error boundary a chance to update its state. + try { + acknowledgeErrorInBoundary(boundary, error); + affectedBoundaries.add(boundary); + } catch (nextError) { + // If it throws, propagate the error. + nextTrappedErrors = nextTrappedErrors || []; + nextTrappedErrors.push(trapError(boundary, nextError)); + } + }); + + // We will process an update caused by each error boundary synchronously. + affectedBoundaries.forEach(boundary => { + // FIXME: We only specify LowPriority here so that setState() calls from the error + // boundaries are respected. Instead we should set default priority level or something + // like this. Reconsider this piece when synchronous scheduling is in place. + const priority = LowPriority; + const root = scheduleErrorBoundaryWork(boundary, priority); + // This should use findNextUnitOfWork() when synchronous scheduling is implemented. + let fiber = cloneFiber(root.current, priority); + try { + while (fiber) { + // TODO: this is the only place where we recurse and it's unfortunate. + // (This may potentially get us into handleErrors() again.) + fiber = performUnitOfWork(fiber, true); } + } catch (nextError) { + // If it throws, propagate the error. + nextTrappedErrors = nextTrappedErrors || []; + nextTrappedErrors.push(trapError(boundary, nextError)); } - fiber = fiber.return; - } - - if (!fiber) { - throw new Error('Could not find an error boundary root.'); - } + }); + } - // Find the work in progress tree. - const root : FiberRoot = (fiber.stateNode : any); - fiber = root.current.alternate; + // Surface the first error uncaught by the boundaries to the user. + if (firstUncaughtError) { + // We need to make sure any future root can get scheduled despite these errors. + // Currently after throwing, nothing gets scheduled because these fields are set. + // FIXME: this is likely a wrong fix! It's still better than ignoring updates though. + nextScheduledRoot = null; + lastScheduledRoot = null; - // Perform all the work synchronously. - while (fiber) { - fiber = performUnitOfWork(fiber, true); - } - } catch (nextError) { - // Propagate error to the next boundary or rethrow. - const nextTrappedError = trapError(boundary, nextError); - handleError(nextTrappedError); + // Throw any unhandled errors. + throw firstUncaughtError; } } From ed11b35302a408f4ac5a2efc1a8c023b8658bdc7 Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Fri, 28 Oct 2016 21:48:37 +0100 Subject: [PATCH 181/271] The issue with tests is solved now --- .../stack/reconciler/__tests__/ReactErrorBoundaries-test.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/renderers/shared/stack/reconciler/__tests__/ReactErrorBoundaries-test.js b/src/renderers/shared/stack/reconciler/__tests__/ReactErrorBoundaries-test.js index 658b76a1deef..405d6567c17e 100644 --- a/src/renderers/shared/stack/reconciler/__tests__/ReactErrorBoundaries-test.js +++ b/src/renderers/shared/stack/reconciler/__tests__/ReactErrorBoundaries-test.js @@ -36,9 +36,6 @@ describe('ReactErrorBoundaries', () => { var Normal; beforeEach(() => { - // TODO: Fiber isn't error resilient and one test can bring down them all. - jest.resetModuleRegistry(); - ReactDOM = require('ReactDOM'); React = require('React'); From 1efb7a4e354703010c2001d769b516c18ed4726f Mon Sep 17 00:00:00 2001 From: Andrew Lo Date: Fri, 28 Oct 2016 19:31:01 -0400 Subject: [PATCH 182/271] In the community support doc, I noticed that the React Facebook (#8138) page link is broken since it's missing '.com'. --- docs/community/support.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/community/support.md b/docs/community/support.md index 2e7f9882eb80..86e3c1be7a56 100644 --- a/docs/community/support.md +++ b/docs/community/support.md @@ -25,6 +25,6 @@ If you need an answer right away, check out the [Reactiflux Discord](https://dis ## Facebook and Twitter -For the latest news about React, [like us on Facebook](https://facebook/react) and [follow **@reactjs** on Twitter](https://twitter.com/reactjs). In addition, you can use the [#reactjs](https://twitter.com/hashtag/reactjs) hashtag to see what others are saying or add to the conversation. +For the latest news about React, [like us on Facebook](https://facebook.com/react) and [follow **@reactjs** on Twitter](https://twitter.com/reactjs). In addition, you can use the [#reactjs](https://twitter.com/hashtag/reactjs) hashtag to see what others are saying or add to the conversation.
    From 9dc7a9594af963c4e5311e95d344e809a75f66ee Mon Sep 17 00:00:00 2001 From: Maksim Shastsel Date: Sat, 29 Oct 2016 13:24:12 +0300 Subject: [PATCH 183/271] Add support for node v7 (#8135) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 1b95e0826c4f..f73fd37e359b 100644 --- a/package.json +++ b/package.json @@ -71,7 +71,7 @@ "uglifyify": "^3.0.1" }, "devEngines": { - "node": "4.x || 5.x || 6.x", + "node": "4.x || 5.x || 6.x || 7.x", "npm": "2.x || 3.x || 4.x" }, "commonerConfig": { From f29a7bac0e232ef0250245638012212b7c60d272 Mon Sep 17 00:00:00 2001 From: Lee Sanghyeon Date: Sat, 29 Oct 2016 21:01:41 +0900 Subject: [PATCH 184/271] Update codebase-overview.md (#8142) * Update codebase-overview.md Fix the broken source code URL in 'Event System' section. * Update codebase-overview.md Re-fix link name --- docs/contributing/codebase-overview.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/contributing/codebase-overview.md b/docs/contributing/codebase-overview.md index 51bcf5375930..54845ef069bb 100644 --- a/docs/contributing/codebase-overview.md +++ b/docs/contributing/codebase-overview.md @@ -397,7 +397,7 @@ Its source code is located in [`src/renderers/shared/fiber`](https://github.com/ ### Event System -React implements a synthetic event system which is agnostic of the renderers and works both with React DOM and React Native. Its source code is located in [`src/renderers/shared/stack/event`](https://github.com/facebook/react/tree/master/src/renderers/shared/stack/event). +React implements a synthetic event system which is agnostic of the renderers and works both with React DOM and React Native. Its source code is located in [`src/renderers/shared/shared/event`](https://github.com/facebook/react/tree/master/src/renderers/shared/shared/event). There is a [video with a deep code dive into it](https://www.youtube.com/watch?v=dRo_egw7tBc) (66 mins). From 928541dc9ee1df266d621541922f71bb6e4b770f Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Sat, 29 Oct 2016 22:02:13 +0100 Subject: [PATCH 185/271] [Fiber] Fix TestUtils.findAllInRenderedTree (#8147) * [Fiber] Fix TestUtils.findAllInRenderedTree * Add a comment about coroutines --- src/test/ReactTestUtils.js | 46 ++++++++++++++++++++++++++++++++++---- 1 file changed, 42 insertions(+), 4 deletions(-) diff --git a/src/test/ReactTestUtils.js b/src/test/ReactTestUtils.js index a5c3b31d2316..0c2e567ba7b1 100644 --- a/src/test/ReactTestUtils.js +++ b/src/test/ReactTestUtils.js @@ -20,6 +20,7 @@ var ReactDOM = require('ReactDOM'); var ReactDOMComponentTree = require('ReactDOMComponentTree'); var ReactBrowserEventEmitter = require('ReactBrowserEventEmitter'); var ReactInstanceMap = require('ReactInstanceMap'); +var ReactTypeOfWork = require('ReactTypeOfWork'); var ReactUpdates = require('ReactUpdates'); var SyntheticEvent = require('SyntheticEvent'); var ReactShallowRenderer = require('ReactShallowRenderer'); @@ -28,6 +29,11 @@ var findDOMNode = require('findDOMNode'); var invariant = require('invariant'); var topLevelTypes = EventConstants.topLevelTypes; +var { + ClassComponent, + HostComponent, + HostText, +} = ReactTypeOfWork; function Event(suffix) {} @@ -35,7 +41,7 @@ function Event(suffix) {} * @class ReactTestUtils */ -function findAllInRenderedTreeInternal(inst, test) { +function findAllInRenderedStackTreeInternal(inst, test) { if (!inst || !inst.getPublicInstance) { return []; } @@ -50,7 +56,7 @@ function findAllInRenderedTreeInternal(inst, test) { continue; } ret = ret.concat( - findAllInRenderedTreeInternal( + findAllInRenderedStackTreeInternal( renderedChildren[key], test ) @@ -61,12 +67,39 @@ function findAllInRenderedTreeInternal(inst, test) { typeof currentElement.type === 'function' ) { ret = ret.concat( - findAllInRenderedTreeInternal(inst._renderedComponent, test) + findAllInRenderedStackTreeInternal(inst._renderedComponent, test) ); } return ret; } +function findAllInRenderedFiberTreeInternal(fiber, test) { + if (!fiber) { + return []; + } + if ( + fiber.tag !== ClassComponent && + fiber.tag !== HostComponent && + fiber.tag !== HostText + ) { + return []; + } + var publicInst = fiber.stateNode; + var ret = publicInst && test(publicInst) ? [publicInst] : []; + var child = fiber.child; + while (child) { + ret = ret.concat( + findAllInRenderedFiberTreeInternal( + child, + test + ) + ); + child = child.sibling; + } + // TODO: visit stateNode for coroutines + return ret; +} + /** * Utilities for making it easy to test React components. * @@ -170,7 +203,12 @@ var ReactTestUtils = { ReactTestUtils.isCompositeComponent(inst), 'findAllInRenderedTree(...): instance must be a composite component' ); - return findAllInRenderedTreeInternal(ReactInstanceMap.get(inst), test); + var internalInstance = ReactInstanceMap.get(inst); + if (internalInstance && typeof internalInstance.tag === 'number') { + return findAllInRenderedFiberTreeInternal(internalInstance, test); + } else { + return findAllInRenderedStackTreeInternal(internalInstance, test); + } }, /** From 6d747a742647f6e22790919647af36829e1f06a4 Mon Sep 17 00:00:00 2001 From: Robin Ricard Date: Sun, 30 Oct 2016 01:42:59 +0200 Subject: [PATCH 186/271] [Fiber] Fix tests for Fiber in ReactElement-test (#8150) * Test Fiber internals differently in ReactElement Instead of being able to access the owner's instance via getPublicInstance(), we use the Fiber's stateNode. (Fiber does not have methods, it's just a structure!) * Fix isCompositeComponentWithType for fiber In ReactTestUtils.isCompositeComponentWithType, we provide a way to walk through Fiber's internals in place of using _currentElement.type. We keep support for non-fiber though. * Test fiber reconciler consistently Use typeof instance.tag to test if we're using the fiber reconciler * Remove unnecessary comment --- .../classic/element/__tests__/ReactElement-test.js | 6 +++++- src/test/ReactTestUtils.js | 6 +++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/isomorphic/classic/element/__tests__/ReactElement-test.js b/src/isomorphic/classic/element/__tests__/ReactElement-test.js index 36814982c3f1..091e3e502ded 100644 --- a/src/isomorphic/classic/element/__tests__/ReactElement-test.js +++ b/src/isomorphic/classic/element/__tests__/ReactElement-test.js @@ -266,7 +266,11 @@ describe('ReactElement', () => { React.createElement(Wrapper) ); - expect(element._owner.getPublicInstance()).toBe(instance); + if (typeof element._owner.tag === 'number') { // Fiber reconciler + expect(element._owner.stateNode).toBe(instance); + } else { // Stack reconciler + expect(element._owner.getPublicInstance()).toBe(instance); + } }); it('merges an additional argument onto the children prop', () => { diff --git a/src/test/ReactTestUtils.js b/src/test/ReactTestUtils.js index 0c2e567ba7b1..b027850310d7 100644 --- a/src/test/ReactTestUtils.js +++ b/src/test/ReactTestUtils.js @@ -157,9 +157,9 @@ var ReactTestUtils = { return false; } var internalInstance = ReactInstanceMap.get(inst); - var constructor = internalInstance - ._currentElement - .type; + var constructor = typeof internalInstance.tag === 'number' ? + internalInstance.type : // Fiber reconciler + internalInstance._currentElement.type; // Stack reconciler return (constructor === type); }, From c17bf38035b48e7aa56f5c12ec2bf9b93a85883f Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Sun, 30 Oct 2016 00:43:34 +0100 Subject: [PATCH 187/271] Get a few more Fiber tests passing (#8149) * Get a few more Fiber tests passing * Fix Flow and wrap in __DEV__ --- .../classic/element/ReactElementValidator.js | 5 ++- .../hooks/ReactComponentTreeHook.js | 3 +- .../dom/shared/CSSPropertyOperations.js | 3 +- .../dom/shared/utils/LinkedValueUtils.js | 3 +- .../dom/shared/validateDOMNesting.js | 7 ++-- src/shared/utils/getComponentName.js | 40 +++++++++++++++++++ 6 files changed, 53 insertions(+), 8 deletions(-) create mode 100644 src/shared/utils/getComponentName.js diff --git a/src/isomorphic/classic/element/ReactElementValidator.js b/src/isomorphic/classic/element/ReactElementValidator.js index 3f7f717715d6..477c814d2f0c 100644 --- a/src/isomorphic/classic/element/ReactElementValidator.js +++ b/src/isomorphic/classic/element/ReactElementValidator.js @@ -25,12 +25,13 @@ var ReactElement = require('ReactElement'); var checkReactTypeSpec = require('checkReactTypeSpec'); var canDefineProperty = require('canDefineProperty'); +var getComponentName = require('getComponentName'); var getIteratorFn = require('getIteratorFn'); var warning = require('warning'); function getDeclarationErrorAddendum() { if (ReactCurrentOwner.current) { - var name = ReactCurrentOwner.current.getName(); + var name = getComponentName(ReactCurrentOwner.current); if (name) { return ' Check the render method of `' + name + '`.'; } @@ -94,7 +95,7 @@ function validateExplicitKey(element, parentType) { element._owner !== ReactCurrentOwner.current) { // Give the component that originally created this child. childOwner = - ` It was passed a child from ${element._owner.getName()}.`; + ` It was passed a child from ${getComponentName(element._owner)}.`; } warning( diff --git a/src/isomorphic/hooks/ReactComponentTreeHook.js b/src/isomorphic/hooks/ReactComponentTreeHook.js index cfae3b6401d4..75292cbfebb7 100644 --- a/src/isomorphic/hooks/ReactComponentTreeHook.js +++ b/src/isomorphic/hooks/ReactComponentTreeHook.js @@ -14,6 +14,7 @@ var ReactCurrentOwner = require('ReactCurrentOwner'); +var getComponentName = require('getComponentName'); var invariant = require('invariant'); var warning = require('warning'); @@ -310,7 +311,7 @@ var ReactComponentTreeHook = { info += describeComponentFrame( name, topElement._source, - owner && owner.getName() + owner && getComponentName(owner) ); } diff --git a/src/renderers/dom/shared/CSSPropertyOperations.js b/src/renderers/dom/shared/CSSPropertyOperations.js index 81c531995968..4843ed0dac59 100644 --- a/src/renderers/dom/shared/CSSPropertyOperations.js +++ b/src/renderers/dom/shared/CSSPropertyOperations.js @@ -17,6 +17,7 @@ var ReactInstrumentation = require('ReactInstrumentation'); var camelizeStyleName = require('camelizeStyleName'); var dangerousStyleValue = require('dangerousStyleValue'); +var getComponentName = require('getComponentName'); var hyphenateStyleName = require('hyphenateStyleName'); var memoizeStringOnly = require('memoizeStringOnly'); var warning = require('warning'); @@ -114,7 +115,7 @@ if (__DEV__) { var checkRenderMessage = function(owner) { if (owner) { - var name = owner.getName(); + var name = getComponentName(owner); if (name) { return ' Check the render method of `' + name + '`.'; } diff --git a/src/renderers/dom/shared/utils/LinkedValueUtils.js b/src/renderers/dom/shared/utils/LinkedValueUtils.js index 3b6c0057f691..c2143827ad10 100644 --- a/src/renderers/dom/shared/utils/LinkedValueUtils.js +++ b/src/renderers/dom/shared/utils/LinkedValueUtils.js @@ -13,6 +13,7 @@ var React = require('React'); +var getComponentName = require('getComponentName'); var invariant = require('invariant'); var warning = require('warning'); @@ -88,7 +89,7 @@ var propTypes = { var loggedTypeFailures = {}; function getDeclarationErrorAddendum(owner) { if (owner) { - var name = owner.getName(); + var name = getComponentName(owner); if (name) { return ' Check the render method of `' + name + '`.'; } diff --git a/src/renderers/dom/shared/validateDOMNesting.js b/src/renderers/dom/shared/validateDOMNesting.js index 860e37da1fba..790a43e1e759 100644 --- a/src/renderers/dom/shared/validateDOMNesting.js +++ b/src/renderers/dom/shared/validateDOMNesting.js @@ -12,6 +12,7 @@ 'use strict'; var emptyFunction = require('emptyFunction'); +var getComponentName = require('getComponentName'); var warning = require('warning'); var validateDOMNesting = emptyFunction; @@ -371,16 +372,16 @@ if (__DEV__) { var UNKNOWN = '(unknown)'; var childOwnerNames = childOwners.slice(deepestCommon + 1).map( - (inst) => inst.getName() || UNKNOWN + (inst) => getComponentName(inst) || UNKNOWN ); var ancestorOwnerNames = ancestorOwners.slice(deepestCommon + 1).map( - (inst) => inst.getName() || UNKNOWN + (inst) => getComponentName(inst) || UNKNOWN ); var ownerInfo = [].concat( // If the parent and child instances have a common owner ancestor, start // with that -- otherwise we just start with the parent's owners. deepestCommon !== -1 ? - childOwners[deepestCommon].getName() || UNKNOWN : + getComponentName(childOwners[deepestCommon]) || UNKNOWN : [], ancestorOwnerNames, ancestorTag, diff --git a/src/shared/utils/getComponentName.js b/src/shared/utils/getComponentName.js new file mode 100644 index 000000000000..a562b0685d7e --- /dev/null +++ b/src/shared/utils/getComponentName.js @@ -0,0 +1,40 @@ +/** + * 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. + * + * @flow + * @providesModule getComponentName + */ + +'use strict'; + +import type { ReactInstance } from 'ReactInstanceType'; +import type { Fiber } from 'ReactFiber'; + +function getComponentName(instanceOrFiber : ReactInstance | Fiber) : string | null { + if (__DEV__) { + if (typeof instanceOrFiber.getName === 'function') { + // Stack reconciler + const instance = ((instanceOrFiber : any) : ReactInstance); + return instance.getName() || 'Component'; + } + if (typeof instanceOrFiber.tag === 'number') { + // Fiber reconciler + const fiber = ((instanceOrFiber : any) : Fiber); + const {type} = fiber; + if (typeof type === 'string') { + return type; + } + if (typeof type === 'function') { + return type.displayName || type.name || null; + } + } + } + return null; +} + +module.exports = getComponentName; From c567b6e6187538b996db0da605806b0b91fc9c96 Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Sun, 30 Oct 2016 00:44:33 +0100 Subject: [PATCH 188/271] Remove use of reactComponentExpect in our tests (#8148) --- .../__tests__/ReactContextValidator-test.js | 7 +- .../classic/class/__tests__/ReactBind-test.js | 41 +++++------ .../class/__tests__/ReactBindOptout-test.js | 67 +++++++---------- .../__tests__/ReactCompositeComponent-test.js | 71 +++++++------------ ...actCompositeComponentDOMMinimalism-test.js | 27 +++---- .../__tests__/ReactEmptyComponent-test.js | 21 +++--- .../stack/reconciler/__tests__/refs-test.js | 15 ++-- 7 files changed, 92 insertions(+), 157 deletions(-) diff --git a/src/isomorphic/classic/__tests__/ReactContextValidator-test.js b/src/isomorphic/classic/__tests__/ReactContextValidator-test.js index bda23752f712..c0ba47b6240d 100644 --- a/src/isomorphic/classic/__tests__/ReactContextValidator-test.js +++ b/src/isomorphic/classic/__tests__/ReactContextValidator-test.js @@ -21,8 +21,6 @@ var React; var ReactDOM; var ReactTestUtils; -var reactComponentExpect; - describe('ReactContextValidator', () => { function normalizeCodeLocInfo(str) { return str.replace(/\(at .+?:\d+\)/g, '(at **)'); @@ -34,7 +32,6 @@ describe('ReactContextValidator', () => { React = require('React'); ReactDOM = require('ReactDOM'); ReactTestUtils = require('ReactTestUtils'); - reactComponentExpect = require('reactComponentExpect'); }); // TODO: This behavior creates a runtime dependency on propTypes. We should @@ -65,12 +62,12 @@ describe('ReactContextValidator', () => { }, render: function() { - return ; + return ; }, }); var instance = ReactTestUtils.renderIntoDocument(); - reactComponentExpect(instance).expectRenderedChild().scalarContextEqual({foo: 'abc'}); + expect(instance.refs.child.context).toEqual({foo: 'abc'}); }); it('should filter context properly in callbacks', () => { diff --git a/src/isomorphic/classic/class/__tests__/ReactBind-test.js b/src/isomorphic/classic/class/__tests__/ReactBind-test.js index a5b612eba3a7..5d33f8ed15ad 100644 --- a/src/isomorphic/classic/class/__tests__/ReactBind-test.js +++ b/src/isomorphic/classic/class/__tests__/ReactBind-test.js @@ -13,7 +13,6 @@ var React = require('React'); var ReactTestUtils = require('ReactTestUtils'); -var reactComponentExpect = require('reactComponentExpect'); // TODO: Test render and all stock methods. describe('autobinding', () => { @@ -42,6 +41,7 @@ describe('autobinding', () => { render: function() { return (
    { }, }); - var instance1 = ; - var mountedInstance1 = ReactTestUtils.renderIntoDocument(instance1); - var rendered1 = reactComponentExpect(mountedInstance1) - .expectRenderedChild() - .instance(); + var instance1 = ReactTestUtils.renderIntoDocument(); + var rendered1 = instance1.refs.child; - var instance2 = ; - var mountedInstance2 = ReactTestUtils.renderIntoDocument(instance2); - var rendered2 = reactComponentExpect(mountedInstance2) - .expectRenderedChild() - .instance(); + var instance2 = ReactTestUtils.renderIntoDocument(); + var rendered2 = instance2.refs.child; expect(function() { var badIdea = instance1.badIdeas.badBind; badIdea(); }).toThrow(); - expect(mountedInstance1.onClick).not.toBe(mountedInstance2.onClick); + expect(instance1.onClick).not.toBe(instance2.onClick); ReactTestUtils.Simulate.click(rendered1); expect(mouseDidClick.mock.instances.length).toBe(1); - expect(mouseDidClick.mock.instances[0]).toBe(mountedInstance1); + expect(mouseDidClick.mock.instances[0]).toBe(instance1); ReactTestUtils.Simulate.click(rendered2); expect(mouseDidClick.mock.instances.length).toBe(2); - expect(mouseDidClick.mock.instances[1]).toBe(mountedInstance2); + expect(mouseDidClick.mock.instances[1]).toBe(instance2); ReactTestUtils.Simulate.mouseOver(rendered1); expect(mouseDidEnter.mock.instances.length).toBe(1); - expect(mouseDidEnter.mock.instances[0]).toBe(mountedInstance1); + expect(mouseDidEnter.mock.instances[0]).toBe(instance1); ReactTestUtils.Simulate.mouseOver(rendered2); expect(mouseDidEnter.mock.instances.length).toBe(2); - expect(mouseDidEnter.mock.instances[1]).toBe(mountedInstance2); + expect(mouseDidEnter.mock.instances[1]).toBe(instance2); ReactTestUtils.Simulate.mouseOut(rendered1); expect(mouseDidLeave.mock.instances.length).toBe(1); - expect(mouseDidLeave.mock.instances[0]).toBe(mountedInstance1); + expect(mouseDidLeave.mock.instances[0]).toBe(instance1); ReactTestUtils.Simulate.mouseOut(rendered2); expect(mouseDidLeave.mock.instances.length).toBe(2); - expect(mouseDidLeave.mock.instances[1]).toBe(mountedInstance2); + expect(mouseDidLeave.mock.instances[1]).toBe(instance2); }); it('works with mixins', () => { @@ -105,19 +99,16 @@ describe('autobinding', () => { mixins: [TestMixin], render: function() { - return
    ; + return
    ; }, }); - var instance1 = ; - var mountedInstance1 = ReactTestUtils.renderIntoDocument(instance1); - var rendered1 = reactComponentExpect(mountedInstance1) - .expectRenderedChild() - .instance(); + var instance1 = ReactTestUtils.renderIntoDocument(); + var rendered1 = instance1.refs.child; ReactTestUtils.Simulate.click(rendered1); expect(mouseDidClick.mock.instances.length).toBe(1); - expect(mouseDidClick.mock.instances[0]).toBe(mountedInstance1); + expect(mouseDidClick.mock.instances[0]).toBe(instance1); }); it('warns if you try to bind to this', () => { diff --git a/src/isomorphic/classic/class/__tests__/ReactBindOptout-test.js b/src/isomorphic/classic/class/__tests__/ReactBindOptout-test.js index 635b594dee27..bfaa8f8e0f64 100644 --- a/src/isomorphic/classic/class/__tests__/ReactBindOptout-test.js +++ b/src/isomorphic/classic/class/__tests__/ReactBindOptout-test.js @@ -13,7 +13,6 @@ var React = require('React'); var ReactTestUtils = require('ReactTestUtils'); -var reactComponentExpect = require('reactComponentExpect'); // TODO: Test render and all stock methods. describe('autobind optout', () => { @@ -36,6 +35,7 @@ describe('autobind optout', () => { render: function() { return (
    { }, }); - var instance1 = ; - var mountedInstance1 = ReactTestUtils.renderIntoDocument(instance1); - var rendered1 = reactComponentExpect(mountedInstance1) - .expectRenderedChild() - .instance(); + var instance1 = ReactTestUtils.renderIntoDocument(); + var rendered1 = instance1.refs.child; - var instance2 = ; - var mountedInstance2 = ReactTestUtils.renderIntoDocument(instance2); - var rendered2 = reactComponentExpect(mountedInstance2) - .expectRenderedChild() - .instance(); + var instance2 = ReactTestUtils.renderIntoDocument(); + var rendered2 = instance2.refs.child; ReactTestUtils.Simulate.click(rendered1); expect(mouseDidClick.mock.instances.length).toBe(1); - expect(mouseDidClick.mock.instances[0]).toBe(mountedInstance1); + expect(mouseDidClick.mock.instances[0]).toBe(instance1); ReactTestUtils.Simulate.click(rendered2); expect(mouseDidClick.mock.instances.length).toBe(2); - expect(mouseDidClick.mock.instances[1]).toBe(mountedInstance2); + expect(mouseDidClick.mock.instances[1]).toBe(instance2); ReactTestUtils.Simulate.mouseOver(rendered1); expect(mouseDidEnter.mock.instances.length).toBe(1); - expect(mouseDidEnter.mock.instances[0]).toBe(mountedInstance1); + expect(mouseDidEnter.mock.instances[0]).toBe(instance1); ReactTestUtils.Simulate.mouseOver(rendered2); expect(mouseDidEnter.mock.instances.length).toBe(2); - expect(mouseDidEnter.mock.instances[1]).toBe(mountedInstance2); + expect(mouseDidEnter.mock.instances[1]).toBe(instance2); ReactTestUtils.Simulate.mouseOut(rendered1); expect(mouseDidLeave.mock.instances.length).toBe(1); - expect(mouseDidLeave.mock.instances[0]).toBe(mountedInstance1); + expect(mouseDidLeave.mock.instances[0]).toBe(instance1); ReactTestUtils.Simulate.mouseOut(rendered2); expect(mouseDidLeave.mock.instances.length).toBe(2); - expect(mouseDidLeave.mock.instances[1]).toBe(mountedInstance2); + expect(mouseDidLeave.mock.instances[1]).toBe(instance2); }); it('should not hold reference to instance', () => { @@ -103,30 +97,25 @@ describe('autobind optout', () => { render: function() { return (
    ); }, }); - var instance1 = ; - var mountedInstance1 = ReactTestUtils.renderIntoDocument(instance1); - var rendered1 = reactComponentExpect(mountedInstance1) - .expectRenderedChild() - .instance(); + var instance1 = ReactTestUtils.renderIntoDocument(); + var rendered1 = instance1.refs.child; - var instance2 = ; - var mountedInstance2 = ReactTestUtils.renderIntoDocument(instance2); - var rendered2 = reactComponentExpect(mountedInstance2) - .expectRenderedChild() - .instance(); + var instance2 = ReactTestUtils.renderIntoDocument(); + var rendered2 = instance2.refs.child; expect(function() { var badIdea = instance1.badIdeas.badBind; badIdea(); }).toThrow(); - expect(mountedInstance1.onClick).toBe(mountedInstance2.onClick); + expect(instance1.onClick).toBe(instance2.onClick); expect(function() { ReactTestUtils.Simulate.click(rendered1); @@ -148,19 +137,16 @@ describe('autobind optout', () => { mixins: [TestMixin], render: function() { - return
    ; + return
    ; }, }); - var instance1 = ; - var mountedInstance1 = ReactTestUtils.renderIntoDocument(instance1); - var rendered1 = reactComponentExpect(mountedInstance1) - .expectRenderedChild() - .instance(); + var instance1 = ReactTestUtils.renderIntoDocument(); + var rendered1 = instance1.refs.child; ReactTestUtils.Simulate.click(rendered1); expect(mouseDidClick.mock.instances.length).toBe(1); - expect(mouseDidClick.mock.instances[0]).toBe(mountedInstance1); + expect(mouseDidClick.mock.instances[0]).toBe(instance1); }); it('works with mixins that have opted out of autobinding', () => { @@ -175,19 +161,16 @@ describe('autobind optout', () => { mixins: [TestMixin], render: function() { - return
    ; + return
    ; }, }); - var instance1 = ; - var mountedInstance1 = ReactTestUtils.renderIntoDocument(instance1); - var rendered1 = reactComponentExpect(mountedInstance1) - .expectRenderedChild() - .instance(); + var instance1 = ReactTestUtils.renderIntoDocument(); + var rendered1 = instance1.refs.child; ReactTestUtils.Simulate.click(rendered1); expect(mouseDidClick.mock.instances.length).toBe(1); - expect(mouseDidClick.mock.instances[0]).toBe(mountedInstance1); + expect(mouseDidClick.mock.instances[0]).toBe(instance1); }); it('does not warn if you try to bind to this', () => { diff --git a/src/renderers/shared/stack/reconciler/__tests__/ReactCompositeComponent-test.js b/src/renderers/shared/stack/reconciler/__tests__/ReactCompositeComponent-test.js index f2aadf76e199..d62d4a332ec1 100644 --- a/src/renderers/shared/stack/reconciler/__tests__/ReactCompositeComponent-test.js +++ b/src/renderers/shared/stack/reconciler/__tests__/ReactCompositeComponent-test.js @@ -21,13 +21,10 @@ var ReactServerRendering; var ReactTestUtils; var ReactUpdates; -var reactComponentExpect; - describe('ReactCompositeComponent', () => { beforeEach(() => { jest.resetModuleRegistry(); - reactComponentExpect = require('reactComponentExpect'); React = require('React'); ReactDOM = require('ReactDOM'); ReactCurrentOwner = require('ReactCurrentOwner'); @@ -85,22 +82,17 @@ describe('ReactCompositeComponent', () => { }); it('should support rendering to different child types over time', () => { - var instance = ; - instance = ReactTestUtils.renderIntoDocument(instance); - - reactComponentExpect(instance) - .expectRenderedChild() - .toBeComponentOfType('a'); + var instance = ReactTestUtils.renderIntoDocument(); + var el = ReactDOM.findDOMNode(instance); + expect(el.tagName).toBe('A'); instance._toggleActivatedState(); - reactComponentExpect(instance) - .expectRenderedChild() - .toBeComponentOfType('b'); + el = ReactDOM.findDOMNode(instance); + expect(el.tagName).toBe('B'); instance._toggleActivatedState(); - reactComponentExpect(instance) - .expectRenderedChild() - .toBeComponentOfType('a'); + el = ReactDOM.findDOMNode(instance); + expect(el.tagName).toBe('A'); }); it('should not thrash a server rendered layout with client side one', () => { @@ -124,22 +116,17 @@ describe('ReactCompositeComponent', () => { }); it('should react to state changes from callbacks', () => { - var instance = ; - instance = ReactTestUtils.renderIntoDocument(instance); - - var renderedChild = reactComponentExpect(instance) - .expectRenderedChild() - .instance(); + var instance = ReactTestUtils.renderIntoDocument(); + var el = ReactDOM.findDOMNode(instance); + expect(el.tagName).toBe('A'); - ReactTestUtils.Simulate.click(renderedChild); - reactComponentExpect(instance) - .expectRenderedChild() - .toBeComponentOfType('b'); + ReactTestUtils.Simulate.click(el); + el = ReactDOM.findDOMNode(instance); + expect(el.tagName).toBe('B'); }); it('should rewire refs when rendering to different child types', () => { - var instance = ; - instance = ReactTestUtils.renderIntoDocument(instance); + var instance = ReactTestUtils.renderIntoDocument(); expect(ReactDOM.findDOMNode(instance.refs.x).tagName).toBe('A'); instance._toggleActivatedState(); @@ -237,17 +224,14 @@ describe('ReactCompositeComponent', () => { } } - var instance1 = ; - instance1 = ReactTestUtils.renderIntoDocument(instance1); - reactComponentExpect(instance1).scalarPropsEqual({prop: 'testKey'}); + var instance1 = ReactTestUtils.renderIntoDocument(); + expect(instance1.props).toEqual({prop: 'testKey'}); - var instance2 = ; - instance2 = ReactTestUtils.renderIntoDocument(instance2); - reactComponentExpect(instance2).scalarPropsEqual({prop: 'testKey'}); + var instance2 = ReactTestUtils.renderIntoDocument(); + expect(instance2.props).toEqual({prop: 'testKey'}); - var instance3 = ; - instance3 = ReactTestUtils.renderIntoDocument(instance3); - reactComponentExpect(instance3).scalarPropsEqual({prop: null}); + var instance3 = ReactTestUtils.renderIntoDocument(); + expect(instance3.props).toEqual({prop: null}); }); it('should not mutate passed-in props object', () => { @@ -659,12 +643,11 @@ describe('ReactCompositeComponent', () => { ); expect(parentInstance.state.flag).toBe(false); - reactComponentExpect(childInstance).scalarContextEqual({foo: 'bar', flag: false}); + expect(childInstance.context).toEqual({foo: 'bar', flag: false}); parentInstance.setState({flag: true}); expect(parentInstance.state.flag).toBe(true); - - reactComponentExpect(childInstance).scalarContextEqual({foo: 'bar', flag: true}); + expect(childInstance.context).toEqual({foo: 'bar', flag: true}); }); it('should pass context when re-rendered for static child within a composite component', () => { @@ -714,13 +697,13 @@ describe('ReactCompositeComponent', () => { ); expect(wrapper.refs.parent.state.flag).toEqual(true); - reactComponentExpect(wrapper.refs.child).scalarContextEqual({flag: true}); + expect(wrapper.refs.child.context).toEqual({flag: true}); // We update while is still a static prop relative to this update wrapper.refs.parent.setState({flag: false}); expect(wrapper.refs.parent.state.flag).toEqual(false); - reactComponentExpect(wrapper.refs.child).scalarContextEqual({flag: false}); + expect(wrapper.refs.child.context).toEqual({flag: false}); }); it('should pass context transitively', () => { @@ -780,8 +763,8 @@ describe('ReactCompositeComponent', () => { } ReactTestUtils.renderIntoDocument(); - reactComponentExpect(childInstance).scalarContextEqual({foo: 'bar', depth: 0}); - reactComponentExpect(grandchildInstance).scalarContextEqual({foo: 'bar', depth: 1}); + expect(childInstance.context).toEqual({foo: 'bar', depth: 0}); + expect(grandchildInstance.context).toEqual({foo: 'bar', depth: 1}); }); it('should pass context when re-rendered', () => { @@ -835,7 +818,7 @@ describe('ReactCompositeComponent', () => { }); expect(parentInstance.state.flag).toBe(true); - reactComponentExpect(childInstance).scalarContextEqual({foo: 'bar', depth: 0}); + expect(childInstance.context).toEqual({foo: 'bar', depth: 0}); }); it('unmasked context propagates through updates', () => { diff --git a/src/renderers/shared/stack/reconciler/__tests__/ReactCompositeComponentDOMMinimalism-test.js b/src/renderers/shared/stack/reconciler/__tests__/ReactCompositeComponentDOMMinimalism-test.js index beb58d7f2432..499295f3b295 100644 --- a/src/renderers/shared/stack/reconciler/__tests__/ReactCompositeComponentDOMMinimalism-test.js +++ b/src/renderers/shared/stack/reconciler/__tests__/ReactCompositeComponentDOMMinimalism-test.js @@ -13,8 +13,8 @@ // Requires var React; +var ReactDOM; var ReactTestUtils; -var reactComponentExpect; // Test components var LowerLevelComposite; @@ -30,8 +30,8 @@ var expectSingleChildlessDiv; describe('ReactCompositeComponentDOMMinimalism', () => { beforeEach(() => { - reactComponentExpect = require('reactComponentExpect'); React = require('React'); + ReactDOM = require('ReactDOM'); ReactTestUtils = require('ReactTestUtils'); LowerLevelComposite = class extends React.Component { @@ -55,12 +55,9 @@ describe('ReactCompositeComponentDOMMinimalism', () => { }; expectSingleChildlessDiv = function(instance) { - reactComponentExpect(instance) - .expectRenderedChild() - .toBeCompositeComponentWithType(LowerLevelComposite) - .expectRenderedChild() - .toBeComponentOfType('div') - .toBeDOMComponentWithNoChildren(); + var el = ReactDOM.findDOMNode(instance); + expect(el.tagName).toBe('DIV'); + expect(el.children.length).toBe(0); }; }); @@ -93,15 +90,11 @@ describe('ReactCompositeComponentDOMMinimalism', () => { ); instance = ReactTestUtils.renderIntoDocument(instance); - reactComponentExpect(instance) - .expectRenderedChild() - .toBeCompositeComponentWithType(LowerLevelComposite) - .expectRenderedChild() - .toBeComponentOfType('div') - .toBeDOMComponentWithChildCount(1) - .expectRenderedChildAt(0) - .toBeComponentOfType('ul') - .toBeDOMComponentWithNoChildren(); + var el = ReactDOM.findDOMNode(instance); + expect(el.tagName).toBe('DIV'); + expect(el.children.length).toBe(1); + expect(el.children[0].tagName).toBe('UL'); + expect(el.children[0].children.length).toBe(0); }); }); diff --git a/src/renderers/shared/stack/reconciler/__tests__/ReactEmptyComponent-test.js b/src/renderers/shared/stack/reconciler/__tests__/ReactEmptyComponent-test.js index a2e5076c0822..21b8f41c9481 100644 --- a/src/renderers/shared/stack/reconciler/__tests__/ReactEmptyComponent-test.js +++ b/src/renderers/shared/stack/reconciler/__tests__/ReactEmptyComponent-test.js @@ -16,8 +16,6 @@ var ReactDOM; var ReactTestUtils; var TogglingComponent; -var reactComponentExpect; - var log; describe('ReactEmptyComponent', () => { @@ -28,8 +26,6 @@ describe('ReactEmptyComponent', () => { ReactDOM = require('ReactDOM'); ReactTestUtils = require('ReactTestUtils'); - reactComponentExpect = require('reactComponentExpect'); - log = jasmine.createSpy(); TogglingComponent = class extends React.Component { @@ -51,7 +47,7 @@ describe('ReactEmptyComponent', () => { }; }); - it('should render null and false as a noscript tag under the hood', () => { + it('should not produce child DOM nodes for null and false', () => { class Component1 extends React.Component { render() { return null; @@ -64,14 +60,13 @@ describe('ReactEmptyComponent', () => { } } - var instance1 = ReactTestUtils.renderIntoDocument(); - var instance2 = ReactTestUtils.renderIntoDocument(); - reactComponentExpect(instance1) - .expectRenderedChild() - .toBeEmptyComponent(); - reactComponentExpect(instance2) - .expectRenderedChild() - .toBeEmptyComponent(); + var container1 = document.createElement('div'); + ReactDOM.render(, container1); + expect(container1.children.length).toBe(0); + + var container2 = document.createElement('div'); + ReactDOM.render(, container2); + expect(container2.children.length).toBe(0); }); it('should still throw when rendering to undefined', () => { diff --git a/src/renderers/shared/stack/reconciler/__tests__/refs-test.js b/src/renderers/shared/stack/reconciler/__tests__/refs-test.js index d33a4aeacd76..84faf923fedf 100644 --- a/src/renderers/shared/stack/reconciler/__tests__/refs-test.js +++ b/src/renderers/shared/stack/reconciler/__tests__/refs-test.js @@ -14,9 +14,6 @@ var React = require('React'); var ReactTestUtils = require('ReactTestUtils'); -var reactComponentExpect = require('reactComponentExpect'); - - /** * Counts clicks and has a renders an item for each click. Each item rendered * has a ref of the form "clickLogN". @@ -92,17 +89,13 @@ class TestRefsComponent extends React.Component { var renderTestRefsComponent = function() { var testRefsComponent = ReactTestUtils.renderIntoDocument(); - - reactComponentExpect(testRefsComponent) - .toBeCompositeComponentWithType(TestRefsComponent); + expect(testRefsComponent instanceof TestRefsComponent).toBe(true); var generalContainer = testRefsComponent.refs.myContainer; - var counter = testRefsComponent.refs.myCounter; + expect(generalContainer instanceof GeneralContainerComponent).toBe(true); - reactComponentExpect(generalContainer) - .toBeCompositeComponentWithType(GeneralContainerComponent); - reactComponentExpect(counter) - .toBeCompositeComponentWithType(ClickCounter); + var counter = testRefsComponent.refs.myCounter; + expect(counter instanceof ClickCounter).toBe(true); return testRefsComponent; }; From 264bed13ba638f14bf8f0b3af6ca8dfe0915dcc8 Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Sun, 30 Oct 2016 22:15:34 +0000 Subject: [PATCH 189/271] [Fiber] Miscellaneous fixes to get more tests passing (#8151) * Reset current owner when handling an error * Make string ref deletion tests accept null * Relax requirements on ref and render interleaving --- .../shared/fiber/ReactFiberScheduler.js | 2 ++ .../__tests__/ReactComponent-test.js | 23 +++++++++++++----- .../__tests__/refs-destruction-test.js | 24 +++++++++++++++---- 3 files changed, 39 insertions(+), 10 deletions(-) diff --git a/src/renderers/shared/fiber/ReactFiberScheduler.js b/src/renderers/shared/fiber/ReactFiberScheduler.js index d861c81cd17d..4c7b0c515422 100644 --- a/src/renderers/shared/fiber/ReactFiberScheduler.js +++ b/src/renderers/shared/fiber/ReactFiberScheduler.js @@ -555,6 +555,8 @@ module.exports = function(config : HostConfig) { }); } + ReactCurrentOwner.current = null; + // Surface the first error uncaught by the boundaries to the user. if (firstUncaughtError) { // We need to make sure any future root can get scheduled despite these errors. diff --git a/src/renderers/shared/stack/reconciler/__tests__/ReactComponent-test.js b/src/renderers/shared/stack/reconciler/__tests__/ReactComponent-test.js index 769b2bafcc30..6043979b0c14 100644 --- a/src/renderers/shared/stack/reconciler/__tests__/ReactComponent-test.js +++ b/src/renderers/shared/stack/reconciler/__tests__/ReactComponent-test.js @@ -13,12 +13,14 @@ var React; var ReactDOM; +var ReactDOMFeatureFlags; var ReactTestUtils; describe('ReactComponent', () => { beforeEach(() => { React = require('React'); ReactDOM = require('ReactDOM'); + ReactDOMFeatureFlags = require('ReactDOMFeatureFlags'); ReactTestUtils = require('ReactTestUtils'); }); @@ -195,7 +197,7 @@ describe('ReactComponent', () => { getInner = () => { // (With old-style refs, it's impossible to get a ref to this div // because Wrapper is the current owner when this function is called.) - return
    this.innerRef = c} />; + return
    this.innerRef = c} />; }; render() { @@ -211,7 +213,7 @@ describe('ReactComponent', () => { componentDidMount() { // Check .props.title to make sure we got the right elements back expect(this.wrapperRef.getTitle()).toBe('wrapper'); - expect(ReactDOM.findDOMNode(this.innerRef).title).toBe('inner'); + expect(ReactDOM.findDOMNode(this.innerRef).className).toBe('inner'); mounted = true; } } @@ -291,10 +293,19 @@ describe('ReactComponent', () => { 'outer componentDidMount', 'start update', // Previous (equivalent) refs get cleared - 'ref 1 got null', - 'inner 1 render', - 'ref 2 got null', - 'inner 2 render', + ...(ReactDOMFeatureFlags.useFiber ? [ + // Fiber renders first, resets refs later + 'inner 1 render', + 'inner 2 render', + 'ref 1 got null', + 'ref 2 got null', + ] : [ + // Stack resets refs before rendering + 'ref 1 got null', + 'inner 1 render', + 'ref 2 got null', + 'inner 2 render', + ]), 'inner 1 componentDidUpdate', 'ref 1 got instance 1', 'inner 2 componentDidUpdate', diff --git a/src/renderers/shared/stack/reconciler/__tests__/refs-destruction-test.js b/src/renderers/shared/stack/reconciler/__tests__/refs-destruction-test.js index 1fdc948a2793..b75b25c8bc58 100644 --- a/src/renderers/shared/stack/reconciler/__tests__/refs-destruction-test.js +++ b/src/renderers/shared/stack/reconciler/__tests__/refs-destruction-test.js @@ -45,9 +45,17 @@ describe('refs-destruction', () => { var testInstance = ReactDOM.render(, container); expect(ReactTestUtils.isDOMComponent(testInstance.refs.theInnerDiv)) .toBe(true); - expect(Object.keys(testInstance.refs || {}).length).toEqual(1); + expect( + Object.keys(testInstance.refs || {}) + .filter(key => testInstance.refs[key]) + .length + ).toEqual(1); ReactDOM.unmountComponentAtNode(container); - expect(Object.keys(testInstance.refs || {}).length).toEqual(0); + expect( + Object.keys(testInstance.refs || {}) + .filter(key => testInstance.refs[key]) + .length + ).toEqual(0); }); it('should remove refs when destroying the child', () => { @@ -55,9 +63,17 @@ describe('refs-destruction', () => { var testInstance = ReactDOM.render(, container); expect(ReactTestUtils.isDOMComponent(testInstance.refs.theInnerDiv)) .toBe(true); - expect(Object.keys(testInstance.refs || {}).length).toEqual(1); + expect( + Object.keys(testInstance.refs || {}) + .filter(key => testInstance.refs[key]) + .length + ).toEqual(1); ReactDOM.render(, container); - expect(Object.keys(testInstance.refs || {}).length).toEqual(0); + expect( + Object.keys(testInstance.refs || {}) + .filter(key => testInstance.refs[key]) + .length + ).toEqual(0); }); it('should not error when destroying child with ref asynchronously', () => { From 980cb01a845d92656776fc95ff60a1225f6b3406 Mon Sep 17 00:00:00 2001 From: bel3atar Date: Mon, 31 Oct 2016 11:41:43 +0000 Subject: [PATCH 190/271] add missing verb (#8139) `why is an` should be `why it is an` --- docs/tutorial/tutorial.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/tutorial/tutorial.md b/docs/tutorial/tutorial.md index 180ba05ed0b4..75e21aeb1781 100644 --- a/docs/tutorial/tutorial.md +++ b/docs/tutorial/tutorial.md @@ -204,7 +204,7 @@ Square no longer keeps its own state; it receives its value from its parent `Boa ## Why Immutability Is Important -In the previous code example, I suggest using the `.slice()` operator to copy the `squares` array prior to making changes and to prevent mutating the existing array. Let's talk about what this means and why it an important concept to learn. +In the previous code example, I suggest using the `.slice()` operator to copy the `squares` array prior to making changes and to prevent mutating the existing array. Let's talk about what this means and why it is an important concept to learn. There are generally two ways for changing data. The first, and most common method in past, has been to *mutate* the data by directly changing the values of a variable. The second method is to replace the data with a new copy of the object that also includes desired changes. From abf37ea4841f84e7f8b0c2af9e5477ca37f240d8 Mon Sep 17 00:00:00 2001 From: Skasi Date: Mon, 31 Oct 2016 12:42:02 +0100 Subject: [PATCH 191/271] Remove duplicated word in doc (#8157) Gets rid of an obsolete word in the documentation for "State and Lifecycle": "Consider the ticking clock example from the one of the previous sections." -> "Consider the ticking clock example from one of the previous sections." --- docs/docs/state-and-lifecycle.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/docs/state-and-lifecycle.md b/docs/docs/state-and-lifecycle.md index c957c7faaf24..ab585aff0356 100644 --- a/docs/docs/state-and-lifecycle.md +++ b/docs/docs/state-and-lifecycle.md @@ -7,7 +7,7 @@ prev: components-and-props.html next: handling-events.html --- -Consider the ticking clock example from the [one of the previous sections](/react/docs/rendering-elements.html#updating-the-rendered-element). +Consider the ticking clock example from [one of the previous sections](/react/docs/rendering-elements.html#updating-the-rendered-element). So far we have only learned one way to update the UI. From f53854424b33692907234fe7a1f80b888fd80751 Mon Sep 17 00:00:00 2001 From: Damien Soulard Date: Mon, 31 Oct 2016 14:01:06 +0100 Subject: [PATCH 192/271] update-unknown-warning-page - add a reason for the warning (#8131) * update-unknown-warning-page - add a reason for this warning * Minor tweaks --- docs/warnings/unknown-prop.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/warnings/unknown-prop.md b/docs/warnings/unknown-prop.md index 9f030a82003c..eb7585f650d1 100644 --- a/docs/warnings/unknown-prop.md +++ b/docs/warnings/unknown-prop.md @@ -13,6 +13,8 @@ There are a couple of likely reasons this warning could be appearing: 3. React does not yet recognize the attribute you specified. This will likely be fixed in a future version of React. However, React currently strips all unknown attributes, so specifying them in your React app will not cause them to be rendered. +4. You are using a React component without an upper case. React interprets it as a DOM tag because [React JSX transform uses the upper vs. lower case convention to distinguish between user-defined components and DOM tags](/react/docs/jsx-in-depth.html#user-defined-components-must-be-capitalized). + --- To fix this, composite components should "consume" any prop that is intended for the composite component and not intended for the child component. Example: From 34fd4f4aa0225e5e23a2973ce7786d61a3b25bd4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Markb=C3=A5ge?= Date: Mon, 31 Oct 2016 16:45:01 -0700 Subject: [PATCH 193/271] Kill ReactLink, LinkedStateMixin, valueLink and checkedLink (#8165) This should be safe because we've been warning for this. The LinkedStateMixin is technically exposed on React.addons without a warning but presumably you wouldn't be using it without a valueLink or checkedLink. I do this primarily to clear up what the custom onChange listeners are doing. Renamed the final prop type helper to ReactControlledValuePropTypes. --- src/addons/ReactWithAddons.js | 2 - src/addons/link/LinkedStateMixin.js | 38 ----- src/addons/link/ReactLink.js | 73 -------- .../link/__tests__/LinkedStateMixin-test.js | 46 ----- .../link/__tests__/ReactLinkPropTypes-test.js | 159 ------------------ .../hooks/ReactDOMUnknownPropertyHook.js | 2 - ...ls.js => ReactControlledValuePropTypes.js} | 74 +------- .../wrappers/__tests__/ReactDOMInput-test.js | 137 --------------- .../wrappers/__tests__/ReactDOMSelect-test.js | 36 ---- .../__tests__/ReactDOMTextarea-test.js | 25 --- .../stack/client/wrappers/ReactDOMInput.js | 32 +--- .../stack/client/wrappers/ReactDOMSelect.js | 24 +-- .../stack/client/wrappers/ReactDOMTextarea.js | 21 +-- 13 files changed, 29 insertions(+), 640 deletions(-) delete mode 100644 src/addons/link/LinkedStateMixin.js delete mode 100644 src/addons/link/ReactLink.js delete mode 100644 src/addons/link/__tests__/LinkedStateMixin-test.js delete mode 100644 src/addons/link/__tests__/ReactLinkPropTypes-test.js rename src/renderers/dom/shared/utils/{LinkedValueUtils.js => ReactControlledValuePropTypes.js} (53%) diff --git a/src/addons/ReactWithAddons.js b/src/addons/ReactWithAddons.js index 4d556e922348..ae6890a3f824 100644 --- a/src/addons/ReactWithAddons.js +++ b/src/addons/ReactWithAddons.js @@ -11,7 +11,6 @@ 'use strict'; -var LinkedStateMixin = require('LinkedStateMixin'); var React = require('React'); var ReactAddonsDOMDependencies = require('ReactAddonsDOMDependencies'); var ReactComponentWithPureRenderMixin = @@ -25,7 +24,6 @@ var update = require('update'); React.addons = { CSSTransitionGroup: ReactCSSTransitionGroup, - LinkedStateMixin: LinkedStateMixin, PureRenderMixin: ReactComponentWithPureRenderMixin, TransitionGroup: ReactTransitionGroup, diff --git a/src/addons/link/LinkedStateMixin.js b/src/addons/link/LinkedStateMixin.js deleted file mode 100644 index 9e446f48364a..000000000000 --- a/src/addons/link/LinkedStateMixin.js +++ /dev/null @@ -1,38 +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 LinkedStateMixin - */ - -'use strict'; - -var ReactLink = require('ReactLink'); -var ReactStateSetters = require('ReactStateSetters'); - -/** - * A simple mixin around ReactLink.forState(). - * See https://facebook.github.io/react/docs/two-way-binding-helpers.html - */ -var LinkedStateMixin = { - /** - * Create a ReactLink that's linked to part of this component's state. The - * ReactLink will have the current value of this.state[key] and will call - * setState() when a change is requested. - * - * @param {string} key state key to update. - * @return {ReactLink} ReactLink instance linking to the state. - */ - linkState: function(key) { - return new ReactLink( - this.state[key], - ReactStateSetters.createStateKeySetter(this, key) - ); - }, -}; - -module.exports = LinkedStateMixin; diff --git a/src/addons/link/ReactLink.js b/src/addons/link/ReactLink.js deleted file mode 100644 index d327b4f8ecb8..000000000000 --- a/src/addons/link/ReactLink.js +++ /dev/null @@ -1,73 +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 ReactLink - */ - -'use strict'; - -/** - * ReactLink encapsulates a common pattern in which a component wants to modify - * a prop received from its parent. ReactLink allows the parent to pass down a - * value coupled with a callback that, when invoked, expresses an intent to - * modify that value. For example: - * - * React.createClass({ - * getInitialState: function() { - * return {value: ''}; - * }, - * render: function() { - * var valueLink = new ReactLink(this.state.value, this._handleValueChange); - * return ; - * }, - * _handleValueChange: function(newValue) { - * this.setState({value: newValue}); - * } - * }); - * - * We have provided some sugary mixins to make the creation and - * consumption of ReactLink easier; see LinkedValueUtils and LinkedStateMixin. - */ - -var React = require('React'); - -/** - * Deprecated: An an easy way to express two-way binding with React. - * See https://facebook.github.io/react/docs/two-way-binding-helpers.html - * - * @param {*} value current value of the link - * @param {function} requestChange callback to request a change - */ -function ReactLink(value, requestChange) { - this.value = value; - this.requestChange = requestChange; -} - -/** - * Creates a PropType that enforces the ReactLink API and optionally checks the - * type of the value being passed inside the link. Example: - * - * MyComponent.propTypes = { - * tabIndexLink: ReactLink.PropTypes.link(React.PropTypes.number) - * } - */ -function createLinkTypeChecker(linkType) { - var shapes = { - value: linkType === undefined ? - React.PropTypes.any.isRequired : - linkType.isRequired, - requestChange: React.PropTypes.func.isRequired, - }; - return React.PropTypes.shape(shapes); -} - -ReactLink.PropTypes = { - link: createLinkTypeChecker, -}; - -module.exports = ReactLink; diff --git a/src/addons/link/__tests__/LinkedStateMixin-test.js b/src/addons/link/__tests__/LinkedStateMixin-test.js deleted file mode 100644 index 1d6fa18aff85..000000000000 --- a/src/addons/link/__tests__/LinkedStateMixin-test.js +++ /dev/null @@ -1,46 +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('LinkedStateMixin', () => { - var LinkedStateMixin; - var React; - var ReactTestUtils; - - beforeEach(() => { - LinkedStateMixin = require('LinkedStateMixin'); - React = require('React'); - ReactTestUtils = require('ReactTestUtils'); - }); - - it('should create a ReactLink for state', () => { - var Component = React.createClass({ - mixins: [LinkedStateMixin], - - getInitialState: function() { - return {value: 'initial value'}; - }, - - render: function() { - return value is {this.state.value}; - }, - }); - var component = ReactTestUtils.renderIntoDocument(); - var link = component.linkState('value'); - expect(component.state.value).toBe('initial value'); - expect(link.value).toBe('initial value'); - link.requestChange('new value'); - expect(component.state.value).toBe('new value'); - expect(component.linkState('value').value).toBe('new value'); - }); -}); diff --git a/src/addons/link/__tests__/ReactLinkPropTypes-test.js b/src/addons/link/__tests__/ReactLinkPropTypes-test.js deleted file mode 100644 index d4e3a9e5cb36..000000000000 --- a/src/addons/link/__tests__/ReactLinkPropTypes-test.js +++ /dev/null @@ -1,159 +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 emptyFunction = require('emptyFunction'); -var LinkPropTypes = require('ReactLink').PropTypes; -var React = require('React'); - -var invalidMessage = 'Invalid prop `testProp` supplied to `testComponent`.'; -var requiredMessage = 'The prop `testProp` is marked as required in ' + - '`testComponent`, but its value is `undefined`.'; - -function typeCheckFail(declaration, value, message) { - var props = {testProp: value}; - var error = declaration( - props, - 'testProp', - 'testComponent', - 'prop', - ); - expect(error instanceof Error).toBe(true); - expect(error.message).toBe(message); -} - -function typeCheckPass(declaration, value) { - var props = {testProp: value}; - var error = declaration( - props, - 'testProp', - 'testComponent', - 'prop', - ); - expect(error).toBe(null); -} - -describe('ReactLink', () => { - it('should fail if the argument does not implement the Link API', () => { - typeCheckFail( - LinkPropTypes.link(React.PropTypes.any), - {}, - 'The prop `testProp.value` is marked as required in `testComponent`, ' + - 'but its value is `undefined`.' - ); - typeCheckFail( - LinkPropTypes.link(React.PropTypes.any), - {value: 123}, - 'The prop `testProp.requestChange` is marked as required in ' + - '`testComponent`, but its value is `undefined`.' - ); - typeCheckFail( - LinkPropTypes.link(React.PropTypes.any), - {requestChange: emptyFunction}, - 'The prop `testProp.value` is marked as required in `testComponent`, ' + - 'but its value is `undefined`.' - ); - typeCheckFail( - LinkPropTypes.link(React.PropTypes.any), - {value: null, requestChange: null}, - 'The prop `testProp.value` is marked as required in `testComponent`, ' + - 'but its value is `null`.' - ); - }); - - it('should allow valid links even if no type was specified', () => { - typeCheckPass( - LinkPropTypes.link(), - {value: 42, requestChange: emptyFunction} - ); - typeCheckPass( - LinkPropTypes.link(), - {value: {}, requestChange: emptyFunction, - }); - }); - - it('should allow no link to be passed at all', () => { - typeCheckPass( - LinkPropTypes.link(React.PropTypes.string), - undefined - ); - }); - - it('should allow valid links with correct value format', () => { - typeCheckPass( - LinkPropTypes.link(React.PropTypes.any), - {value: 42, requestChange: emptyFunction} - ); - typeCheckPass( - LinkPropTypes.link(React.PropTypes.number), - {value: 42, requestChange: emptyFunction} - ); - typeCheckPass( - LinkPropTypes.link(React.PropTypes.node), - {value: 42, requestChange: emptyFunction} - ); - }); - - it('should fail if the link`s value type does not match', () => { - typeCheckFail( - LinkPropTypes.link(React.PropTypes.string), - {value: 123, requestChange: emptyFunction}, - 'Invalid prop `testProp.value` of type `number` supplied to `testComponent`,' + - ' expected `string`.' - ); - }); - - it('should be implicitly optional and not warn without values', () => { - typeCheckPass(LinkPropTypes.link(), null); - typeCheckPass(LinkPropTypes.link(), undefined); - typeCheckPass(LinkPropTypes.link(React.PropTypes.string), null); - typeCheckPass(LinkPropTypes.link(React.PropTypes.string), undefined); - }); - - it('should warn for missing required values', () => { - var specifiedButIsNullMsg = 'The prop `testProp` is marked as required ' + - 'in `testComponent`, but its value is `null`.'; - typeCheckFail(LinkPropTypes.link().isRequired, null, specifiedButIsNullMsg); - typeCheckFail(LinkPropTypes.link().isRequired, undefined, requiredMessage); - typeCheckFail( - LinkPropTypes.link(React.PropTypes.string).isRequired, - null, - specifiedButIsNullMsg - ); - typeCheckFail( - LinkPropTypes.link(React.PropTypes.string).isRequired, - undefined, - requiredMessage - ); - }); - - it('should be compatible with React.PropTypes.oneOfType', () => { - typeCheckPass( - React.PropTypes.oneOfType([LinkPropTypes.link(React.PropTypes.number)]), - {value: 123, requestChange: emptyFunction} - ); - typeCheckFail( - React.PropTypes.oneOfType([LinkPropTypes.link(React.PropTypes.number)]), - 123, - invalidMessage - ); - typeCheckPass( - LinkPropTypes.link(React.PropTypes.oneOfType([React.PropTypes.number])), - {value: 123, requestChange: emptyFunction} - ); - typeCheckFail( - LinkPropTypes.link(React.PropTypes.oneOfType([React.PropTypes.number])), - {value: 'imastring', requestChange: emptyFunction}, - 'Invalid prop `testProp.value` supplied to `testComponent`.' - ); - }); -}); diff --git a/src/renderers/dom/shared/hooks/ReactDOMUnknownPropertyHook.js b/src/renderers/dom/shared/hooks/ReactDOMUnknownPropertyHook.js index ef0937a7fc55..5f409cddb376 100644 --- a/src/renderers/dom/shared/hooks/ReactDOMUnknownPropertyHook.js +++ b/src/renderers/dom/shared/hooks/ReactDOMUnknownPropertyHook.js @@ -26,9 +26,7 @@ if (__DEV__) { autoFocus: true, defaultValue: true, - valueLink: true, defaultChecked: true, - checkedLink: true, innerHTML: true, suppressContentEditableWarning: true, onFocusIn: true, diff --git a/src/renderers/dom/shared/utils/LinkedValueUtils.js b/src/renderers/dom/shared/utils/ReactControlledValuePropTypes.js similarity index 53% rename from src/renderers/dom/shared/utils/LinkedValueUtils.js rename to src/renderers/dom/shared/utils/ReactControlledValuePropTypes.js index c2143827ad10..9d83c6a88828 100644 --- a/src/renderers/dom/shared/utils/LinkedValueUtils.js +++ b/src/renderers/dom/shared/utils/ReactControlledValuePropTypes.js @@ -6,7 +6,7 @@ * 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 LinkedValueUtils + * @providesModule ReactControlledValuePropTypes */ 'use strict'; @@ -14,7 +14,6 @@ var React = require('React'); var getComponentName = require('getComponentName'); -var invariant = require('invariant'); var warning = require('warning'); var hasReadOnlyValue = { @@ -27,32 +26,6 @@ var hasReadOnlyValue = { 'submit': true, }; -function _assertSingleLink(inputProps) { - invariant( - inputProps.checkedLink == null || inputProps.valueLink == null, - 'Cannot provide a checkedLink and a valueLink. If you want to use ' + - 'checkedLink, you probably don\'t want to use valueLink and vice versa.' - ); -} -function _assertValueLink(inputProps) { - _assertSingleLink(inputProps); - invariant( - inputProps.value == null && inputProps.onChange == null, - 'Cannot provide a valueLink and a value or onChange event. If you want ' + - 'to use value or onChange, you probably don\'t want to use valueLink.' - ); -} - -function _assertCheckedLink(inputProps) { - _assertSingleLink(inputProps); - invariant( - inputProps.checked == null && inputProps.onChange == null, - 'Cannot provide a checkedLink and a checked property or onChange event. ' + - 'If you want to use checked or onChange, you probably don\'t want to ' + - 'use checkedLink' - ); -} - var propTypes = { value: function(props, propName, componentName) { if (!props[propName] || @@ -101,7 +74,7 @@ function getDeclarationErrorAddendum(owner) { * Provide a linked `value` attribute for controlled forms. You should not use * this outside of the ReactDOM controlled form components. */ -var LinkedValueUtils = { +var ReactControlledValuePropTypes = { checkPropTypes: function(tagName, props, owner) { for (var propName in propTypes) { if (propTypes.hasOwnProperty(propName)) { @@ -122,47 +95,6 @@ var LinkedValueUtils = { } } }, - - /** - * @param {object} inputProps Props for form component - * @return {*} current value of the input either from value prop or link. - */ - getValue: function(inputProps) { - if (inputProps.valueLink) { - _assertValueLink(inputProps); - return inputProps.valueLink.value; - } - return inputProps.value; - }, - - /** - * @param {object} inputProps Props for form component - * @return {*} current checked status of the input either from checked prop - * or link. - */ - getChecked: function(inputProps) { - if (inputProps.checkedLink) { - _assertCheckedLink(inputProps); - return inputProps.checkedLink.value; - } - return inputProps.checked; - }, - - /** - * @param {object} inputProps Props for form component - * @param {SyntheticEvent} event change event to handle - */ - executeOnChange: function(inputProps, event) { - if (inputProps.valueLink) { - _assertValueLink(inputProps); - return inputProps.valueLink.requestChange(event.target.value); - } else if (inputProps.checkedLink) { - _assertCheckedLink(inputProps); - return inputProps.checkedLink.requestChange(event.target.checked); - } else if (inputProps.onChange) { - return inputProps.onChange.call(undefined, event); - } - }, }; -module.exports = LinkedValueUtils; +module.exports = ReactControlledValuePropTypes; diff --git a/src/renderers/dom/shared/wrappers/__tests__/ReactDOMInput-test.js b/src/renderers/dom/shared/wrappers/__tests__/ReactDOMInput-test.js index a09d63b0608d..6e76c795e65a 100644 --- a/src/renderers/dom/shared/wrappers/__tests__/ReactDOMInput-test.js +++ b/src/renderers/dom/shared/wrappers/__tests__/ReactDOMInput-test.js @@ -19,7 +19,6 @@ describe('ReactDOMInput', () => { var ReactDOM; var ReactDOMServer; var ReactDOMFeatureFlags; - var ReactLink; var ReactTestUtils; var inputValueTracking; @@ -36,7 +35,6 @@ describe('ReactDOMInput', () => { ReactDOM = require('ReactDOM'); ReactDOMServer = require('ReactDOMServer'); ReactDOMFeatureFlags = require('ReactDOMFeatureFlags'); - ReactLink = require('ReactLink'); ReactTestUtils = require('ReactTestUtils'); inputValueTracking = require('inputValueTracking'); spyOn(console, 'error'); @@ -373,39 +371,6 @@ describe('ReactDOMInput', () => { expect(cNode.checked).toBe(true); }); - it('should support ReactLink', () => { - var link = new ReactLink('yolo', jest.fn()); - var instance = ; - - instance = ReactTestUtils.renderIntoDocument(instance); - - expect(ReactDOM.findDOMNode(instance).value).toBe('yolo'); - expect(link.value).toBe('yolo'); - expect(link.requestChange.mock.calls.length).toBe(0); - - ReactDOM.findDOMNode(instance).value = 'test'; - ReactTestUtils.Simulate.change(ReactDOM.findDOMNode(instance)); - - expect(link.requestChange.mock.calls.length).toBe(1); - expect(link.requestChange.mock.calls[0][0]).toEqual('test'); - }); - - it('should warn with value and no onChange handler', () => { - var link = new ReactLink('yolo', jest.fn()); - ReactTestUtils.renderIntoDocument(); - expect(console.error.calls.count()).toBe(1); - expect(console.error.calls.argsFor(0)[0]).toContain( - '`valueLink` prop on `input` is deprecated; set `value` and `onChange` instead.' - ); - - ReactTestUtils.renderIntoDocument( - - ); - expect(console.error.calls.count()).toBe(1); - ReactTestUtils.renderIntoDocument(); - expect(console.error.calls.count()).toBe(2); - }); - it('should warn with value and no onChange handler and readOnly specified', () => { ReactTestUtils.renderIntoDocument( @@ -429,71 +394,6 @@ describe('ReactDOMInput', () => { ReactTestUtils.Simulate.change(instance); }); - it('should throw if both value and valueLink are provided', () => { - var node = document.createElement('div'); - var link = new ReactLink('yolo', jest.fn()); - var instance = ; - - expect(() => ReactDOM.render(instance, node)).not.toThrow(); - - instance = - ; - expect(() => ReactDOM.render(instance, node)).toThrow(); - - instance = ; - expect(() => ReactDOM.render(instance, node)).toThrow(); - - }); - - it('should support checkedLink', () => { - var link = new ReactLink(true, jest.fn()); - var instance = ; - - instance = ReactTestUtils.renderIntoDocument(instance); - - expect(ReactDOM.findDOMNode(instance).checked).toBe(true); - expect(link.value).toBe(true); - expect(link.requestChange.mock.calls.length).toBe(0); - - ReactDOM.findDOMNode(instance).checked = false; - ReactTestUtils.Simulate.change(ReactDOM.findDOMNode(instance)); - - expect(link.requestChange.mock.calls.length).toBe(1); - expect(link.requestChange.mock.calls[0][0]).toEqual(false); - }); - - it('should warn with checked and no onChange handler', () => { - var node = document.createElement('div'); - var link = new ReactLink(true, jest.fn()); - ReactDOM.render(, node); - expect(console.error.calls.count()).toBe(1); - expect(console.error.calls.argsFor(0)[0]).toContain( - '`checkedLink` prop on `input` is deprecated; set `value` and `onChange` instead.' - ); - - ReactTestUtils.renderIntoDocument( - - ); - expect(console.error.calls.count()).toBe(1); - - ReactTestUtils.renderIntoDocument( - - ); - expect(console.error.calls.count()).toBe(1); - - ReactTestUtils.renderIntoDocument(); - expect(console.error.calls.count()).toBe(2); - }); - it('should warn with checked and no onChange handler with readOnly specified', () => { ReactTestUtils.renderIntoDocument( @@ -506,28 +406,6 @@ describe('ReactDOMInput', () => { expect(console.error.calls.count()).toBe(1); }); - it('should throw if both checked and checkedLink are provided', () => { - var node = document.createElement('div'); - var link = new ReactLink(true, jest.fn()); - var instance = ; - - expect(() => ReactDOM.render(instance, node)).not.toThrow(); - - instance = - ; - expect(() => ReactDOM.render(instance, node)).toThrow(); - - instance = - ; - expect(() => ReactDOM.render(instance, node)).toThrow(); - - }); - it('should update defaultValue to empty string', () => { var container = document.createElement('div'); ReactDOM.render(, container); @@ -535,21 +413,6 @@ describe('ReactDOMInput', () => { expect(container.firstChild.defaultValue).toBe(''); }); - it('should throw if both checkedLink and valueLink are provided', () => { - var node = document.createElement('div'); - var link = new ReactLink(true, jest.fn()); - var instance = ; - - expect(() => ReactDOM.render(instance, node)).not.toThrow(); - - instance = ; - expect(() => ReactDOM.render(instance, node)).not.toThrow(); - - instance = - ; - expect(() => ReactDOM.render(instance, node)).toThrow(); - }); - it('should warn if value is null', () => { ReactTestUtils.renderIntoDocument(); expect(console.error.calls.argsFor(0)[0]).toContain( diff --git a/src/renderers/dom/shared/wrappers/__tests__/ReactDOMSelect-test.js b/src/renderers/dom/shared/wrappers/__tests__/ReactDOMSelect-test.js index 816d2b57836e..784953990375 100644 --- a/src/renderers/dom/shared/wrappers/__tests__/ReactDOMSelect-test.js +++ b/src/renderers/dom/shared/wrappers/__tests__/ReactDOMSelect-test.js @@ -16,7 +16,6 @@ describe('ReactDOMSelect', () => { var React; var ReactDOM; var ReactDOMServer; - var ReactLink; var ReactTestUtils; var noop = function() {}; @@ -25,7 +24,6 @@ describe('ReactDOMSelect', () => { React = require('React'); ReactDOM = require('ReactDOM'); ReactDOMServer = require('ReactDOMServer'); - ReactLink = require('ReactLink'); ReactTestUtils = require('ReactTestUtils'); }); @@ -347,40 +345,6 @@ describe('ReactDOMSelect', () => { expect(node.options[2].selected).toBe(true); // gorilla }); - it('should support ReactLink', () => { - var link = new ReactLink('giraffe', jest.fn()); - var stub = - ; - - spyOn(console, 'error'); - - stub = ReactTestUtils.renderIntoDocument(stub); - - expect(console.error.calls.count()).toBe(1); - expect(console.error.calls.argsFor(0)[0]).toContain( - '`valueLink` prop on `select` is deprecated; set `value` and `onChange` instead.' - ); - - var node = ReactDOM.findDOMNode(stub); - - expect(node.options[0].selected).toBe(false); // monkey - expect(node.options[1].selected).toBe(true); // giraffe - expect(node.options[2].selected).toBe(false); // gorilla - expect(link.requestChange.mock.calls.length).toBe(0); - - node.options[1].selected = false; - node.options[2].selected = true; - ReactTestUtils.Simulate.change(node); - - expect(link.requestChange.mock.calls.length).toBe(1); - expect(link.requestChange.mock.calls[0][0]).toEqual('gorilla'); - - }); - it('should support server-side rendering', () => { var stub = -``` - -For HTML, this easily allows developers to supply multiline values. However, since React is JavaScript, we do not have string limitations and can use `\n` if we want newlines. In a world where we have `value` and `defaultValue`, it is ambiguous what role children play. For this reason, you should not use children when setting ` ``` -If you *do* decide to use children, they will behave like `defaultValue`. +In React, a `