@samitouri / QOS-React-2 / commits / c07ea0ba34

Allow iterables in traverseAllChildren

This lets you use any kind of iterable as a container of react child elements. It also preserves keys when possible if the iterable is keyed. * Use an ES6 Map or Set to hold children. * Use an Immutable-js collection to hold children. * Use a function* which yields children. Concretely, this removes the necessary conversion to JS objects if you're mapping over Immutable-js collections to get children, i.e.: ``` <div> {myImmutable.map(val => <span>{val}</span>).toJS() </div> ``` Now we can remove the `toJS()` and the intermediate allocation along with it. This also cleans up the traverseAllChildrenImpl method.

Lee Byron committed Oct 18, 2014 at 02:17 UTC c07ea0ba34df57fc54b218d6b4edcd090ed6411c
5 files changed +427 -57
src/core/ReactElementValidator.js
+23 -7
@@ -22,6 +22,7 @@ var ReactElement = require('ReactElement');
22 var ReactPropTypeLocations = require('ReactPropTypeLocations');
23 var ReactCurrentOwner = require('ReactCurrentOwner');
24
25 +var getIteratorFn = require('getIteratorFn');
26 var monitorCodeUse = require('monitorCodeUse');
27
28 /**
@@ -68,7 +69,7 @@ function validateExplicitKey(component, parentType) {
69
70 warnAndMonitorForKeyUse(
71 'react_key_warning',
71 - 'Each child in an array should have a unique "key" prop.',
72 + 'Each child in an array or iterator should have a unique "key" prop.',
73 component,
74 parentType
75 );
@@ -123,7 +124,9 @@ function warnAndMonitorForKeyUse(warningID, message, component, parentType) {
124 // property, it may be the creator of the child that's responsible for
125 // assigning it a key.
126 var childOwnerName = null;
126 - if (component._owner && component._owner !== ReactCurrentOwner.current) {
127 + if (component &&
128 + component._owner &&
129 + component._owner !== ReactCurrentOwner.current) {
130 // Name of the component that originally created this child.
131 childOwnerName = component._owner.constructor.displayName;
132
@@ -161,7 +164,6 @@ function monitorUseOfObjectMap() {
164 * @internal
165 * @param {*} component Statically passed child of any type.
166 * @param {*} parentType component's parent's type.
164 - * @return {boolean}
167 */
168 function validateChildKeys(component, parentType) {
169 if (Array.isArray(component)) {
@@ -174,10 +176,24 @@ function validateChildKeys(component, parentType) {
176 } else if (ReactElement.isValidElement(component)) {
177 // This component was passed in a valid location.
178 component._store.validated = true;
177 - } else if (component && typeof component === 'object') {
178 - monitorUseOfObjectMap();
179 - for (var name in component) {
180 - validatePropertyKey(name, component[name], parentType);
179 + } else if (component) {
180 + var iteratorFn = getIteratorFn(component);
181 + // Entry iterators provide implicit keys.
182 + if (iteratorFn && iteratorFn !== component.entries) {
183 + var iterator = iteratorFn.call(component);
184 + var step;
185 + while (!(step = iterator.next()).done) {
186 + if (ReactElement.isValidElement(step.value)) {
187 + validateExplicitKey(step.value, parentType);
188 + }
189 + }
190 + } else if (typeof component === 'object') {
191 + monitorUseOfObjectMap();
192 + for (var key in component) {
193 + if (component.hasOwnProperty(key)) {
194 + validatePropertyKey(key, component[key], parentType);
195 + }
196 + }
197 }
198 }
199 }
src/core/__tests__/ReactElement-test.js
+92 -1
@@ -169,10 +169,101 @@ describe('ReactElement', function() {
169
170 expect(console.warn.argsForCall.length).toBe(1);
171 expect(console.warn.argsForCall[0][0]).toContain(
172 - 'Each child in an array should have a unique "key" prop'
172 + 'Each child in an array or iterator should have a unique "key" prop.'
173 );
174 });
175
176 + it('warns for keys for iterables of elements in rest args', function() {
177 + spyOn(console, 'warn');
178 + var Component = React.createFactory(ComponentFactory);
179 +
180 + var iterable = {
181 + '@@iterator': function() {
182 + var i = 0;
183 + return {
184 + next: function() {
185 + var done = ++i > 2;
186 + return { value: done ? undefined : Component(), done: done };
187 + }
188 + };
189 + }
190 + };
191 +
192 + Component(null, iterable);
193 +
194 + expect(console.warn.argsForCall.length).toBe(1);
195 + expect(console.warn.argsForCall[0][0]).toContain(
196 + 'Each child in an array or iterator should have a unique "key" prop.'
197 + );
198 + });
199 +
200 + it('does not warns for arrays of elements with keys', function() {
201 + spyOn(console, 'warn');
202 + var Component = React.createFactory(ComponentFactory);
203 +
204 + Component(null, [ Component({key: '#1'}), Component({key: '#2'}) ]);
205 +
206 + expect(console.warn.argsForCall.length).toBe(0);
207 + });
208 +
209 + it('does not warns for iterable elements with keys', function() {
210 + spyOn(console, 'warn');
211 + var Component = React.createFactory(ComponentFactory);
212 +
213 + var iterable = {
214 + '@@iterator': function() {
215 + var i = 0;
216 + return {
217 + next: function() {
218 + var done = ++i > 2;
219 + return {
220 + value: done ? undefined : Component({key: '#' + i}),
221 + done: done
222 + };
223 + }
224 + };
225 + }
226 + };
227 +
228 + Component(null, iterable);
229 +
230 + expect(console.warn.argsForCall.length).toBe(0);
231 + });
232 +
233 + it('warns for numeric keys on objects in rest args', function() {
234 + spyOn(console, 'warn');
235 + var Component = React.createFactory(ComponentFactory);
236 +
237 + Component(null, { 1: Component(), 2: Component() });
238 +
239 + expect(console.warn.argsForCall.length).toBe(1);
240 + expect(console.warn.argsForCall[0][0]).toContain(
241 + 'Child objects should have non-numeric keys so ordering is preserved.'
242 + );
243 + });
244 +
245 + it('does not warn for numeric keys in entry iterables in rest args', function() {
246 + spyOn(console, 'warn');
247 + var Component = React.createFactory(ComponentFactory);
248 +
249 + var iterable = {
250 + '@@iterator': function() {
251 + var i = 0;
252 + return {
253 + next: function() {
254 + var done = ++i > 2;
255 + return { value: done ? undefined : [i, Component()], done: done };
256 + }
257 + };
258 + }
259 + };
260 + iterable.entries = iterable['@@iterator'];
261 +
262 + Component(null, iterable);
263 +
264 + expect(console.warn.argsForCall.length).toBe(0);
265 + });
266 +
267 it('does not warn when the element is directly in rest args', function() {
268 spyOn(console, 'warn');
269 var Component = React.createFactory(ComponentFactory);
src/utils/__tests__/traverseAllChildren-test.js
+161 -1
@@ -125,7 +125,64 @@ describe('traverseAllChildren', function() {
125 );
126 });
127
128 - // Todo: test that nums/strings are converted to ReactComponents.
128 + it('should traverse children of different kinds', function() {
129 + var div = <div key="divNode" />;
130 + var span = <span key="spanNode" />;
131 + var a = <a key="aNode" />;
132 +
133 + var traverseContext = [];
134 + var traverseFn =
135 + jasmine.createSpy().andCallFake(function(context, kid, key, index) {
136 + context.push(true);
137 + });
138 +
139 + var instance = (
140 + <div>
141 + {div}
142 + {[{span}]}
143 + {{a: a}}
144 + {'string'}
145 + {1234}
146 + {true}
147 + {false}
148 + {null}
149 + {undefined}
150 + </div>
151 + );
152 +
153 + traverseAllChildren(instance.props.children, traverseFn, traverseContext);
154 +
155 + expect(traverseFn.calls.length).toBe(9);
156 + expect(traverseContext.length).toEqual(9);
157 +
158 + expect(traverseFn).toHaveBeenCalledWith(
159 + traverseContext, div, '.$divNode', 0
160 + );
161 + expect(traverseFn).toHaveBeenCalledWith(
162 + traverseContext, span, '.1:0:$span:$spanNode', 1
163 + );
164 + expect(traverseFn).toHaveBeenCalledWith(
165 + traverseContext, a, '.2:$a:$aNode', 2
166 + );
167 + expect(traverseFn).toHaveBeenCalledWith(
168 + traverseContext, 'string', '.3', 3
169 + );
170 + expect(traverseFn).toHaveBeenCalledWith(
171 + traverseContext, 1234, '.4', 4
172 + );
173 + expect(traverseFn).toHaveBeenCalledWith(
174 + traverseContext, null, '.5', 5
175 + );
176 + expect(traverseFn).toHaveBeenCalledWith(
177 + traverseContext, null, '.6', 6
178 + );
179 + expect(traverseFn).toHaveBeenCalledWith(
180 + traverseContext, null, '.7', 7
181 + );
182 + expect(traverseFn).toHaveBeenCalledWith(
183 + traverseContext, null, '.8', 8
184 + );
185 + });
186
187 it('should be called for each child in nested structure', function() {
188 var zero = <div key="keyZero" />;
@@ -231,4 +288,107 @@ describe('traverseAllChildren', function() {
288 );
289 });
290
291 + it('should be called for each child in an iterable', function() {
292 + var threeDivIterable = {
293 + '@@iterator': function() {
294 + var i = 0;
295 + return {
296 + next: function() {
297 + if (i++ < 3) {
298 + return { value: <div key={'#'+i} />, done: false };
299 + } else {
300 + return { value: undefined, done: true };
301 + }
302 + }
303 + };
304 + }
305 + };
306 +
307 + var traverseContext = [];
308 + var traverseFn =
309 + jasmine.createSpy().andCallFake(function(context, kid, key, index) {
310 + context.push(kid);
311 + });
312 +
313 + var instance = (
314 + <div>
315 + {threeDivIterable}
316 + </div>
317 + );
318 +
319 + traverseAllChildren(instance.props.children, traverseFn, traverseContext);
320 + expect(traverseFn.calls.length).toBe(3);
321 +
322 + expect(traverseFn).toHaveBeenCalledWith(
323 + traverseContext,
324 + traverseContext[0],
325 + '.$#1',
326 + 0
327 + );
328 + expect(traverseFn).toHaveBeenCalledWith(
329 + traverseContext,
330 + traverseContext[1],
331 + '.$#2',
332 + 1
333 + );
334 + expect(traverseFn).toHaveBeenCalledWith(
335 + traverseContext,
336 + traverseContext[2],
337 + '.$#3',
338 + 2
339 + );
340 + });
341 +
342 + it('should use keys from entry iterables', function() {
343 + var threeDivEntryIterable = {
344 + '@@iterator': function() {
345 + var i = 0;
346 + return {
347 + next: function() {
348 + if (i++ < 3) {
349 + return { value: ['#'+i, <div />], done: false };
350 + } else {
351 + return { value: undefined, done: true };
352 + }
353 + }
354 + };
355 + }
356 + };
357 + threeDivEntryIterable.entries = threeDivEntryIterable['@@iterator'];
358 +
359 + var traverseContext = [];
360 + var traverseFn =
361 + jasmine.createSpy().andCallFake(function(context, kid, key, index) {
362 + context.push(kid);
363 + });
364 +
365 + var instance = (
366 + <div>
367 + {threeDivEntryIterable}
368 + </div>
369 + );
370 +
371 + traverseAllChildren(instance.props.children, traverseFn, traverseContext);
372 + expect(traverseFn.calls.length).toBe(3);
373 +
374 + expect(traverseFn).toHaveBeenCalledWith(
375 + traverseContext,
376 + traverseContext[0],
377 + '.$#1:0',
378 + 0
379 + );
380 + expect(traverseFn).toHaveBeenCalledWith(
381 + traverseContext,
382 + traverseContext[1],
383 + '.$#2:0',
384 + 1
385 + );
386 + expect(traverseFn).toHaveBeenCalledWith(
387 + traverseContext,
388 + traverseContext[2],
389 + '.$#3:0',
390 + 2
391 + );
392 + });
393 +
394 });
src/utils/getIteratorFn.js new
+43
@@ -0,0 +1,43 @@
1 +/**
2 + * Copyright 2013-2014, Facebook, Inc.
3 + * All rights reserved.
4 + *
5 + * This source code is licensed under the BSD-style license found in the
6 + * LICENSE file in the root directory of this source tree. An additional grant
7 + * of patent rights can be found in the PATENTS file in the same directory.
8 + *
9 + * @providesModule getIteratorFn
10 + * @typechecks static-only
11 + */
12 +
13 +"use strict";
14 +
15 +/* global Symbol */
16 +var ITERATOR_SYMBOL = typeof Symbol === 'function' && Symbol.iterator;
17 +var FAUX_ITERATOR_SYMBOL = '@@iterator'; // Before Symbol spec.
18 +
19 +/**
20 + * Returns the iterator method function contained on the iterable object.
21 + *
22 + * Be sure to invoke the function with the iterable as context:
23 + *
24 + * var iteratorFn = getIteratorFn(myIterable);
25 + * if (iteratorFn) {
26 + * var iterator = iteratorFn.call(myIterable);
27 + * ...
28 + * }
29 + *
30 + * @param {?object} maybeIterable
31 + * @return {?function}
32 + */
33 +function getIteratorFn(maybeIterable) {
34 + var iteratorFn = maybeIterable && (
35 + (ITERATOR_SYMBOL && maybeIterable[ITERATOR_SYMBOL]) ||
36 + maybeIterable[FAUX_ITERATOR_SYMBOL]
37 + );
38 + if (typeof iteratorFn === 'function') {
39 + return iteratorFn;
40 + }
41 +}
42 +
43 +module.exports = getIteratorFn;
src/utils/traverseAllChildren.js
+108 -48
@@ -14,6 +14,7 @@
14 var ReactElement = require('ReactElement');
15 var ReactInstanceHandles = require('ReactInstanceHandles');
16
17 +var getIteratorFn = require('getIteratorFn');
18 var invariant = require('invariant');
19
20 var SEPARATOR = ReactInstanceHandles.SEPARATOR;
@@ -88,58 +89,91 @@ function wrapUserProvidedKey(key) {
89 * process.
90 * @return {!number} The number of children in this subtree.
91 */
91 -var traverseAllChildrenImpl =
92 - function(children, nameSoFar, indexSoFar, callback, traverseContext) {
93 - var nextName, nextIndex;
94 - var subtreeCount = 0; // Count of children found in the current subtree.
95 - if (Array.isArray(children)) {
96 - for (var i = 0; i < children.length; i++) {
97 - var child = children[i];
98 - nextName = (
99 - nameSoFar +
100 - (nameSoFar ? SUBSEPARATOR : SEPARATOR) +
101 - getComponentKey(child, i)
102 - );
103 - nextIndex = indexSoFar + subtreeCount;
104 - subtreeCount += traverseAllChildrenImpl(
105 - child,
106 - nextName,
107 - nextIndex,
108 - callback,
109 - traverseContext
110 - );
111 - }
112 - } else {
113 - var type = typeof children;
114 - var isOnlyChild = nameSoFar === '';
92 +function traverseAllChildrenImpl(
93 + children,
94 + nameSoFar,
95 + indexSoFar,
96 + callback,
97 + traverseContext
98 +) {
99 + var type = typeof children;
100 +
101 + if (type === 'undefined' || type === 'boolean') {
102 + // All of the above are perceived as null.
103 + children = null;
104 + }
105 +
106 + if (children === null ||
107 + type === 'string' ||
108 + type === 'number' ||
109 + ReactElement.isValidElement(children)) {
110 + callback(
111 + traverseContext,
112 + children,
113 // If it's the only child, treat the name as if it was wrapped in an array
116 - // so that it's consistent if the number of children grows
117 - var storageName =
118 - isOnlyChild ? SEPARATOR + getComponentKey(children, 0) : nameSoFar;
119 - if (children == null || type === 'boolean') {
120 - // All of the above are perceived as null.
121 - callback(traverseContext, null, storageName, indexSoFar);
122 - subtreeCount = 1;
123 - } else if (type === 'string' || type === 'number' ||
124 - ReactElement.isValidElement(children)) {
125 - callback(traverseContext, children, storageName, indexSoFar);
126 - subtreeCount = 1;
127 - } else if (type === 'object') {
128 - invariant(
129 - !children || children.nodeType !== 1,
130 - 'traverseAllChildren(...): Encountered an invalid child; DOM ' +
131 - 'elements are not valid children of React components.'
132 - );
133 - for (var key in children) {
134 - if (children.hasOwnProperty(key)) {
114 + // so that it's consistent if the number of children grows.
115 + nameSoFar === '' ? SEPARATOR + getComponentKey(children, 0) : nameSoFar,
116 + indexSoFar
117 + );
118 + return 1;
119 + }
120 +
121 + var child, nextName, nextIndex;
122 + var subtreeCount = 0; // Count of children found in the current subtree.
123 +
124 + if (Array.isArray(children)) {
125 + for (var i = 0; i < children.length; i++) {
126 + child = children[i];
127 + nextName = (
128 + nameSoFar +
129 + (nameSoFar ? SUBSEPARATOR : SEPARATOR) +
130 + getComponentKey(child, i)
131 + );
132 + nextIndex = indexSoFar + subtreeCount;
133 + subtreeCount += traverseAllChildrenImpl(
134 + child,
135 + nextName,
136 + nextIndex,
137 + callback,
138 + traverseContext
139 + );
140 + }
141 + } else {
142 + var iteratorFn = getIteratorFn(children);
143 + if (iteratorFn) {
144 + var iterator = iteratorFn.call(children);
145 + var step;
146 + if (iteratorFn !== children.entries) {
147 + while (!(step = iterator.next()).done) {
148 + child = step.value;
149 + nextName = (
150 + nameSoFar +
151 + (nameSoFar ? SUBSEPARATOR : SEPARATOR) +
152 + getComponentKey(child, i)
153 + );
154 + nextIndex = indexSoFar + subtreeCount;
155 + subtreeCount += traverseAllChildrenImpl(
156 + child,
157 + nextName,
158 + nextIndex,
159 + callback,
160 + traverseContext
161 + );
162 + }
163 + } else {
164 + // Iterator will provide entry [k,v] tuples rather than values.
165 + while (!(step = iterator.next()).done) {
166 + var entry = step.value;
167 + if (entry) {
168 + child = entry[1];
169 nextName = (
170 nameSoFar + (nameSoFar ? SUBSEPARATOR : SEPARATOR) +
137 - wrapUserProvidedKey(key) + SUBSEPARATOR +
138 - getComponentKey(children[key], 0)
171 + wrapUserProvidedKey(entry[0]) + SUBSEPARATOR +
172 + getComponentKey(child, 0)
173 );
174 nextIndex = indexSoFar + subtreeCount;
175 subtreeCount += traverseAllChildrenImpl(
142 - children[key],
176 + child,
177 nextName,
178 nextIndex,
179 callback,
@@ -148,9 +182,35 @@ var traverseAllChildrenImpl =
182 }
183 }
184 }
185 + } else if (type === 'object') {
186 + invariant(
187 + children.nodeType !== 1,
188 + 'traverseAllChildren(...): Encountered an invalid child; DOM ' +
189 + 'elements are not valid children of React components.'
190 + );
191 + for (var key in children) {
192 + if (children.hasOwnProperty(key)) {
193 + child = children[key];
194 + nextName = (
195 + nameSoFar + (nameSoFar ? SUBSEPARATOR : SEPARATOR) +
196 + wrapUserProvidedKey(key) + SUBSEPARATOR +
197 + getComponentKey(child, 0)
198 + );
199 + nextIndex = indexSoFar + subtreeCount;
200 + subtreeCount += traverseAllChildrenImpl(
201 + child,
202 + nextName,
203 + nextIndex,
204 + callback,
205 + traverseContext
206 + );
207 + }
208 + }
209 }
152 - return subtreeCount;
153 - };
210 + }
211 +
212 + return subtreeCount;
213 +}
214
215 /**
216 * Traverses children that are typically specified as `props.children`, but