diff --git a/scripts/fiber/tests-passing.txt b/scripts/fiber/tests-passing.txt index 2e36948c29a..06b48abf157 100644 --- a/scripts/fiber/tests-passing.txt +++ b/scripts/fiber/tests-passing.txt @@ -41,6 +41,7 @@ scripts/shared/__tests__/evalToString-test.js * should throw when it finds other types src/isomorphic/children/__tests__/ReactChildren-test.js +* should support single child without key * should support identity for simple * should treat single arrayless child as being in array * should treat single child in array as expected diff --git a/src/isomorphic/children/ReactChildren.js b/src/isomorphic/children/ReactChildren.js index 206d9465755..5d37ed8d437 100644 --- a/src/isomorphic/children/ReactChildren.js +++ b/src/isomorphic/children/ReactChildren.js @@ -7,110 +7,201 @@ * of patent rights can be found in the PATENTS file in the same directory. * * @providesModule ReactChildren + * @flow */ 'use strict'; -var PooledClass = require('PooledClass'); var ReactElement = require('ReactElement'); - var emptyFunction = require('fbjs/lib/emptyFunction'); -var traverseAllChildren = require('traverseAllChildren'); +var invariant = require('fbjs/lib/invariant'); -var twoArgumentPooler = PooledClass.twoArgumentPooler; -var fourArgumentPooler = PooledClass.fourArgumentPooler; +var ITERATOR_SYMBOL = typeof Symbol === 'function' && Symbol.iterator; +var FAUX_ITERATOR_SYMBOL = '@@iterator'; // Before Symbol spec. +// The Symbol used to tag the ReactElement type. If there is no native Symbol +// nor polyfill, then a plain number is used for performance. +var REACT_ELEMENT_TYPE = + (typeof Symbol === 'function' && Symbol.for && Symbol.for('react.element')) || + 0xeac7; -var userProvidedKeyEscapeRegex = /\/+/g; -function escapeUserProvidedKey(text) { - return ('' + text).replace(userProvidedKeyEscapeRegex, '$&/'); +if (__DEV__) { + var warning = require('fbjs/lib/warning'); + var {getCurrentStackAddendum} = require('ReactComponentTreeHook'); } +var SEPARATOR = '.'; +var SUBSEPARATOR = ':'; + /** - * PooledClass representing the bookkeeping associated with performing a child - * traversal. Allows avoiding binding callbacks. + * Escape and wrap key so it is safe to use as a reactid * - * @constructor ForEachBookKeeping - * @param {!function} forEachFunction Function to perform traversal with. - * @param {?*} forEachContext Context to perform context with. + * @param {string} key to be escaped. + * @return {string} the escaped key. */ -function ForEachBookKeeping(forEachFunction, forEachContext) { - this.func = forEachFunction; - this.context = forEachContext; - this.count = 0; -} -ForEachBookKeeping.prototype.destructor = function() { - this.func = null; - this.context = null; - this.count = 0; -}; -PooledClass.addPoolingTo(ForEachBookKeeping, twoArgumentPooler); +function escape(key: string): string { + var escapeRegex = /[=:]/g; + var escaperLookup = { + '=': '=0', + ':': '=2', + }; + var escapedString = ('' + key).replace(escapeRegex, function(match) { + return escaperLookup[match]; + }); -function forEachSingleChild(bookKeeping, child, name) { - var {func, context} = bookKeeping; - func.call(context, child, bookKeeping.count++); + return '$' + escapedString; } +var didWarnAboutMaps = false; + /** - * Iterates through children that are typically specified as `props.children`. - * - * See https://facebook.github.io/react/docs/react-api.html#react.children.foreach - * - * The provided forEachFunc(child, index) will be called for each - * leaf child. - * - * @param {?*} children Children tree container. - * @param {function(*, int)} forEachFunc - * @param {*} forEachContext Context for forEachContext. + * Generate a key string that identifies a ReactElement within a set. */ -function forEachChildren(children, forEachFunc, forEachContext) { - if (children == null) { - return children; +function getReactElementKey(element, index) { + // Do some typechecking here since we call this blindly. We want to ensure + // that we don't block potential future ES APIs. + if (typeof element === 'object' && element !== null && element.key != null) { + // Explicit key + return escape(element.key); } - var traverseContext = ForEachBookKeeping.getPooled( - forEachFunc, - forEachContext, - ); - traverseAllChildren(children, forEachSingleChild, traverseContext); - ForEachBookKeeping.release(traverseContext); + // Implicit key determined by the index in the set + return index.toString(36); } -/** - * PooledClass representing the bookkeeping associated with performing a child - * mapping. Allows avoiding binding callbacks. - * - * @constructor MapBookKeeping - * @param {!*} mapResult Object containing the ordered map of results. - * @param {!function} mapFunction Function to perform mapping with. - * @param {?*} mapContext Context to perform mapping with. - */ -function MapBookKeeping(mapResult, keyPrefix, mapFunction, mapContext) { - this.result = mapResult; - this.keyPrefix = keyPrefix; - this.func = mapFunction; - this.context = mapContext; - this.count = 0; +function traverseAllChildren( + children: T | null, + nameSoFar: string, + callback: (context: I, children: T | null, nameSoFar: string) => void, + traverseContext: I, +) { + var type = typeof children; + + if (type === 'undefined' || type === 'boolean') { + // All of the above are perceived as null. + children = null; + } + + if ( + children === null || + type === 'string' || + type === 'number' || + // The following is inlined from ReactElement. This means we can optimize + // some checks. React Fiber also inlines this logic for similar purposes. + (type === 'object' && + (children: ReactElement).$$typeof === REACT_ELEMENT_TYPE) + ) { + callback( + traverseContext, + children, + // 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. + nameSoFar === '' + ? SEPARATOR + getReactElementKey((children: ReactElement), 0) + : nameSoFar, + ); + return 1; + } + + var child; + var nextName; + var subtreeCount = 0; // Count of children found in the current subtree. + var nextNamePrefix = nameSoFar === '' ? SEPARATOR : nameSoFar + SUBSEPARATOR; + + if (Array.isArray(children)) { + for (var i = 0; i < children.length; i++) { + child = (children[i]: ReactElement); + nextName = nextNamePrefix + getReactElementKey(child, i); + subtreeCount += traverseAllChildren( + child, + nextName, + callback, + traverseContext, + ); + } + } else { + var iteratorFn = + (ITERATOR_SYMBOL && children[ITERATOR_SYMBOL]) || + children[FAUX_ITERATOR_SYMBOL]; + if (typeof iteratorFn === 'function') { + if (__DEV__) { + // Warn about using Maps as children + if (children != null && iteratorFn === children.entries) { + warning( + didWarnAboutMaps, + 'Using Maps as children is unsupported and will likely yield ' + + 'unexpected results. Convert it to a sequence/iterable of keyed ' + + 'ReactElements instead.%s', + getCurrentStackAddendum(), + ); + didWarnAboutMaps = true; + } + } + + var iterator = iteratorFn.call(children); + var step; + var ii = 0; + while (iterator && !(step = iterator.next()).done) { + child = step != null ? (step.value: ReactElement) : null; + nextName = nextNamePrefix + getReactElementKey(child, ii++); + subtreeCount += traverseAllChildren( + child, + nextName, + callback, + traverseContext, + ); + } + } else if (type === 'object') { + var addendum = ''; + if (__DEV__) { + addendum = + ' If you meant to render a collection of children, use an array ' + + 'instead.' + + getCurrentStackAddendum(); + } + var childrenString = '' + (children: ReactElement); + invariant( + false, + 'Objects are not valid as a React child (found: %s).%s', + childrenString === '[object Object]' + ? 'object with keys {' + + Object.keys((children: ReactElement)).join(', ') + + '}' + : childrenString, + addendum, + ); + } + } + + return subtreeCount; +} + +var userProvidedKeyEscapeRegex = /\/+/g; +function escapeUserProvidedKey(text: string) { + return ('' + text).replace(userProvidedKeyEscapeRegex, '$&/'); } -MapBookKeeping.prototype.destructor = function() { - this.result = null; - this.keyPrefix = null; - this.func = null; - this.context = null; - this.count = 0; -}; -PooledClass.addPoolingTo(MapBookKeeping, fourArgumentPooler); function mapSingleChildIntoContext(bookKeeping, child, childKey) { var {result, keyPrefix, func, context} = bookKeeping; var mappedChild = func.call(context, child, bookKeeping.count++); + if (mappedChild == null) { + return; + } + if (Array.isArray(mappedChild)) { - mapIntoWithKeyPrefixInternal( + var traverseContext = { + result: result, + keyPrefix: childKey != null ? escapeUserProvidedKey(childKey) + '/' : '', + func: emptyFunction.thatReturnsArgument, + context: null, + count: 0, + }; + traverseAllChildren( mappedChild, - result, - childKey, - emptyFunction.thatReturnsArgument, + '', + mapSingleChildIntoContext, + traverseContext, ); - } else if (mappedChild != null) { + } else { if (ReactElement.isValidElement(mappedChild)) { mappedChild = ReactElement.cloneAndReplaceKey( mappedChild, @@ -118,7 +209,7 @@ function mapSingleChildIntoContext(bookKeeping, child, childKey) { // traverseAllChildren used to do for objects as children keyPrefix + (mappedChild.key && (!child || child.key !== mappedChild.key) - ? escapeUserProvidedKey(mappedChild.key) + '/' + ? escapeUserProvidedKey((mappedChild: ReactElement).key) + '/' : '') + childKey, ); @@ -127,21 +218,6 @@ function mapSingleChildIntoContext(bookKeeping, child, childKey) { } } -function mapIntoWithKeyPrefixInternal(children, array, prefix, func, context) { - var escapedPrefix = ''; - if (prefix != null) { - escapedPrefix = escapeUserProvidedKey(prefix) + '/'; - } - var traverseContext = MapBookKeeping.getPooled( - array, - escapedPrefix, - func, - context, - ); - traverseAllChildren(children, mapSingleChildIntoContext, traverseContext); - MapBookKeeping.release(traverseContext); -} - /** * Maps children that are typically specified as `props.children`. * @@ -149,36 +225,54 @@ function mapIntoWithKeyPrefixInternal(children, array, prefix, func, context) { * * The provided mapFunction(child, key, index) will be called for each * leaf child. - * - * @param {?*} children Children tree container. - * @param {function(*, int)} func The map function. - * @param {*} context Context for mapFunction. - * @return {object} Object containing the ordered map of results. */ -function mapChildren(children, func, context) { +function mapChildren( + children: mixed, + func: (child: mixed, count: number) => void, + context?: Object, +): ?(mixed[]) { if (children == null) { return children; } var result = []; - mapIntoWithKeyPrefixInternal(children, result, null, func, context); + var traverseContext = { + result: result, + keyPrefix: '', + func: func, + context: context, + count: 0, + }; + traverseAllChildren(children, '', mapSingleChildIntoContext, traverseContext); return result; } -function forEachSingleChildDummy(traverseContext, child, name) { - return null; +function forEachSingleChild(bookKeeping, child, name) { + var {func, context} = bookKeeping; + func.call(context, child, bookKeeping.count++); } /** - * Count the number of children that are typically specified as - * `props.children`. + * Iterates through children that are typically specified as `props.children`. * - * See https://facebook.github.io/react/docs/react-api.html#react.children.count + * See https://facebook.github.io/react/docs/react-api.html#react.children.foreach * - * @param {?*} children Children tree container. - * @return {number} The number of children. + * The provided forEachFunc(child, index) will be called for each + * leaf child. */ -function countChildren(children, context) { - return traverseAllChildren(children, forEachSingleChildDummy, null); +function forEachChildren( + children: mixed, + forEachFunc: (child: mixed, count: number) => void, + forEachContext?: Object, +): void { + if (children == null) { + return; + } + var traverseContext = { + func: forEachFunc, + context: forEachContext, + count: 0, + }; + traverseAllChildren(children, '', forEachSingleChild, traverseContext); } /** @@ -187,21 +281,29 @@ function countChildren(children, context) { * * See https://facebook.github.io/react/docs/react-api.html#react.children.toarray */ -function toArray(children) { - var result = []; - mapIntoWithKeyPrefixInternal( - children, - result, - null, - emptyFunction.thatReturnsArgument, - ); - return result; +function toArray(children: mixed): ?(mixed[]) { + if (children == null) { + return []; + } + return mapChildren(children, emptyFunction.thatReturnsArgument); +} + +/** + * Count the number of children that are typically specified as + * `props.children`. + * + * See https://facebook.github.io/react/docs/react-api.html#react.children.count + */ +function countChildren(children: mixed): number { + if (children == null) { + return 0; + } + return traverseAllChildren(children, '', emptyFunction.thatReturnsNull, null); } var ReactChildren = { forEach: forEachChildren, map: mapChildren, - mapIntoWithKeyPrefixInternal: mapIntoWithKeyPrefixInternal, count: countChildren, toArray: toArray, }; diff --git a/src/isomorphic/children/__tests__/ReactChildren-test.js b/src/isomorphic/children/__tests__/ReactChildren-test.js index 9d97007cd17..0f9eb06726e 100644 --- a/src/isomorphic/children/__tests__/ReactChildren-test.js +++ b/src/isomorphic/children/__tests__/ReactChildren-test.js @@ -28,6 +28,27 @@ describe('ReactChildren', () => { ReactTestUtils = require('react-dom/test-utils'); }); + it('should support single child without key', () => { + var context = {}; + var callback = jasmine.createSpy().and.callFake(function(kid, index) { + expect(this).toBe(context); + return kid; + }); + + var simpleKid = ; + var instance =
{simpleKid}
; + React.Children.forEach(instance.props.children, callback, context); + expect(callback).toHaveBeenCalledWith(simpleKid, 0); + callback.calls.reset(); + var mappedChildren = React.Children.map( + instance.props.children, + callback, + context, + ); + expect(callback).toHaveBeenCalledWith(simpleKid, 0); + expect(mappedChildren[0]).toEqual(); + }); + it('should support identity for simple', () => { var context = {}; var callback = jasmine.createSpy().and.callFake(function(kid, index) { @@ -60,7 +81,7 @@ describe('ReactChildren', () => { return kid; }); - var simpleKid = ; + var simpleKid = ; var instance =
{simpleKid}
; React.Children.forEach(instance.props.children, callback, context); expect(callback).toHaveBeenCalledWith(simpleKid, 0); @@ -71,7 +92,7 @@ describe('ReactChildren', () => { context, ); expect(callback).toHaveBeenCalledWith(simpleKid, 0); - expect(mappedChildren[0]).toEqual(); + expect(mappedChildren[0]).toEqual(); }); it('should treat single child in array as expected', () => {