Skip to content

Commit

Permalink
Revert Flattening of Children
Browse files Browse the repository at this point in the history
I still think the semantics of flattening children is valid but we'll
want to revert the flattening implementation while we solidify the
semantics and try another approach.

This reverts flattening so that this.props.children can once again be
either a single child, flat array or nested array.

mapChildren calls flattenChildren before doing anything else. This is
not the most efficient approach but I wanted to keep this inital diff
simple. It also ignores empty values for backwards compatibility.

We may want to try another approach where empty values are included
in the map.

Validation of keys is still done inside ReactComponent. Ideally I'd
like to extract that into a separate module but to avoid cyclic
dependencies, I'm keeping it in ReactComponent for now.
  • Loading branch information
jeffmo authored and zpao committed Jul 1, 2013
1 parent 40bebf0 commit dd61439
Show file tree
Hide file tree
Showing 10 changed files with 152 additions and 396 deletions.
238 changes: 85 additions & 153 deletions src/core/ReactComponent.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand All @@ -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;
}
}

Expand Down Expand Up @@ -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
*/
Expand Down Expand Up @@ -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;
},

/**
Expand Down
3 changes: 0 additions & 3 deletions src/core/ReactCompositeComponent.js
Original file line number Diff line number Diff line change
Expand Up @@ -472,7 +472,6 @@ var ReactCompositeComponentMixin = {
this.state = null;
this._pendingState = null;
this._compositeLifeCycleState = null;
this._compositionLevel = ReactCurrentOwner.getDepth() + 1;
},

/**
Expand Down Expand Up @@ -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);
Expand Down
14 changes: 1 addition & 13 deletions src/core/ReactCurrentOwner.js
Original file line number Diff line number Diff line change
Expand Up @@ -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

};

Expand Down
Loading

0 comments on commit dd61439

Please sign in to comment.