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