diff --git a/src/core/ReactComponent.js b/src/core/ReactComponent.js index addad00ab5198..3f2ed6aff47a8 100644 --- a/src/core/ReactComponent.js +++ b/src/core/ReactComponent.js @@ -37,6 +37,12 @@ var merge = require('merge'); */ var OWNER = '{owner}'; +/** + * Props key that determines if a component's key was already validated. + * @private + */ +var IS_KEY_VALIDATED = '{is.key.validated}'; + /** * Every React component is in one of these life cycles. */ @@ -57,124 +63,70 @@ var ComponentLifeCycle = keyMirror({ * This allows us to keep track of children between updates. */ -var CHILD_HAS_NO_IDENTITY = - 'Each child in an array should have a unique "key" prop. ' + - 'Check the render method of '; - -var CHILD_CAME_FROM_ANOTHER_OWNER = '. It was passed a child from '; - var ownerHasWarned = {}; /** - * Helpers for flattening child arguments onto a new array or use an existing - * one. - */ - -/** - * Generate a unique key that identifies this child within a set. + * Warn if the component doesn't have an explicit key assigned to it. + * This component is in an array. The array could grow and shrink or be + * reordered. All children, that hasn't already been validated, are required to + * have a "key" property assigned to it. * - * @param {*} Manually provided key. - * @param {number} Index that is used if a manual key is not provided. - * @param {?number} Grouping index if this is used in a nested array. - * @return {string} - */ -function createKey(explicitKey, childIndex, groupingIndex) { - return ReactCurrentOwner.getDepth() + ':' + - (groupingIndex == null ? '' : groupingIndex + ':') + - (explicitKey == null ? '' + childIndex : explicitKey); -} - -/** - * Returns true if this parameter type is considered an empty child slot. - * Used to filter out empty slots and generate a compact array. - * - * @param {*} Child component or any value. - * @return {boolean} + * @internal + * @param {ReactComponent} component Component that requires a key. */ -function isEmptyChild(child) { - return child == null || typeof child === 'boolean'; -} +function validateExplicitKey(component) { + if (component[IS_KEY_VALIDATED] || component.props.key != null) { + return; + } + component[IS_KEY_VALIDATED] = true; -/** - * Assign an internal identity to a child component. - * - * @param {number} Index of the current array grouping. - * @param {*} Child component or any value. - * @param {number} Index of the current child within it's grouping. - */ -function assignKey(groupingIndex, child, index) { - // Only truthy internal keys are valid. If it's not, we assign one. - if (ReactComponent.isValidComponent(child) && !child._key) { - var key = child.props.key; - if (__DEV__) { - // This is in an array. This could grow and shrink or be reordered. - // All children that doesn't already have autogenerated identity needs - // to have an explicit key provided. - if (key == null) { - // Name of the component whose render method tried to pass children. - var currentName = - ReactCurrentOwner.current && - ReactCurrentOwner.current.constructor.displayName; - - // Name of the component that originally created this child. - var childOwnerName = - child.props[OWNER] && child.props[OWNER].constructor.displayName; - - if (currentName && !ownerHasWarned.hasOwnProperty(currentName)) { - ownerHasWarned[currentName] = true; - var message = CHILD_HAS_NO_IDENTITY + currentName; - if (childOwnerName && currentName !== childOwnerName) { - // Usually the current owner is the offender, but if it accepts - // children as a property, it may be the creator of the child that's - // responsible for assigning it a key. - message += CHILD_CAME_FROM_ANOTHER_OWNER + childOwnerName; - } - message += '.'; - console && console.warn && console.warn(message); - } - } - } - child._key = createKey(key, index, groupingIndex); + // We can't provide friendly warnings for top level components. + if (!ReactCurrentOwner.current) { + return; } -} -/** - * Make sure all children have an internal identity. Returns true if this is - * already a compact array. - * - * @param {array} Children of any type. - * @return {boolean} - */ -function tryToReuseArray(children) { - for (var i = 0; i < children.length; i++) { - var child = children[i]; - if (isEmptyChild(child)) { - return false; - } - assignKey(0, child, i); + // Name of the component whose render method tried to pass children. + var currentName = ReactCurrentOwner.current.constructor.displayName; + if (ownerHasWarned.hasOwnProperty(currentName)) { + return; + } + ownerHasWarned[currentName] = true; + + var message = 'Each child in an array should have a unique "key" prop. ' + + 'Check the render method of ' + currentName + '.'; + if (!component.isOwnedBy(ReactCurrentOwner.current)) { + // Name of the component that originally created this child. + var childOwnerName = + component.props[OWNER] && component.props[OWNER].constructor.displayName; + + // Usually the current owner is the offender, but if it accepts + // children as a property, it may be the creator of the child that's + // responsible for assigning it a key. + message += ' It was passed a child from ' + childOwnerName + '.'; } - return true; + + global.console && console.warn && console.warn(message); } /** - * Append children from the source array to the target array. Make sure all - * children have an internal identity assigned to it based on insertion point. + * Ensure that every component either is passed in a static location or, if + * if it's passed in an array, has an explicit key property defined. * - * @param {number} Index of the current array grouping. - * @param {array} Source array. - * @param {array} Target array that will be appended to. + * @internal + * @param {*} component Statically passed child of any type. + * @return {boolean} */ -function appendNestedChildren(groupingIndex, sourceArray, targetArray) { - for (var i = 0; i < sourceArray.length; i++) { - var child = sourceArray[i]; - if (isEmptyChild(child)) { - continue; +function validateChildKeys(component) { + if (Array.isArray(component)) { + for (var i = 0; i < component.length; i++) { + var child = component[i]; + if (ReactComponent.isValidComponent(child)) { + validateExplicitKey(child); + } } - assignKey(groupingIndex, child, i); - // TODO: Invalid components like strings could possibly need - // keys assigned to them here. Usually they're not stateful but - // CSS transitions and special events could make them stateful. - targetArray.push(child); + } else if (ReactComponent.isValidComponent(component)) { + // This component was passed in a valid location. + component[IS_KEY_VALIDATED] = true; } } @@ -218,6 +170,23 @@ var ReactComponent = { ); }, + /** + * Generate a key string that identifies a component within a set. + * + * @param {*} component A component that could contain a manual key. + * @param {number} index Index that is used if a manual key is not provided. + * @return {string} + * @internal + */ + getKey: function(component, index) { + if (component && component.props && component.props.key != null) { + // Explicit key + return '' + component.props.key; + } + // Implicit key determined by the index in the set + return '' + index; + }, + /** * @internal */ @@ -337,60 +306,23 @@ var ReactComponent = { // All components start unmounted. this._lifeCycleState = ComponentLifeCycle.UNMOUNTED; - // Children can be either an array or more than one argument - if (arguments.length < 2) { - return; - } - - if (arguments.length === 2) { - - // A single string or number child is treated as content, not an array. - var type = typeof children; - if (children == null || type === 'string' || type === 'number') { - this.props.children = children; - return; - } - - // A single array can be reused if it's already flat - if (Array.isArray(children) && tryToReuseArray(children)) { - this.props.children = children; - return; + // Children can be more than one argument + var childrenLength = arguments.length - 1; + if (childrenLength === 1) { + if (__DEV__) { + validateChildKeys(children); } - - } - - // Subsequent arguments are rolled into one child array. Array arguments - // are flattened onto it. This is inlined to avoid extra heap allocation. - var targetArray = null; - for (var i = 1; i < arguments.length; i++) { - var child = arguments[i]; - if (Array.isArray(child)) { - if (child.length === 0) { - continue; - } - - if (targetArray === null) { - targetArray = []; - } - appendNestedChildren(i - 1, child, targetArray); - - } else if (!isEmptyChild(child)) { - - // Only truthy internal keys are valid. If it's not, we assign one. - if (ReactComponent.isValidComponent(child) && !child._key) { - // This is a static node and therefore safe to key by index. - // No warning necessary. - child._key = createKey(child.props.key, i - 1); + this.props.children = children; + } else if (childrenLength > 1) { + var childArray = Array(childrenLength); + for (var i = 0; i < childrenLength; i++) { + if (__DEV__) { + validateChildKeys(arguments[i + 1]); } - - if (targetArray === null) { - targetArray = []; - } - targetArray.push(child); - + childArray[i] = arguments[i + 1]; } + this.props.children = childArray; } - this.props.children = targetArray; }, /** diff --git a/src/core/ReactCompositeComponent.js b/src/core/ReactCompositeComponent.js index be911dc6f8e0e..e7fef2733b428 100644 --- a/src/core/ReactCompositeComponent.js +++ b/src/core/ReactCompositeComponent.js @@ -472,7 +472,6 @@ var ReactCompositeComponentMixin = { this.state = null; this._pendingState = null; this._compositeLifeCycleState = null; - this._compositionLevel = ReactCurrentOwner.getDepth() + 1; }, /** @@ -521,8 +520,6 @@ var ReactCompositeComponentMixin = { this._renderedComponent = this._renderValidatedComponent(); - this._renderedComponent._compositionLevel = this._compositionLevel + 1; - // Done with mounting, `setState` will now trigger UI changes. this._compositeLifeCycleState = null; var markup = this._renderedComponent.mountComponent(rootID, transaction); diff --git a/src/core/ReactCurrentOwner.js b/src/core/ReactCurrentOwner.js index 9ac0ab6edec57..ad137cdf2eff6 100644 --- a/src/core/ReactCurrentOwner.js +++ b/src/core/ReactCurrentOwner.js @@ -32,19 +32,7 @@ var ReactCurrentOwner = { * @internal * @type {ReactComponent} */ - current: null, - - /** - * @internal - * @return {number} - */ - getDepth: function() { - var owner = ReactCurrentOwner.current; - if (!owner) { - return 0; - } - return owner._compositionLevel; - } + current: null }; diff --git a/src/core/__tests__/ReactComponentFlattenChildren-test.js b/src/core/__tests__/ReactComponentFlattenChildren-test.js deleted file mode 100644 index 9ebe7baa2a61f..0000000000000 --- a/src/core/__tests__/ReactComponentFlattenChildren-test.js +++ /dev/null @@ -1,160 +0,0 @@ -/** - * Copyright 2013 Facebook, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - * @jsx React.DOM - * @emails react-core - */ - -"use strict"; - -var React; - -var Fake; - -var singleChild = function(children) { - return ({children}).props.children; -}; - -var multiChild = function(child1, child2, child3, child4) { - return ({child1} {child2} {child3}{child4}).props.children; -}; - - -describe('ReactComponentFlatten', function() { - beforeEach(function() { - require('mock-modules').dumpCache(); - React = require('React'); - var ReactComponent = require('ReactComponent'); - var copyProperties = require('copyProperties'); - var Constructor = function() {}; - copyProperties(Constructor.prototype, ReactComponent.Mixin); - Fake = function() { - var instance = new Constructor(); - instance.construct.apply(instance, arguments); - return instance; - }; - }); - - it("should reuse a flat array", function() { - var children = [ -
, -
- ]; - - var flat = singleChild(children); - - expect(flat).toBe(children); - expect(flat[0]._key).toEqual('0:0:foo'); - expect(flat[1]._key).toEqual('0:0:bar'); - }); - - it("should collapse a sparse array", function() { - var children = [ - null, -
, -
- ]; - - var flat = singleChild(children); - - expect(flat).not.toBe(children); - expect(flat.length).toEqual(2); - expect(flat[0]._key).toEqual('0:0:foo'); - expect(flat[1]._key).toEqual('0:0:bar'); - }); - - it("should collapse a nested array into a flat array", function() { - var flat = multiChild( - null, -
, - [
, null] - ); - - expect(flat.length).toEqual(2); - expect(flat[0]._key).toEqual('0:foo'); - expect(flat[1]._key).toEqual('0:2:bar'); - }); - - it("should retain key index despite static empty values", function() { - var before = multiChild( -
, -
, - [
], - [
] - ); - - var after = multiChild( - null, -
, - null, - [
] - ); - - expect(before.length).toEqual(4); - expect(after.length).toEqual(2); - expect(before[1]._key).toEqual(after[0]._key); - expect(before[3]._key).toEqual(after[1]._key); - }); - - it("should retain key index when wrapped", function() { - var before = singleChild(multiChild( -
, -
, - [
], - [
] - )); - - var after = singleChild(multiChild( - null, -
, - null, - [
] - )); - - expect(before.length).toEqual(4); - expect(after.length).toEqual(2); - expect(before[1]._key).toEqual(after[0]._key); - expect(before[3]._key).toEqual(after[1]._key); - }); - - it("should assign idempotent strings through flattening", function() { - var children = [ - 'FOO', - 'BAR' - ]; - var flat = singleChild(children); - var wrappedFlat = multiChild(null, flat); - expect(flat).toBe(children); - expect(wrappedFlat.length).toBe(2); - expect(wrappedFlat[0]).toBe(flat[0]); - expect(wrappedFlat[1]).toBe(flat[1]); - }); - - it("cannot keep keys unique when children are unboxed", function() { - // This is a case that difficult to solve and should not actually be solved. - var children1 = [
]; - var children2 = [
]; - var flat1 = singleChild(children1); - var flat2 = singleChild(children2); - // There's no way to tell flat1[0] and flat2[0] apart. - var mergedChildren = [flat1[0], flat2[0]]; - var newChildren = multiChild(mergedChildren); - expect(newChildren.length).toBe(2); - expect(newChildren[0]._key).toBe(newChildren[1]._key); - }); - -}); - - diff --git a/src/core/__tests__/ReactIdentity-test.js b/src/core/__tests__/ReactIdentity-test.js index 9d638d5579e93..fac5cde23d336 100644 --- a/src/core/__tests__/ReactIdentity-test.js +++ b/src/core/__tests__/ReactIdentity-test.js @@ -55,8 +55,8 @@ describe('ReactIdentity', function() { React.renderComponent(instance, document.createElement('div')); var node = instance.getDOMNode(); reactComponentExpect(instance).toBeDOMComponentWithChildCount(2); - checkId(node.childNodes[0], '.r[0].[0]{first}'); - checkId(node.childNodes[1], '.r[0].[0]{second}'); + checkId(node.childNodes[0], '.r[0].{first}'); + checkId(node.childNodes[1], '.r[0].{second}'); }); it('should allow key property to express identity', function() { @@ -71,10 +71,10 @@ describe('ReactIdentity', function() { React.renderComponent(instance, document.createElement('div')); var node = instance.getDOMNode(); reactComponentExpect(instance).toBeDOMComponentWithChildCount(4); - checkId(node.childNodes[0], '.r[0].[0:apple]'); - checkId(node.childNodes[1], '.r[0].[0:banana]'); - checkId(node.childNodes[2], '.r[0].[0:0]'); - checkId(node.childNodes[3], '.r[0].[0:123]'); + checkId(node.childNodes[0], '.r[0].[apple]'); + checkId(node.childNodes[1], '.r[0].[banana]'); + checkId(node.childNodes[2], '.r[0].[0]'); + checkId(node.childNodes[3], '.r[0].[123]'); }); it('should use instance identity', function() { @@ -95,15 +95,13 @@ describe('ReactIdentity', function() { React.renderComponent(instance, document.createElement('div')); var node = instance.getDOMNode(); reactComponentExpect(instance).toBeDOMComponentWithChildCount(3); - checkId(node.childNodes[0], '.r[0].[0:wrap1]'); - checkId( - node.childNodes[0].firstChild, - '.r[0].[0:wrap1].[0:squirrel]' - ); - checkId(node.childNodes[1], '.r[0].[0:wrap2]'); - checkId(node.childNodes[1].firstChild, '.r[0].[0:wrap2].[0:bunny]'); - checkId(node.childNodes[2], '.r[0].[0:2]'); - checkId(node.childNodes[2].firstChild, '.r[0].[0:2].[0:chipmunk]'); + + checkId(node.childNodes[0], '.r[0].[wrap1]'); + checkId(node.childNodes[0].firstChild, '.r[0].[wrap1].[squirrel]'); + checkId(node.childNodes[1], '.r[0].[wrap2]'); + checkId(node.childNodes[1].firstChild, '.r[0].[wrap2].[bunny]'); + checkId(node.childNodes[2], '.r[0].[2]'); + checkId(node.childNodes[2].firstChild, '.r[0].[2].[chipmunk]'); }); function renderAComponentWithKeyIntoContainer(key, container) { @@ -118,7 +116,7 @@ describe('ReactIdentity', function() { expect(span1.getDOMNode()).not.toBe(null); expect(span2.getDOMNode()).not.toBe(null); - checkId(span1.getDOMNode(), '.r[0].[0:0:' + key + ']'); + checkId(span1.getDOMNode(), '.r[0].[' + key + ']'); checkId(span2.getDOMNode(), '.r[0].[1]{' + key + '}'); } @@ -247,7 +245,7 @@ describe('ReactIdentity', function() { }).not.toThrow(); }); - it('should retain keys during updates in composite components', function() { + it('should retain key during updates in composite components', function() { var TestComponent = React.createClass({ render: function() { @@ -283,19 +281,13 @@ describe('ReactIdentity', function() { React.renderComponent(wrapped, document.createElement('div')); - var beforeKey = wrapped - ._renderedComponent - ._renderedComponent - .props.children[0]._key; + var beforeID = ReactID.getID(wrapped.getDOMNode().firstChild); wrapped.swap(); - var afterKey = wrapped - ._renderedComponent - ._renderedComponent - .props.children[0]._key; + var afterID = ReactID.getID(wrapped.getDOMNode().firstChild); - expect(beforeKey).not.toEqual(afterKey); + expect(beforeID).not.toEqual(afterID); }); diff --git a/src/core/__tests__/ReactMultiChildReconcile-test.js b/src/core/__tests__/ReactMultiChildReconcile-test.js index 81b680c35b69b..0ff3cf2eab6b9 100644 --- a/src/core/__tests__/ReactMultiChildReconcile-test.js +++ b/src/core/__tests__/ReactMultiChildReconcile-test.js @@ -42,12 +42,11 @@ var stripEmptyValues = function(obj) { }; /** - * These children are wrapped in an array and therefore their keys are prefixed. - * Their name is also wrapped in an prefix and suffix character. We strip those + * Child names is are wrapped in an prefix and suffix character. We strip those * out. This relies on a tiny implementation detail of the rendering system. */ var getOriginalKey = function(childName) { - return childName.substr(4, childName.length - 5); + return childName.substr(1, childName.length - 2); }; /** diff --git a/src/utils/__tests__/mapChildren-test.js b/src/utils/__tests__/mapChildren-test.js index 3e1d3ca2366fe..3f34482b0961c 100644 --- a/src/utils/__tests__/mapChildren-test.js +++ b/src/utils/__tests__/mapChildren-test.js @@ -48,11 +48,17 @@ describe('mapChildren', function() { this.addMatchers({ toHaveKeys: function(expected) { + if (!Array.isArray(this.actual)) { + if (expected.length !== 1) { + return false; + } + return this.actual.props.key === expected[0]; + } if (this.actual.length != expected.length) { return false; } return this.actual.every(function(component, index) { - return component._key === expected[index]; + return component.props.key === expected[index]; }, this); } }); @@ -73,9 +79,9 @@ describe('mapChildren', function() { .expectRenderedChild() .instance(); - expect(mapFn).toHaveBeenCalledWith(simpleKid, '0:simple', 0); + expect(mapFn).toHaveBeenCalledWith(simpleKid, '[simple]', 0); expect(rendered.props.children[0]).toBe(simpleKid); - expect(rendered.props.children).toHaveKeys(['0:simple']); + expect(rendered.props.children).toHaveKeys(['[simple]']); }); it('should pass key to returned component', function() { @@ -93,10 +99,10 @@ describe('mapChildren', function() { .instance(); expect(rendered.props.children[0]).not.toBe(simpleKid); - expect(rendered.props.children[0].props.children[0]).toBe(simpleKid); - expect(rendered.props.children).toHaveKeys(['1:0:0:simple']); + expect(rendered.props.children[0].props.children).toBe(simpleKid); + expect(rendered.props.children).toHaveKeys(['[simple]']); expect(rendered.props.children[0].props.children) - .toHaveKeys(['0:simple']); + .toHaveKeys(['simple']); }); it('should be called for each child', function() { @@ -123,12 +129,12 @@ describe('mapChildren', function() { .instance(); expect(mapFn.calls.length).toBe(3); - expect(mapFn).toHaveBeenCalledWith(kidOne, '0:one', 0); - expect(mapFn).toHaveBeenCalledWith(kidTwo, '0:two', 1); - expect(mapFn).toHaveBeenCalledWith(kidThree, '0:three', 2); + expect(mapFn).toHaveBeenCalledWith(kidOne, '[one]', 0); + expect(mapFn).toHaveBeenCalledWith(kidTwo, '[two]', 1); + expect(mapFn).toHaveBeenCalledWith(kidThree, '[three]', 2); expect(rendered.props.children).not.toEqual(instance.props.children); expect(rendered.props.children).toHaveKeys([ - '1:0:0:one', '1:0:0:two', '1:0:0:three' + '[one]', '[two]', '[three]' ]); }); }); diff --git a/src/utils/flattenChildren.js b/src/utils/flattenChildren.js index 7f18ab2fbde25..a0e65d213717d 100644 --- a/src/utils/flattenChildren.js +++ b/src/utils/flattenChildren.js @@ -18,6 +18,7 @@ "use strict"; +var ReactComponent = require('ReactComponent'); var ReactTextComponent = require('ReactTextComponent'); var throwIf = require('throwIf'); @@ -39,27 +40,24 @@ var DUPLICATE_KEY_ERROR = 'You have two children with identical keys. Make sure that you set the ' + '"key" property to a unique value such as a row ID.'; -/** - * If there is only a single child, it still needs a name. - */ -var ONLY_CHILD_NAME = '0'; - var flattenChildrenImpl = function(res, children, nameSoFar) { - var key; if (Array.isArray(children)) { for (var i = 0; i < children.length; i++) { var child = children[i]; - key = child && (child._key || (child.props && child.props.key)); flattenChildrenImpl( res, child, - nameSoFar + '[' + (key ? key : ('' + i)) + ']' + nameSoFar + '[' + ReactComponent.getKey(child, i) + ']' ); } } else { var type = typeof children; var isOnlyChild = nameSoFar === ''; - var storageName = isOnlyChild ? ONLY_CHILD_NAME : nameSoFar; + // If it's the only child, treat the name as if it was wrapped in an array + // so that it's consistent if the number of children grows + var storageName = isOnlyChild ? + '[' + ReactComponent.getKey(children, 0) + ']' : + nameSoFar; if (children === null || children === undefined || type === 'boolean') { res[storageName] = null; } else if (children.mountComponentIntoNode) { @@ -71,7 +69,7 @@ var flattenChildrenImpl = function(res, children, nameSoFar) { } else { if (type === 'object') { throwIf(children && children.nodeType === 1, INVALID_CHILD); - for (key in children) { + for (var key in children) { if (children.hasOwnProperty(key)) { flattenChildrenImpl( res, diff --git a/src/utils/mapChildren.js b/src/utils/mapChildren.js index a357eefa06367..fbdfd0cb1108a 100644 --- a/src/utils/mapChildren.js +++ b/src/utils/mapChildren.js @@ -18,14 +18,14 @@ "use strict"; +var flattenChildren = require('flattenChildren'); + /** * Maps children that are typically specified as `props.children`. * * The provided mapFunction(child, key, index) will be called for each * leaf child. * - * Note: mapChildren assumes children have already been flattened. - * * @param {array} children * @param {function(*, string, int)} mapFunction * @param {*} context @@ -36,12 +36,20 @@ function mapChildren(children, mapFunction, context) { return children; } var mappedChildren = []; - for (var ii = 0; ii < children.length; ii++) { - var child = children[ii]; - var key = child._key; - var mappedChild = mapFunction.call(context, child, key, ii); - mappedChild.props.key = key; - mappedChildren.push(mappedChild); + var flattenedMap = flattenChildren(children); + var ii = 0; + for (var key in flattenedMap) { + if (!flattenedMap.hasOwnProperty(key)) { + continue; + } + var child = flattenedMap[key]; + // In this version of map children we ignore empty children. + if (child !== null) { + var mappedChild = mapFunction.call(context, child, key, ii); + mappedChild.props.key = key; + mappedChildren.push(mappedChild); + ii++; + } } return mappedChildren; } diff --git a/src/utils/onlyChild.js b/src/utils/onlyChild.js index 2a78dcbfdab42..ccc5137668d63 100644 --- a/src/utils/onlyChild.js +++ b/src/utils/onlyChild.js @@ -24,24 +24,20 @@ var invariant = require('invariant'); /** * Returns the first child in a collection of children and verifies that there * is only one child in the collection. The current implementation of this - * function assumes that children have been flattened, but the purpose of this - * helper function is to abstract away the particular structure of children. + * function assumes that a single child gets passed without a wrapper, but the + * purpose of this helper function is to abstract away the particular structure + * of children. * * @param {?object} children Child collection structure. * @return {ReactComponent} The first and only `ReactComponent` contained in the * structure. */ function onlyChild(children) { - invariant(Array.isArray(children), 'onlyChild must be passed a valid Array.'); invariant( - children.length === 1, - 'onlyChild must be passed an Array with exactly one child.' + ReactComponent.isValidComponent(children), + 'onlyChild must be passed a children with exactly one child.' ); - invariant( - ReactComponent.isValidComponent(children[0]), - 'onlyChild must be passed an Array with exactly one child.' - ); - return children[0]; + return children; } module.exports = onlyChild;