Make keyed object fragments an opaque type
This triggers a warning if you try to pass a keyed object as a child. You now have to wrap it in React.addons.createFragment(object) which creates a proxy (in dev) which warns if it is accessed. The purpose of this is to make these into opaque objects so that nobody relies on its data structure. After that we can turn it into a different data structure such as a ReactFragment node or an iterable of flattened ReactElements.
Sebastian Markbage committed
Feb 9, 2015 at 14:24 UTC
56f51156bab53e39b80397211d4b1ba853902ac4
19 files changed
+468
-120
src/addons/ReactFragment.js
new
+164
@@ -0,0 +1,164 @@
1
+/**
2
+ * Copyright 2015, 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 ReactFragment
10
+*/
11
+
12
+'use strict';
13
+
14
+var ReactElement = require('ReactElement');
15
+
16
+var warning = require('warning');
17
+
18
+/**
19
+ * We used to allow keyed objects to serve as a collection of ReactElements,
20
+ * or nested sets. This allowed us a way to explicitly key a set a fragment of
21
+ * components. This is now being replaced with an opaque data structure.
22
+ * The upgrade path is to call React.addons.createFragment({ key: value }) to
23
+ * create a keyed fragment. The resulting data structure is opaque, for now.
24
+ */
25
+
26
+if (__DEV__) {
27
+ var fragmentKey = '_reactFragment';
28
+ var didWarnKey = '_reactDidWarn';
29
+ var canWarnForReactFragment = false;
30
+
31
+ try {
32
+ // Feature test. Don't even try to issue this warning if we can't use
33
+ // enumerable: false.
34
+
35
+ var dummy = function() {
36
+ return 1;
37
+ };
38
+
39
+ Object.defineProperty(
40
+ {},
41
+ fragmentKey,
42
+ { enumerable: false, value: true }
43
+ );
44
+
45
+ Object.defineProperty(
46
+ {},
47
+ 'key',
48
+ { enumerable: true, get: dummy }
49
+ );
50
+
51
+ canWarnForReactFragment = true;
52
+ } catch (x) { }
53
+
54
+ var proxyPropertyAccessWithWarning = function(obj, key) {
55
+ Object.defineProperty(obj, key, {
56
+ enumerable: true,
57
+ get: function() {
58
+ warning(
59
+ this[didWarnKey],
60
+ 'A ReactFragment is an opaque type. Accessing any of its ' +
61
+ 'properties is deprecated. Pass it to one of the React.Children ' +
62
+ 'helpers.'
63
+ );
64
+ this[didWarnKey] = true;
65
+ return this[fragmentKey][key];
66
+ },
67
+ set: function(value) {
68
+ warning(
69
+ this[didWarnKey],
70
+ 'A ReactFragment is an immutable opaque type. Mutating its ' +
71
+ 'properties is deprecated.'
72
+ );
73
+ this[didWarnKey] = true;
74
+ this[fragmentKey][key] = value;
75
+ }
76
+ });
77
+ };
78
+
79
+ var issuedWarnings = {};
80
+
81
+ var didWarnForFragment = function(fragment) {
82
+ // We use the keys and the type of the value as a heuristic to dedupe the
83
+ // warning to avoid spamming too much.
84
+ var fragmentCacheKey = '';
85
+ for (var key in fragment) {
86
+ fragmentCacheKey += key + ':' + (typeof fragment[key]) + ',';
87
+ }
88
+ var alreadyWarnedOnce = !!issuedWarnings[fragmentCacheKey];
89
+ issuedWarnings[fragmentCacheKey] = true;
90
+ return alreadyWarnedOnce;
91
+ };
92
+}
93
+
94
+var ReactFragment = {
95
+ // Wrap a keyed object in an opaque proxy that warns you if you access any
96
+ // of its properties.
97
+ create: function(object) {
98
+ if (__DEV__) {
99
+ if (canWarnForReactFragment) {
100
+ var proxy = {};
101
+ Object.defineProperty(proxy, fragmentKey, {
102
+ enumerable: false,
103
+ value: object
104
+ });
105
+ Object.defineProperty(proxy, didWarnKey, {
106
+ writable: true,
107
+ enumerable: false,
108
+ value: false
109
+ });
110
+ for (var key in object) {
111
+ proxyPropertyAccessWithWarning(proxy, key);
112
+ }
113
+ Object.preventExtensions(proxy);
114
+ return proxy;
115
+ }
116
+ }
117
+ return object;
118
+ },
119
+ // Extract the original keyed object from the fragment opaque type. Warn if
120
+ // a plain object is passed here.
121
+ extract: function(fragment) {
122
+ if (__DEV__) {
123
+ if (canWarnForReactFragment) {
124
+ if (!fragment[fragmentKey]) {
125
+ warning(
126
+ didWarnForFragment(fragment),
127
+ 'Any use of a keyed object should be wrapped in ' +
128
+ 'React.addons.createFragment(object) before passed as a child.'
129
+ );
130
+ return fragment;
131
+ }
132
+ return fragment[fragmentKey];
133
+ }
134
+ }
135
+ return fragment;
136
+ },
137
+ // Check if this is a fragment and if so, extract the keyed object. If it
138
+ // is a fragment-like object, warn that it should be wrapped. Ignore if we
139
+ // can't determine what kind of object this is.
140
+ extractIfFragment: function(fragment) {
141
+ if (__DEV__) {
142
+ if (canWarnForReactFragment) {
143
+ // If it is the opaque type, return the keyed object.
144
+ if (fragment[fragmentKey]) {
145
+ return fragment[fragmentKey];
146
+ }
147
+ // Otherwise, check each property if it has an element, if it does
148
+ // it is probably meant as a fragment, so we can warn early. Defer,
149
+ // the warning to extract.
150
+ for (var key in fragment) {
151
+ if (fragment.hasOwnProperty(key) &&
152
+ ReactElement.isValidElement(fragment[key])) {
153
+ // This looks like a fragment object, we should provide an
154
+ // early warning.
155
+ return ReactFragment.extract(fragment);
156
+ }
157
+ }
158
+ }
159
+ }
160
+ return fragment;
161
+ }
162
+};
163
+
164
+module.exports = ReactFragment;
src/addons/__tests__/ReactFragment-test.js
new
+76
@@ -0,0 +1,76 @@
1
+/**
2
+ * Copyright 2015, 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
+ * @emails react-core
10
+ */
11
+
12
+'use strict';
13
+
14
+var React;
15
+var ReactFragment;
16
+
17
+describe('ReactFragment', function() {
18
+
19
+ beforeEach(function() {
20
+ React = require('React');
21
+ ReactFragment = require('ReactFragment');
22
+ });
23
+
24
+ it('should warn if a plain object is used as a child', function() {
25
+ spyOn(console, 'warn');
26
+ var children = {
27
+ x: <span />,
28
+ y: <span />
29
+ };
30
+ <div>{children}</div>;
31
+ expect(console.warn.calls.length).toBe(1);
32
+ expect(console.warn.calls[0].args[0]).toContain(
33
+ 'Any use of a keyed object'
34
+ );
35
+ // Only warn once for the same set of children
36
+ var sameChildren = {
37
+ x: <span />,
38
+ y: <span />
39
+ };
40
+ <div>{sameChildren}</div>;
41
+ expect(console.warn.calls.length).toBe(1);
42
+ });
43
+
44
+ it('should warn if a plain object even if it is deep', function() {
45
+ spyOn(console, 'warn');
46
+ var children = {
47
+ x: <span />,
48
+ y: <span />,
49
+ z: <span />
50
+ };
51
+ var element = <div>{[children]}</div>;
52
+ expect(console.warn.calls.length).toBe(0);
53
+ var container = document.createElement('div');
54
+ React.render(element, container);
55
+ expect(console.warn.calls.length).toBe(1);
56
+ expect(console.warn.calls[0].args[0]).toContain(
57
+ 'Any use of a keyed object'
58
+ );
59
+ });
60
+
61
+ it('should warn if accessing any property on a fragment', function() {
62
+ spyOn(console, 'warn');
63
+ var children = {
64
+ x: <span />,
65
+ y: <span />
66
+ };
67
+ var frag = ReactFragment.create(children);
68
+ frag.x;
69
+ frag.y = 10;
70
+ expect(console.warn.calls.length).toBe(1);
71
+ expect(console.warn.calls[0].args[0]).toContain(
72
+ 'A ReactFragment is an opaque type'
73
+ );
74
+ });
75
+
76
+});
src/addons/transitions/ReactTransitionChildMapping.js
+6
-2
@@ -13,6 +13,7 @@
13
'use strict';
14
15
var ReactChildren = require('ReactChildren');
16
+var ReactFragment = require('ReactFragment');
17
18
var ReactTransitionChildMapping = {
19
/**
@@ -23,9 +24,12 @@ var ReactTransitionChildMapping = {
24
* @return {object} Mapping of key to child
25
*/
26
getChildMapping: function(children) {
26
- return ReactChildren.map(children, function(child) {
27
+ if (!children) {
28
+ return children;
29
+ }
30
+ return ReactFragment.extract(ReactChildren.map(children, function(child) {
31
return child;
28
- });
32
+ }));
33
},
34
35
/**
src/addons/transitions/ReactTransitionGroup.js
+4
-4
@@ -202,7 +202,7 @@ var ReactTransitionGroup = React.createClass({
202
render: function() {
203
// TODO: we could get rid of the need for the wrapper node
204
// by cloning a single child
205
- var childrenToRender = {};
205
+ var childrenToRender = [];
206
for (var key in this.state.children) {
207
var child = this.state.children[key];
208
if (child) {
@@ -211,10 +211,10 @@ var ReactTransitionGroup = React.createClass({
211
// already been removed. In case you need this behavior you can provide
212
// a childFactory function to wrap every child, even the ones that are
213
// leaving.
214
- childrenToRender[key] = cloneWithProps(
214
+ childrenToRender.push(cloneWithProps(
215
this.props.childFactory(child),
216
- {ref: key}
217
- );
216
+ {ref: key, key: key}
217
+ ));
218
}
219
}
220
return React.createElement(
src/browser/ReactWithAddons.js
+2
@@ -23,6 +23,7 @@ var React = require('React');
23
var ReactComponentWithPureRenderMixin =
24
require('ReactComponentWithPureRenderMixin');
25
var ReactCSSTransitionGroup = require('ReactCSSTransitionGroup');
26
+var ReactFragment = require('ReactFragment');
27
var ReactTransitionGroup = require('ReactTransitionGroup');
28
var ReactUpdates = require('ReactUpdates');
29
@@ -39,6 +40,7 @@ React.addons = {
40
batchedUpdates: ReactUpdates.batchedUpdates,
41
classSet: cx,
42
cloneWithProps: cloneWithProps,
43
+ createFragment: ReactFragment.create,
44
update: update
45
};
46
src/browser/__tests__/ReactDOM-test.js
+19
-19
@@ -75,37 +75,37 @@ describe('ReactDOM', function() {
75
*/
76
it("should purge the DOM cache when removing nodes", function() {
77
var myDiv = ReactTestUtils.renderIntoDocument(
78
- <div>{{
79
- theDog: <div className="dog" />,
80
- theBird: <div className="bird" />
81
- }}</div>
78
+ <div>
79
+ <div key="theDog" className="dog" />,
80
+ <div key="theBird" className="bird" />
81
+ </div>
82
);
83
// Warm the cache with theDog
84
myDiv.setProps({
85
- children: {
86
- theDog: <div className="dogbeforedelete" />,
87
- theBird: <div className="bird" />
88
- }
85
+ children: [
86
+ <div key="theDog" className="dogbeforedelete" />,
87
+ <div key="theBird" className="bird" />
88
+ ]
89
});
90
// Remove theDog - this should purge the cache
91
myDiv.setProps({
92
- children: {
93
- theBird: <div className="bird" />
94
- }
92
+ children: [
93
+ <div key="theBird" className="bird" />
94
+ ]
95
});
96
// Now, put theDog back. It's now a different DOM node.
97
myDiv.setProps({
98
- children: {
99
- theDog: <div className="dog" />,
100
- theBird: <div className="bird" />
101
- }
98
+ children: [
99
+ <div key="theDog" className="dog" />,
100
+ <div key="theBird" className="bird" />
101
+ ]
102
});
103
// Change the className of theDog. It will use the same element
104
myDiv.setProps({
105
- children: {
106
- theDog: <div className="bigdog" />,
107
- theBird: <div className="bird" />
108
- }
105
+ children: [
106
+ <div key="theDog" className="bigdog" />,
107
+ <div key="theBird" className="bird" />
108
+ ]
109
});
110
var root = myDiv.getDOMNode();
111
var dog = root.childNodes[0];
src/classic/element/ReactElementValidator.js
+13
-25
@@ -19,6 +19,7 @@
19
'use strict';
20
21
var ReactElement = require('ReactElement');
22
+var ReactFragment = require('ReactFragment');
23
var ReactPropTypeLocations = require('ReactPropTypeLocations');
24
var ReactPropTypeLocationNames = require('ReactPropTypeLocationNames');
25
var ReactCurrentOwner = require('ReactCurrentOwner');
@@ -175,21 +176,6 @@ function warnAndMonitorForKeyUse(warningID, message, element, parentType) {
176
console.warn(message);
177
}
178
178
-/**
179
- * Log that we're using an object map. We're considering deprecating this
180
- * feature and replace it with proper Map and ImmutableMap data structures.
181
- *
182
- * @internal
183
- */
184
-function monitorUseOfObjectMap() {
185
- var currentName = getCurrentOwnerDisplayName() || '';
186
- if (ownerHasMonitoredObjectMap.hasOwnProperty(currentName)) {
187
- return;
188
- }
189
- ownerHasMonitoredObjectMap[currentName] = true;
190
- monitorCodeUse('react_object_map_children');
191
-}
192
-
179
/**
180
* Ensure that every element either is passed in a static location, in an
181
* array with an explicit keys property defined, or in an object literal
@@ -213,19 +199,21 @@ function validateChildKeys(node, parentType) {
199
} else if (node) {
200
var iteratorFn = getIteratorFn(node);
201
// Entry iterators provide implicit keys.
216
- if (iteratorFn && iteratorFn !== node.entries) {
217
- var iterator = iteratorFn.call(node);
218
- var step;
219
- while (!(step = iterator.next()).done) {
220
- if (ReactElement.isValidElement(step.value)) {
221
- validateExplicitKey(step.value, parentType);
202
+ if (iteratorFn) {
203
+ if (iteratorFn !== node.entries) {
204
+ var iterator = iteratorFn.call(node);
205
+ var step;
206
+ while (!(step = iterator.next()).done) {
207
+ if (ReactElement.isValidElement(step.value)) {
208
+ validateExplicitKey(step.value, parentType);
209
+ }
210
}
211
}
212
} else if (typeof node === 'object') {
225
- monitorUseOfObjectMap();
226
- for (var key in node) {
227
- if (node.hasOwnProperty(key)) {
228
- validatePropertyKey(key, node[key], parentType);
213
+ var fragment = ReactFragment.extractIfFragment(node);
214
+ for (var key in fragment) {
215
+ if (fragment.hasOwnProperty(key)) {
216
+ validatePropertyKey(key, fragment[key], parentType);
217
}
218
}
219
}
src/classic/element/__tests__/ReactElementValidator-test.js
+15
-1
@@ -15,6 +15,7 @@
15
// classic JS without JSX.
16
17
var React;
18
+var ReactFragment;
19
var ReactTestUtils;
20
21
describe('ReactElementValidator', function() {
@@ -24,12 +25,17 @@ describe('ReactElementValidator', function() {
25
require('mock-modules').dumpCache();
26
27
React = require('React');
28
+ ReactFragment = require('ReactFragment');
29
ReactTestUtils = require('ReactTestUtils');
30
ComponentClass = React.createClass({
31
render: function() { return React.createElement('div'); }
32
});
33
});
34
35
+ function frag(obj) {
36
+ return ReactFragment.create(obj);
37
+ }
38
+
39
it('warns for keys for arrays of elements in rest args', function() {
40
spyOn(console, 'warn');
41
var Component = React.createFactory(ComponentClass);
@@ -135,7 +141,7 @@ describe('ReactElementValidator', function() {
141
spyOn(console, 'warn');
142
var Component = React.createFactory(ComponentClass);
143
138
- Component(null, { 1: Component(), 2: Component() });
144
+ Component(null, frag({ 1: Component(), 2: Component() }));
145
146
expect(console.warn.argsForCall.length).toBe(1);
147
expect(console.warn.argsForCall[0][0]).toContain(
@@ -316,4 +322,12 @@ describe('ReactElementValidator', function() {
322
expect(console.warn.calls.length).toBe(2);
323
});
324
325
+ it('should warn if a fragment is used without the wrapper', function () {
326
+ spyOn(console, 'warn');
327
+ var child = React.createElement('span');
328
+ React.createElement('div', null, { a: child, b: child });
329
+ expect(console.warn.calls.length).toBe(1);
330
+ expect(console.warn.calls[0].args[0]).toContain('use of a keyed object');
331
+ });
332
+
333
});
src/core/__tests__/ReactIdentity-test.js
+9
-3
@@ -12,6 +12,7 @@
12
'use strict';
13
14
var React;
15
+var ReactFragment;
16
var ReactTestUtils;
17
var reactComponentExpect;
18
var ReactMount;
@@ -21,6 +22,7 @@ describe('ReactIdentity', function() {
22
beforeEach(function() {
23
require('mock-modules').dumpCache();
24
React = require('React');
25
+ ReactFragment = require('ReactFragment');
26
ReactTestUtils = require('ReactTestUtils');
27
reactComponentExpect = require('reactComponentExpect');
28
ReactMount = require('ReactMount');
@@ -35,13 +37,17 @@ describe('ReactIdentity', function() {
37
expect(actual[1]).toEqual(expected[1]);
38
}
39
40
+ function frag(obj) {
41
+ return ReactFragment.create(obj);
42
+ }
43
+
44
it('should allow keyed objects to express identity', function() {
45
var instance =
46
<div>
41
- {{
47
+ {frag({
48
first: <div />,
49
second: <div />
44
- }}
50
+ })}
51
</div>;
52
53
instance = React.render(instance, document.createElement('div'));
@@ -106,7 +112,7 @@ describe('ReactIdentity', function() {
112
113
var map = {};
114
map[key] = span2;
109
- return <div>{[span1, map]}</div>;
115
+ return <div>{[span1, frag(map)]}</div>;
116
}
117
118
});
src/core/__tests__/ReactMultiChildReconcile-test.js
+6
-5
@@ -39,7 +39,7 @@ var stripEmptyValues = function(obj) {
39
* here. This relies on an implementation detail of the rendering system.
40
*/
41
var getOriginalKey = function(childName) {
42
- var match = childName.match(/^\.\$([^.]+)\:0$/);
42
+ var match = childName.match(/^\.\$([^.]+)$/);
43
expect(match).not.toBeNull();
44
return match[1];
45
};
@@ -107,13 +107,14 @@ var FriendsStatusDisplay = React.createClass({
107
return res;
108
},
109
render: function() {
110
- var children = null;
110
+ var children = [];
111
var key;
112
for (key in this.props.usernameToStatus) {
113
var status = this.props.usernameToStatus[key];
114
- children = children || {};
115
- children[key] = !status ? null :
116
- <StatusDisplay ref={key} status={status} />;
114
+ children.push(
115
+ !status ? null :
116
+ <StatusDisplay key={key} ref={key} status={status} />
117
+ );
118
}
119
return (
120
<div>
src/core/__tests__/ReactMultiChildText-test.js
+15
-12
@@ -16,10 +16,13 @@
16
require('mock-modules');
17
18
var React = require('React');
19
+var ReactFragment = require('ReactFragment');
20
var ReactTestUtils = require('ReactTestUtils');
21
22
var reactComponentExpect = require('reactComponentExpect');
23
24
+var frag = ReactFragment.create;
25
+
26
// Helpers
27
var testAllPermutations = function(testCases) {
28
for (var i = 0; i < testCases.length; i += 2) {
@@ -182,18 +185,18 @@ describe('ReactMultiChildText', function() {
185
['', 'foo', [true, <div />, 1.2, ''], 'foo'], ['', 'foo', <div />, '1.2', '', 'foo'],
186
187
// values inside objects
185
- [{a: true}, {a: true}], [],
186
- [{a: 1.2}, {a: 1.2}], ['1.2', '1.2'],
187
- [{a: ''}, {a: ''}], ['', ''],
188
- [{a: 'foo'}, {a: 'foo'}], ['foo', 'foo'],
189
- [{a: <div />}, {a: <div />}], [<div />, <div />],
190
-
191
- [{a: true, b: 1.2, c: <div />}, '', 'foo'], ['1.2', <div />, '', 'foo'],
192
- [1.2, '', {a: <div />, b: 'foo', c: true}], ['1.2', '', <div />, 'foo'],
193
- ['', {a: 'foo', b: <div />, c: true}, 1.2], ['', 'foo', <div />, '1.2'],
194
-
195
- [true, {a: 1.2, b: '', c: <div />, d: 'foo'}, true, 1.2], ['1.2', '', <div />, 'foo', '1.2'],
196
- ['', 'foo', {a: true, b: <div />, c: 1.2, d: ''}, 'foo'], ['', 'foo', <div />, '1.2', '', 'foo'],
188
+ [frag({a: true}), frag({a: true})], [],
189
+ [frag({a: 1.2}), frag({a: 1.2})], ['1.2', '1.2'],
190
+ [frag({a: ''}), frag({a: ''})], ['', ''],
191
+ [frag({a: 'foo'}), frag({a: 'foo'})], ['foo', 'foo'],
192
+ [frag({a: <div />}), frag({a: <div />})], [<div />, <div />],
193
+
194
+ [frag({a: true, b: 1.2, c: <div />}), '', 'foo'], ['1.2', <div />, '', 'foo'],
195
+ [1.2, '', frag({a: <div />, b: 'foo', c: true})], ['1.2', '', <div />, 'foo'],
196
+ ['', frag({a: 'foo', b: <div />, c: true}), 1.2], ['', 'foo', <div />, '1.2'],
197
+
198
+ [true, frag({a: 1.2, b: '', c: <div />, d: 'foo'}), true, 1.2], ['1.2', '', <div />, 'foo', '1.2'],
199
+ ['', 'foo', frag({a: true, b: <div />, c: 1.2, d: ''}), 'foo'], ['', 'foo', <div />, '1.2', '', 'foo'],
200
201
// values inside elements
202
[<div>{true}{1.2}{<div />}</div>, '', 'foo'], [<div />, '', 'foo'],
src/modern/element/__tests__/ReactJSXElementValidator-test.js
+7
-1
@@ -15,6 +15,7 @@
15
// of dynamic errors when using JSX with Flow.
16
17
var React;
18
+var ReactFragment;
19
var ReactTestUtils;
20
21
describe('ReactJSXElementValidator', function() {
@@ -24,6 +25,7 @@ describe('ReactJSXElementValidator', function() {
25
require('mock-modules').dumpCache();
26
27
React = require('React');
28
+ ReactFragment = require('ReactFragment');
29
ReactTestUtils = require('ReactTestUtils');
30
31
Component = class {
@@ -31,6 +33,10 @@ describe('ReactJSXElementValidator', function() {
33
};
34
});
35
36
+ function frag(obj) {
37
+ return ReactFragment.create(obj);
38
+ }
39
+
40
it('warns for keys for arrays of elements in children position', function() {
41
spyOn(console, 'warn');
42
@@ -128,7 +134,7 @@ describe('ReactJSXElementValidator', function() {
134
it('warns for numeric keys on objects as children', function() {
135
spyOn(console, 'warn');
136
131
- <Component>{{ 1: <Component />, 2: <Component /> }}</Component>;
137
+ <Component>{frag({ 1: <Component />, 2: <Component /> })}</Component>;
138
139
expect(console.warn.argsForCall.length).toBe(1);
140
expect(console.warn.argsForCall[0][0]).toContain(
src/utils/ReactChildren.js
+2
-1
@@ -12,6 +12,7 @@
12
'use strict';
13
14
var PooledClass = require('PooledClass');
15
+var ReactFragment = require('ReactFragment');
16
17
var traverseAllChildren = require('traverseAllChildren');
18
var warning = require('warning');
@@ -121,7 +122,7 @@ function mapChildren(children, func, context) {
122
var traverseContext = MapBookKeeping.getPooled(mapResult, func, context);
123
traverseAllChildren(children, mapSingleChildIntoContext, traverseContext);
124
MapBookKeeping.release(traverseContext);
124
- return mapResult;
125
+ return ReactFragment.create(mapResult);
126
}
127
128
function forEachSingleChildDummy(traverseContext, child, name, i) {
src/utils/__tests__/ReactChildren-test.js
+99
-34
@@ -14,12 +14,31 @@
14
describe('ReactChildren', function() {
15
var ReactChildren;
16
var React;
17
+ var ReactFragment;
18
19
beforeEach(function() {
20
ReactChildren = require('ReactChildren');
21
React = require('React');
22
+ ReactFragment = require('ReactFragment');
23
});
24
25
+ function frag(obj) {
26
+ return ReactFragment.create(obj);
27
+ }
28
+
29
+ function nthChild(mappedChildren, n) {
30
+ var result = null;
31
+ ReactChildren.forEach(mappedChildren, function(child, index) {
32
+ if (index === n) {
33
+ result = child;
34
+ }
35
+ });
36
+ return result;
37
+ }
38
+
39
+ function keyOfNthChild(mappedChildren, n) {
40
+ return Object.keys(ReactFragment.extract(mappedChildren))[n];
41
+ }
42
43
it('should support identity for simple', function() {
44
var callback = jasmine.createSpy().andCallFake(function(kid, index) {
@@ -37,7 +56,7 @@ describe('ReactChildren', function() {
56
callback.reset();
57
var mappedChildren = ReactChildren.map(instance.props.children, callback);
58
expect(callback).toHaveBeenCalledWith(simpleKid, 0);
40
- expect(mappedChildren[Object.keys(mappedChildren)[0]]).toBe(simpleKid);
59
+ expect(nthChild(mappedChildren, 0)).toBe(simpleKid);
60
});
61
62
it('should treat single arrayless child as being in array', function() {
@@ -52,7 +71,7 @@ describe('ReactChildren', function() {
71
callback.reset();
72
var mappedChildren = ReactChildren.map(instance.props.children, callback);
73
expect(callback).toHaveBeenCalledWith(simpleKid, 0);
55
- expect(mappedChildren[Object.keys(mappedChildren)[0]]).toBe(simpleKid);
74
+ expect(nthChild(mappedChildren, 0)).toBe(simpleKid);
75
});
76
77
it('should treat single child in array as expected', function() {
@@ -67,7 +86,7 @@ describe('ReactChildren', function() {
86
callback.reset();
87
var mappedChildren = ReactChildren.map(instance.props.children, callback);
88
expect(callback).toHaveBeenCalledWith(simpleKid, 0);
70
- expect(mappedChildren[Object.keys(mappedChildren)[0]]).toBe(simpleKid);
89
+ expect(nthChild(mappedChildren, 0)).toBe(simpleKid);
90
});
91
92
it('should pass key to returned component', function() {
@@ -80,11 +99,10 @@ describe('ReactChildren', function() {
99
var instance = <div>{simpleKid}</div>;
100
var mappedChildren = ReactChildren.map(instance.props.children, mapFn);
101
83
- var mappedKeys = Object.keys(mappedChildren);
84
- expect(mappedKeys.length).toBe(1);
85
- expect(mappedChildren[mappedKeys[0]]).not.toBe(simpleKid);
86
- expect(mappedChildren[mappedKeys[0]].props.children).toBe(simpleKid);
87
- expect(mappedKeys[0]).toBe('.$simple');
102
+ expect(ReactChildren.count(mappedChildren)).toBe(1);
103
+ expect(nthChild(mappedChildren, 0)).not.toBe(simpleKid);
104
+ expect(nthChild(mappedChildren, 0).props.children).toBe(simpleKid);
105
+ expect(keyOfNthChild(mappedChildren, 0)).toBe('.$simple');
106
});
107
108
it('should invoke callback with the right context', function() {
@@ -94,7 +112,8 @@ describe('ReactChildren', function() {
112
return this;
113
};
114
97
- var scopeTester = {};
115
+ // TODO: Use an object to test, after non-object fragments has fully landed.
116
+ var scopeTester = 'scope tester';
117
118
var simpleKid = <span key="simple" />;
119
var instance = <div>{simpleKid}</div>;
@@ -104,9 +123,8 @@ describe('ReactChildren', function() {
123
var mappedChildren =
124
ReactChildren.map(instance.props.children, callback, scopeTester);
125
107
- var mappedKeys = Object.keys(mappedChildren);
108
- expect(mappedKeys.length).toBe(1);
109
- expect(mappedChildren[mappedKeys[0]]).toBe(scopeTester);
126
+ expect(ReactChildren.count(mappedChildren)).toBe(1);
127
+ expect(nthChild(mappedChildren, 0)).toBe(scopeTester);
128
});
129
130
it('should be called for each child', function() {
@@ -149,28 +167,33 @@ describe('ReactChildren', function() {
167
168
var mappedChildren =
169
ReactChildren.map(instance.props.children, callback);
152
- var mappedKeys = Object.keys(mappedChildren);
170
expect(callback.calls.length).toBe(5);
154
- expect(mappedKeys.length).toBe(5);
171
+ expect(ReactChildren.count(mappedChildren)).toBe(5);
172
// Keys default to indices.
156
- expect(mappedKeys).toEqual(
173
+ expect([
174
+ keyOfNthChild(mappedChildren, 0),
175
+ keyOfNthChild(mappedChildren, 1),
176
+ keyOfNthChild(mappedChildren, 2),
177
+ keyOfNthChild(mappedChildren, 3),
178
+ keyOfNthChild(mappedChildren, 4)
179
+ ]).toEqual(
180
['.$keyZero', '.1', '.$keyTwo', '.3', '.$keyFour']
181
);
182
183
expect(callback).toHaveBeenCalledWith(zero, 0);
161
- expect(mappedChildren[mappedKeys[0]]).toBe(zeroMapped);
184
+ expect(nthChild(mappedChildren, 0)).toBe(zeroMapped);
185
186
expect(callback).toHaveBeenCalledWith(one, 1);
164
- expect(mappedChildren[mappedKeys[1]]).toBe(oneMapped);
187
+ expect(nthChild(mappedChildren, 1)).toBe(oneMapped);
188
189
expect(callback).toHaveBeenCalledWith(two, 2);
167
- expect(mappedChildren[mappedKeys[2]]).toBe(twoMapped);
190
+ expect(nthChild(mappedChildren, 2)).toBe(twoMapped);
191
192
expect(callback).toHaveBeenCalledWith(three, 3);
170
- expect(mappedChildren[mappedKeys[3]]).toBe(threeMapped);
193
+ expect(nthChild(mappedChildren, 3)).toBe(threeMapped);
194
195
expect(callback).toHaveBeenCalledWith(four, 4);
173
- expect(mappedChildren[mappedKeys[4]]).toBe(fourMapped);
196
+ expect(nthChild(mappedChildren, 4)).toBe(fourMapped);
197
});
198
199
@@ -204,11 +227,11 @@ describe('ReactChildren', function() {
227
228
var instance = (
229
<div>{
207
- [{
230
+ [frag({
231
firstHalfKey: [zero, one, two],
232
secondHalfKey: [three, four],
233
keyFive: five
211
- }]
234
+ })]
235
}</div>
236
);
237
@@ -222,11 +245,17 @@ describe('ReactChildren', function() {
245
callback.reset();
246
247
var mappedChildren = ReactChildren.map(instance.props.children, callback);
225
- var mappedKeys = Object.keys(mappedChildren);
248
expect(callback.calls.length).toBe(6);
227
- expect(mappedKeys.length).toBe(6);
249
+ expect(ReactChildren.count(mappedChildren)).toBe(6);
250
// Keys default to indices.
229
- expect(mappedKeys).toEqual([
251
+ expect([
252
+ keyOfNthChild(mappedChildren, 0),
253
+ keyOfNthChild(mappedChildren, 1),
254
+ keyOfNthChild(mappedChildren, 2),
255
+ keyOfNthChild(mappedChildren, 3),
256
+ keyOfNthChild(mappedChildren, 4),
257
+ keyOfNthChild(mappedChildren, 5)
258
+ ]).toEqual([
259
'.0:$firstHalfKey:0:$keyZero',
260
'.0:$firstHalfKey:0:1',
261
'.0:$firstHalfKey:0:$keyTwo',
@@ -236,22 +265,22 @@ describe('ReactChildren', function() {
265
]);
266
267
expect(callback).toHaveBeenCalledWith(zero, 0);
239
- expect(mappedChildren[mappedKeys[0]]).toBe(zeroMapped);
268
+ expect(nthChild(mappedChildren, 0)).toBe(zeroMapped);
269
270
expect(callback).toHaveBeenCalledWith(one, 1);
242
- expect(mappedChildren[mappedKeys[1]]).toBe(oneMapped);
271
+ expect(nthChild(mappedChildren, 1)).toBe(oneMapped);
272
273
expect(callback).toHaveBeenCalledWith(two, 2);
245
- expect(mappedChildren[mappedKeys[2]]).toBe(twoMapped);
274
+ expect(nthChild(mappedChildren, 2)).toBe(twoMapped);
275
276
expect(callback).toHaveBeenCalledWith(three, 3);
248
- expect(mappedChildren[mappedKeys[3]]).toBe(threeMapped);
277
+ expect(nthChild(mappedChildren, 3)).toBe(threeMapped);
278
279
expect(callback).toHaveBeenCalledWith(four, 4);
251
- expect(mappedChildren[mappedKeys[4]]).toBe(fourMapped);
280
+ expect(nthChild(mappedChildren, 4)).toBe(fourMapped);
281
282
expect(callback).toHaveBeenCalledWith(five, 5);
254
- expect(mappedChildren[mappedKeys[5]]).toBe(fiveMapped);
283
+ expect(nthChild(mappedChildren, 5)).toBe(fiveMapped);
284
});
285
286
it('should retain key across two mappings', function() {
@@ -325,7 +354,8 @@ describe('ReactChildren', function() {
354
var mapped = ReactChildren.map(instance.props.children, mapFn);
355
356
expect(console.warn.calls.length).toEqual(1);
328
- expect(mapped).toEqual({'.$something': zero});
357
+ expect(nthChild(mapped, 0)).toBe(zero);
358
+ expect(keyOfNthChild(mapped, 0)).toBe('.$something');
359
});
360
361
it('should return 0 for null children', function() {
@@ -380,14 +410,49 @@ describe('ReactChildren', function() {
410
411
var instance = (
412
<div>{
383
- [{
413
+ [frag({
414
firstHalfKey: [zero, one, two],
415
secondHalfKey: [three, four],
416
keyFive: five
387
- }]
417
+ })]
418
}</div>
419
);
420
var numberOfChildren = ReactChildren.count(instance.props.children);
421
expect(numberOfChildren).toBe(6);
422
});
423
+
424
+ it('should warn if a fragment is used without the wrapper', function () {
425
+ spyOn(console, 'warn');
426
+ var child = React.createElement('span');
427
+ ReactChildren.forEach({ a: child, b: child}, function(c) {
428
+ expect(c).toBe(child);
429
+ });
430
+ expect(console.warn.calls.length).toBe(1);
431
+ expect(console.warn.calls[0].args[0]).toContain('use of a keyed object');
432
+ });
433
+
434
+ it('should warn if a fragment is accessed', function () {
435
+ spyOn(console, 'warn');
436
+ var child = React.createElement('span');
437
+ var frag = ReactChildren.map([ child, child ], function(c) {
438
+ return c;
439
+ });
440
+ for (var key in frag) {
441
+ frag[key];
442
+ break;
443
+ }
444
+ expect(console.warn.calls.length).toBe(1);
445
+ expect(console.warn.calls[0].args[0]).toContain('is an opaque type');
446
+
447
+ var frag2 = ReactChildren.map([ child, child ], function(c) {
448
+ return c;
449
+ });
450
+ for (var key in frag2) {
451
+ frag2[key] = 123;
452
+ break;
453
+ }
454
+ expect(console.warn.calls.length).toBe(2);
455
+ expect(console.warn.calls[1].args[0]).toContain('is an immutable opaque');
456
+ });
457
+
458
});
src/utils/__tests__/onlyChild-test.js
+3
-1
@@ -14,11 +14,13 @@
14
describe('onlyChild', function() {
15
16
var React;
17
+ var ReactFragment;
18
var onlyChild;
19
var WrapComponent;
20
21
beforeEach(function() {
22
React = require('React');
23
+ ReactFragment = require('ReactFragment');
24
onlyChild = require('onlyChild');
25
WrapComponent = React.createClass({
26
render: function() {
@@ -64,7 +66,7 @@ describe('onlyChild', function() {
66
expect(function() {
67
var instance =
68
<WrapComponent>
67
- {{oneThing: <span />}}
69
+ {ReactFragment.create({oneThing: <span />})}
70
</WrapComponent>;
71
onlyChild(instance.props.children);
72
}).toThrow();
src/utils/__tests__/sliceChildren-test.js
+11
-4
@@ -14,6 +14,7 @@
14
describe('sliceChildren', function() {
15
16
var React;
17
+ var ReactFragment;
18
var ReactTestUtils;
19
20
var sliceChildren;
@@ -23,6 +24,7 @@ describe('sliceChildren', function() {
24
25
beforeEach(function() {
26
React = require('React');
27
+ ReactFragment = require('ReactFragment');
28
ReactTestUtils = require('ReactTestUtils');
29
30
sliceChildren = require('sliceChildren');
@@ -52,6 +54,11 @@ describe('sliceChildren', function() {
54
return rendered.props.children;
55
}
56
57
+ function testKeyValuePairs(children, expectedPairs) {
58
+ var obj = ReactFragment.extract(children);
59
+ expect(obj).toEqual(expectedPairs);
60
+ }
61
+
62
it('should render the whole set if start zero is supplied', function() {
63
var fullSet = [
64
<div key="A" />,
@@ -59,7 +66,7 @@ describe('sliceChildren', function() {
66
<div key="C" />
67
];
68
var children = renderAndSlice(fullSet, 0);
62
- expect(children).toEqual({
69
+ testKeyValuePairs(children, {
70
'.$A': fullSet[0],
71
'.$B': fullSet[1],
72
'.$C': fullSet[2]
@@ -73,7 +80,7 @@ describe('sliceChildren', function() {
80
<div key="C" />
81
];
82
var children = renderAndSlice(fullSet, 1);
76
- expect(children).toEqual({
83
+ testKeyValuePairs(children, {
84
'.$B': fullSet[1],
85
'.$C': fullSet[2]
86
});
@@ -87,7 +94,7 @@ describe('sliceChildren', function() {
94
<div key="D" />
95
];
96
var children = renderAndSlice(fullSet, 1, 2);
90
- expect(children).toEqual({
97
+ testKeyValuePairs(children, {
98
'.$B': fullSet[1]
99
});
100
});
@@ -103,7 +110,7 @@ describe('sliceChildren', function() {
110
.expectRenderedChild()
111
.instance();
112
106
- expect(rendered.props.children).toEqual({
113
+ testKeyValuePairs(rendered.props.children, {
114
'.1': b
115
});
116
});
src/utils/__tests__/traverseAllChildren-test.js
+9
-4
@@ -14,11 +14,16 @@
14
describe('traverseAllChildren', function() {
15
var traverseAllChildren;
16
var React;
17
+ var ReactFragment;
18
beforeEach(function() {
19
traverseAllChildren = require('traverseAllChildren');
20
React = require('React');
21
+ ReactFragment = require('ReactFragment');
22
});
23
24
+ function frag(obj) {
25
+ return ReactFragment.create(obj);
26
+ }
27
28
it('should support identity for simple', function() {
29
var traverseContext = [];
@@ -139,8 +144,8 @@ describe('traverseAllChildren', function() {
144
var instance = (
145
<div>
146
{div}
142
- {[{span}]}
143
- {{a: a}}
147
+ {[frag({span})]}
148
+ {frag({a: a})}
149
{'string'}
150
{1234}
151
{true}
@@ -206,11 +211,11 @@ describe('traverseAllChildren', function() {
211
212
var instance = (
213
<div>{
209
- [{
214
+ [frag({
215
firstHalfKey: [zero, one, two],
216
secondHalfKey: [three, four],
217
keyFive: five
213
- }]
218
+ })]
219
}</div>
220
);
221
src/utils/sliceChildren.js
+3
-1
@@ -11,6 +11,8 @@
11
12
'use strict';
13
14
+var ReactFragment = require('ReactFragment');
15
+
16
var flattenChildren = require('flattenChildren');
17
18
/**
@@ -43,7 +45,7 @@ function sliceChildren(children, start, end) {
45
break;
46
}
47
}
46
- return slicedChildren;
48
+ return ReactFragment.create(slicedChildren);
49
}
50
51
module.exports = sliceChildren;
src/utils/traverseAllChildren.js
+5
-3
@@ -12,6 +12,7 @@
12
'use strict';
13
14
var ReactElement = require('ReactElement');
15
+var ReactFragment = require('ReactFragment');
16
var ReactInstanceHandles = require('ReactInstanceHandles');
17
18
var getIteratorFn = require('getIteratorFn');
@@ -184,9 +185,10 @@ function traverseAllChildrenImpl(
185
'traverseAllChildren(...): Encountered an invalid child; DOM ' +
186
'elements are not valid children of React components.'
187
);
187
- for (var key in children) {
188
- if (children.hasOwnProperty(key)) {
189
- child = children[key];
188
+ var fragment = ReactFragment.extract(children);
189
+ for (var key in fragment) {
190
+ if (fragment.hasOwnProperty(key)) {
191
+ child = fragment[key];
192
nextName = (
193
(nameSoFar !== '' ? nameSoFar + SUBSEPARATOR : SEPARATOR) +
194
wrapUserProvidedKey(key) + SUBSEPARATOR +