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;