diff --git a/scripts/fiber/tests-passing.txt b/scripts/fiber/tests-passing.txt
index 5c3d8a1f88..78b38179ba 100644
--- a/scripts/fiber/tests-passing.txt
+++ b/scripts/fiber/tests-passing.txt
@@ -466,6 +466,8 @@ src/renderers/__tests__/ReactComponent-test.js
* fires the callback after a component is rendered
* throws usefully when rendering badly-typed elements
* includes owner name in the error about badly-typed elements
+* throws if a plain object is used as a child
+* throws if a plain object even if it is in an owner
src/renderers/__tests__/ReactComponentLifeCycle-test.js
* should not reuse an instance when it has been unmounted
@@ -526,6 +528,8 @@ src/renderers/__tests__/ReactCompositeComponent-test.js
* should warn when mutated props are passed
* should only call componentWillUnmount once
* prepares new child before unmounting old
+* respects a shallow shouldComponentUpdate implementation
+* does not do a deep comparison for a shallow shouldComponentUpdate implementation
src/renderers/__tests__/ReactCompositeComponentDOMMinimalism-test.js
* should not render extra nodes for non-interpolated text
@@ -1151,6 +1155,15 @@ src/renderers/dom/shared/__tests__/quoteAttributeValueForBrowser-test.js
* should escape number to string
* should escape string
+src/renderers/dom/shared/__tests__/renderSubtreeIntoContainer-test.js
+* should pass context when rendering subtree elsewhere
+* should throw if parentComponent is invalid
+* should update context if it changes due to setState
+* should update context if it changes due to re-render
+* should render portal with non-context-provider parent
+* should get context through non-context-provider parent
+* should get context through middle non-context-provider layer
+
src/renderers/dom/shared/__tests__/validateDOMNesting-test.js
* allows any tag with no context
* allows valid nestings
diff --git a/src/renderers/__tests__/ReactComponent-test.js b/src/renderers/__tests__/ReactComponent-test.js
index b909c8ab10..50dcec84ef 100644
--- a/src/renderers/__tests__/ReactComponent-test.js
+++ b/src/renderers/__tests__/ReactComponent-test.js
@@ -380,4 +380,38 @@ describe('ReactComponent', () => {
// One warning for each element creation
expectDev(console.error.calls.count()).toBe(1);
});
+
+ it('throws if a plain object is used as a child', () => {
+ var children = {
+ x: ,
+ y: ,
+ z: ,
+ };
+ var element =
{[children]}
;
+ var container = document.createElement('div');
+ expect(() => ReactDOM.render(element, container)).toThrowError(
+ 'Objects are not valid as a React child (found: object with keys ' +
+ '{x, y, z}). If you meant to render a collection of children, use an ' +
+ 'array instead.',
+ );
+ });
+
+ it('throws if a plain object even if it is in an owner', () => {
+ class Foo extends React.Component {
+ render() {
+ var children = {
+ a: ,
+ b: ,
+ c: ,
+ };
+ return {[children]}
;
+ }
+ }
+ var container = document.createElement('div');
+ expect(() => ReactDOM.render(, container)).toThrowError(
+ 'Objects are not valid as a React child (found: object with keys ' +
+ '{a, b, c}). If you meant to render a collection of children, use an ' +
+ 'array instead.\n\nCheck the render method of `Foo`.',
+ );
+ });
});
diff --git a/src/renderers/__tests__/ReactCompositeComponent-test.js b/src/renderers/__tests__/ReactCompositeComponent-test.js
index 6f82e6f01a..7259ec57b2 100644
--- a/src/renderers/__tests__/ReactCompositeComponent-test.js
+++ b/src/renderers/__tests__/ReactCompositeComponent-test.js
@@ -20,6 +20,8 @@ var ReactDOMServer;
var ReactCurrentOwner;
var ReactPropTypes;
var ReactTestUtils;
+var shallowEqual;
+var shallowCompare;
describe('ReactCompositeComponent', () => {
beforeEach(() => {
@@ -31,6 +33,12 @@ describe('ReactCompositeComponent', () => {
ReactCurrentOwner = require('react/lib/ReactCurrentOwner');
ReactPropTypes = require('ReactPropTypes');
ReactTestUtils = require('ReactTestUtils');
+ shallowEqual = require('fbjs/lib/shallowEqual');
+
+ shallowCompare = function(instance, nextProps, nextState) {
+ return !shallowEqual(instance.props, nextProps) ||
+ !shallowEqual(instance.state, nextState);
+ };
MorphingComponent = class extends React.Component {
state = {activated: false};
@@ -1396,4 +1404,119 @@ describe('ReactCompositeComponent', () => {
'B componentDidMount',
]);
});
+
+ it('respects a shallow shouldComponentUpdate implementation', () => {
+ var renderCalls = 0;
+ class PlasticWrap extends React.Component {
+ constructor(props, context) {
+ super(props, context);
+ this.state = {
+ color: 'green',
+ };
+ }
+
+ render() {
+ return ;
+ }
+ }
+
+ class Apple extends React.Component {
+ state = {
+ cut: false,
+ slices: 1,
+ };
+
+ shouldComponentUpdate(nextProps, nextState) {
+ return shallowCompare(this, nextProps, nextState);
+ }
+
+ cut() {
+ this.setState({
+ cut: true,
+ slices: 10,
+ });
+ }
+
+ eatSlice() {
+ this.setState({
+ slices: this.state.slices - 1,
+ });
+ }
+
+ render() {
+ renderCalls++;
+ return ;
+ }
+ }
+
+ var container = document.createElement('div');
+ var instance = ReactDOM.render(, container);
+ expect(renderCalls).toBe(1);
+
+ // Do not re-render based on props
+ instance.setState({color: 'green'});
+ expect(renderCalls).toBe(1);
+
+ // Re-render based on props
+ instance.setState({color: 'red'});
+ expect(renderCalls).toBe(2);
+
+ // Re-render base on state
+ instance.refs.apple.cut();
+ expect(renderCalls).toBe(3);
+
+ // No re-render based on state
+ instance.refs.apple.cut();
+ expect(renderCalls).toBe(3);
+
+ // Re-render based on state again
+ instance.refs.apple.eatSlice();
+ expect(renderCalls).toBe(4);
+ });
+
+ it('does not do a deep comparison for a shallow shouldComponentUpdate implementation', () => {
+ function getInitialState() {
+ return {
+ foo: [1, 2, 3],
+ bar: {a: 4, b: 5, c: 6},
+ };
+ }
+
+ var renderCalls = 0;
+ var initialSettings = getInitialState();
+
+ class Component extends React.Component {
+ state = initialSettings;
+
+ shouldComponentUpdate(nextProps, nextState) {
+ return shallowCompare(this, nextProps, nextState);
+ }
+
+ render() {
+ renderCalls++;
+ return ;
+ }
+ }
+
+ var container = document.createElement('div');
+ var instance = ReactDOM.render(, container);
+ expect(renderCalls).toBe(1);
+
+ // Do not re-render if state is equal
+ var settings = {
+ foo: initialSettings.foo,
+ bar: initialSettings.bar,
+ };
+ instance.setState(settings);
+ expect(renderCalls).toBe(1);
+
+ // Re-render because one field changed
+ initialSettings.foo = [1, 2, 3];
+ instance.setState(initialSettings);
+ expect(renderCalls).toBe(2);
+
+ // Re-render because the object changed
+ instance.setState(getInitialState());
+ expect(renderCalls).toBe(3);
+ });
});
diff --git a/src/renderers/dom/shared/__tests__/renderSubtreeIntoContainer-test.js b/src/renderers/dom/shared/__tests__/renderSubtreeIntoContainer-test.js
new file mode 100644
index 0000000000..30e617480b
--- /dev/null
+++ b/src/renderers/dom/shared/__tests__/renderSubtreeIntoContainer-test.js
@@ -0,0 +1,300 @@
+/**
+ * Copyright 2013-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @emails react-core
+ */
+
+'use strict';
+
+var React = require('react');
+var ReactDOM = require('react-dom');
+var ReactTestUtils = require('ReactTestUtils');
+var renderSubtreeIntoContainer = require('renderSubtreeIntoContainer');
+
+describe('renderSubtreeIntoContainer', () => {
+ it('should pass context when rendering subtree elsewhere', () => {
+ var portal = document.createElement('div');
+
+ class Component extends React.Component {
+ static contextTypes = {
+ foo: React.PropTypes.string.isRequired,
+ };
+
+ render() {
+ return {this.context.foo}
;
+ }
+ }
+
+ class Parent extends React.Component {
+ static childContextTypes = {
+ foo: React.PropTypes.string.isRequired,
+ };
+
+ getChildContext() {
+ return {
+ foo: 'bar',
+ };
+ }
+
+ render() {
+ return null;
+ }
+
+ componentDidMount() {
+ expect(
+ function() {
+ renderSubtreeIntoContainer(this, , portal);
+ }.bind(this),
+ ).not.toThrow();
+ }
+ }
+
+ ReactTestUtils.renderIntoDocument();
+ expect(portal.firstChild.innerHTML).toBe('bar');
+ });
+
+ it('should throw if parentComponent is invalid', () => {
+ var portal = document.createElement('div');
+
+ class Component extends React.Component {
+ static contextTypes = {
+ foo: React.PropTypes.string.isRequired,
+ };
+
+ render() {
+ return {this.context.foo}
;
+ }
+ }
+
+ // ESLint is confused here and thinks Parent is unused, presumably because
+ // it is only used inside of the class body?
+ // eslint-disable-next-line no-unused-vars
+ class Parent extends React.Component {
+ static childContextTypes = {
+ foo: React.PropTypes.string.isRequired,
+ };
+
+ getChildContext() {
+ return {
+ foo: 'bar',
+ };
+ }
+
+ render() {
+ return null;
+ }
+
+ componentDidMount() {
+ expect(function() {
+ renderSubtreeIntoContainer(, , portal);
+ }).toThrowError('parentComponentmust be a valid React Component');
+ }
+ }
+ });
+
+ it('should update context if it changes due to setState', () => {
+ var container = document.createElement('div');
+ document.body.appendChild(container);
+ var portal = document.createElement('div');
+
+ class Component extends React.Component {
+ static contextTypes = {
+ foo: React.PropTypes.string.isRequired,
+ getFoo: React.PropTypes.func.isRequired,
+ };
+
+ render() {
+ return {this.context.foo + '-' + this.context.getFoo()}
;
+ }
+ }
+
+ class Parent extends React.Component {
+ static childContextTypes = {
+ foo: React.PropTypes.string.isRequired,
+ getFoo: React.PropTypes.func.isRequired,
+ };
+
+ state = {
+ bar: 'initial',
+ };
+
+ getChildContext() {
+ return {
+ foo: this.state.bar,
+ getFoo: () => this.state.bar,
+ };
+ }
+
+ render() {
+ return null;
+ }
+
+ componentDidMount() {
+ renderSubtreeIntoContainer(this, , portal);
+ }
+
+ componentDidUpdate() {
+ renderSubtreeIntoContainer(this, , portal);
+ }
+ }
+
+ var instance = ReactDOM.render(, container);
+ expect(portal.firstChild.innerHTML).toBe('initial-initial');
+ instance.setState({bar: 'changed'});
+ expect(portal.firstChild.innerHTML).toBe('changed-changed');
+ });
+
+ it('should update context if it changes due to re-render', () => {
+ var container = document.createElement('div');
+ document.body.appendChild(container);
+ var portal = document.createElement('div');
+
+ class Component extends React.Component {
+ static contextTypes = {
+ foo: React.PropTypes.string.isRequired,
+ getFoo: React.PropTypes.func.isRequired,
+ };
+
+ render() {
+ return {this.context.foo + '-' + this.context.getFoo()}
;
+ }
+ }
+
+ class Parent extends React.Component {
+ static childContextTypes = {
+ foo: React.PropTypes.string.isRequired,
+ getFoo: React.PropTypes.func.isRequired,
+ };
+
+ getChildContext() {
+ return {
+ foo: this.props.bar,
+ getFoo: () => this.props.bar,
+ };
+ }
+
+ render() {
+ return null;
+ }
+
+ componentDidMount() {
+ renderSubtreeIntoContainer(this, , portal);
+ }
+
+ componentDidUpdate() {
+ renderSubtreeIntoContainer(this, , portal);
+ }
+ }
+
+ ReactDOM.render(, container);
+ expect(portal.firstChild.innerHTML).toBe('initial-initial');
+ ReactDOM.render(, container);
+ expect(portal.firstChild.innerHTML).toBe('changed-changed');
+ });
+
+ it('should render portal with non-context-provider parent', () => {
+ var container = document.createElement('div');
+ document.body.appendChild(container);
+ var portal = document.createElement('div');
+
+ class Parent extends React.Component {
+ render() {
+ return null;
+ }
+
+ componentDidMount() {
+ renderSubtreeIntoContainer(this, hello
, portal);
+ }
+ }
+
+ ReactDOM.render(, container);
+ expect(portal.firstChild.innerHTML).toBe('hello');
+ });
+
+ it('should get context through non-context-provider parent', () => {
+ var container = document.createElement('div');
+ document.body.appendChild(container);
+ var portal = document.createElement('div');
+
+ class Parent extends React.Component {
+ render() {
+ return ;
+ }
+ getChildContext() {
+ return {value: this.props.value};
+ }
+ static childContextTypes = {
+ value: React.PropTypes.string.isRequired,
+ };
+ }
+
+ class Middle extends React.Component {
+ render() {
+ return null;
+ }
+ componentDidMount() {
+ renderSubtreeIntoContainer(this, , portal);
+ }
+ }
+
+ class Child extends React.Component {
+ static contextTypes = {
+ value: React.PropTypes.string.isRequired,
+ };
+ render() {
+ return {this.context.value}
;
+ }
+ }
+
+ ReactDOM.render(, container);
+ expect(portal.textContent).toBe('foo');
+ });
+
+ it('should get context through middle non-context-provider layer', () => {
+ var container = document.createElement('div');
+ document.body.appendChild(container);
+ var portal1 = document.createElement('div');
+ var portal2 = document.createElement('div');
+
+ class Parent extends React.Component {
+ render() {
+ return null;
+ }
+ getChildContext() {
+ return {value: this.props.value};
+ }
+ componentDidMount() {
+ renderSubtreeIntoContainer(this, , portal1);
+ }
+ static childContextTypes = {
+ value: React.PropTypes.string.isRequired,
+ };
+ }
+
+ class Middle extends React.Component {
+ render() {
+ return null;
+ }
+ componentDidMount() {
+ renderSubtreeIntoContainer(this, , portal2);
+ }
+ }
+
+ class Child extends React.Component {
+ static contextTypes = {
+ value: React.PropTypes.string.isRequired,
+ };
+ render() {
+ return {this.context.value}
;
+ }
+ }
+
+ ReactDOM.render(, container);
+ expect(portal2.textContent).toBe('foo');
+ });
+});