Skip to content

Commit 6abf025

Browse files
committed
Make createFragment return an array, disallow objects as children
1 parent b38509c commit 6abf025

File tree

15 files changed

+186
-525
lines changed

15 files changed

+186
-525
lines changed

src/addons/ReactFragment.js

Lines changed: 38 additions & 132 deletions
Original file line numberDiff line numberDiff line change
@@ -13,93 +13,35 @@
1313

1414
var ReactElement = require('ReactElement');
1515

16+
var invariant = require('invariant');
17+
var traverseAllChildren = require('traverseAllChildren');
1618
var warning = require('warning');
1719

1820
/**
1921
* We used to allow keyed objects to serve as a collection of ReactElements,
2022
* or nested sets. This allowed us a way to explicitly key a set a fragment of
2123
* components. This is now being replaced with an opaque data structure.
2224
* 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.
25+
* create a keyed fragment. The resulting data structure is an array.
2426
*/
2527

26-
var fragmentKey;
27-
var didWarnKey;
28-
var canWarnForReactFragment;
28+
var numericPropertyRegex = /^\d+$/;
2929

30-
if (__DEV__) {
31-
fragmentKey = '_reactFragment';
32-
didWarnKey = '_reactDidWarn';
33-
34-
try {
35-
// Feature test. Don't even try to issue this warning if we can't use
36-
// enumerable: false.
37-
38-
var dummy = function() {
39-
return 1;
40-
};
41-
42-
Object.defineProperty(
43-
{},
44-
fragmentKey,
45-
{enumerable: false, value: true}
46-
);
47-
48-
Object.defineProperty(
49-
{},
50-
'key',
51-
{enumerable: true, get: dummy}
52-
);
30+
var userProvidedKeyEscapeRegex = /\//g;
31+
function escapeUserProvidedKey(text) {
32+
return ('' + text).replace(userProvidedKeyEscapeRegex, '//');
33+
}
5334

54-
canWarnForReactFragment = true;
55-
} catch (x) {
56-
canWarnForReactFragment = false;
35+
function processSingleChildWithContext(ctx, child, childKey) {
36+
if (ReactElement.isValidElement(child)) {
37+
child = ReactElement.cloneAndReplaceKey(child, ctx.prefix + childKey);
5738
}
58-
59-
var proxyPropertyAccessWithWarning = function(obj, key) {
60-
Object.defineProperty(obj, key, {
61-
enumerable: true,
62-
get: function() {
63-
warning(
64-
this[didWarnKey],
65-
'A ReactFragment is an opaque type. Accessing any of its ' +
66-
'properties is deprecated. Pass it to one of the React.Children ' +
67-
'helpers.'
68-
);
69-
this[didWarnKey] = true;
70-
return this[fragmentKey][key];
71-
},
72-
set: function(value) {
73-
warning(
74-
this[didWarnKey],
75-
'A ReactFragment is an immutable opaque type. Mutating its ' +
76-
'properties is deprecated.'
77-
);
78-
this[didWarnKey] = true;
79-
this[fragmentKey][key] = value;
80-
},
81-
});
82-
};
83-
84-
var issuedWarnings = {};
85-
86-
var getFragmentKeyString = function(fragment) {
87-
var fragmentCacheKey = '';
88-
for (var key in fragment) {
89-
fragmentCacheKey += key + ':' + (typeof fragment[key]) + ',';
90-
}
91-
return fragmentCacheKey;
92-
};
93-
94-
var didWarnForFragment = function(fragmentCacheKey) {
95-
// We use the keys and the type of the value as a heuristic to dedupe the
96-
// warning to avoid spamming too much.
97-
var alreadyWarnedOnce = !!issuedWarnings[fragmentCacheKey];
98-
issuedWarnings[fragmentCacheKey] = true;
99-
return alreadyWarnedOnce;
100-
};
39+
// For text components, leave unkeyed
40+
ctx.result.push(child);
10141
}
10242

43+
var warnedAboutNumeric = false;
44+
10345
var ReactFragment = {
10446
// Wrap a keyed object in an opaque proxy that warns you if you access any
10547
// of its properties.
@@ -121,71 +63,35 @@ var ReactFragment = {
12163
);
12264
return object;
12365
}
124-
if (canWarnForReactFragment) {
125-
var proxy = {};
126-
Object.defineProperty(proxy, fragmentKey, {
127-
enumerable: false,
128-
value: object,
129-
});
130-
Object.defineProperty(proxy, didWarnKey, {
131-
writable: true,
132-
enumerable: false,
133-
value: false,
134-
});
135-
for (var key in object) {
136-
proxyPropertyAccessWithWarning(proxy, key);
137-
}
138-
Object.preventExtensions(proxy);
139-
return proxy;
140-
}
14166
}
142-
return object;
143-
},
144-
// Extract the original keyed object from the fragment opaque type. Warn if
145-
// a plain object is passed here.
146-
extract: function(fragment) {
147-
if (__DEV__) {
148-
if (canWarnForReactFragment) {
149-
if (!fragment[fragmentKey]) {
150-
var fragmentKeys = getFragmentKeyString(fragment);
67+
invariant(
68+
object.nodeType !== 1,
69+
'React.addons.createFragment(...): Encountered an invalid child; DOM ' +
70+
'elements are not valid children of React components.'
71+
);
72+
73+
var result = [];
74+
var context = {
75+
result: result,
76+
prefix: '',
77+
};
78+
79+
for (var key in object) {
80+
if (__DEV__) {
81+
if (!warnedAboutNumeric && numericPropertyRegex.test(key)) {
15182
warning(
152-
didWarnForFragment(fragmentKeys),
153-
'Any use of a keyed object should be wrapped in ' +
154-
'React.addons.createFragment(object) before being passed as a ' +
155-
'child. {%s}',
156-
fragmentKeys
83+
false,
84+
'React.addons.createFragment(...): Child objects should have ' +
85+
'non-numeric keys so ordering is preserved.'
15786
);
158-
return fragment;
159-
}
160-
return fragment[fragmentKey];
161-
}
162-
}
163-
return fragment;
164-
},
165-
// Check if this is a fragment and if so, extract the keyed object. If it
166-
// is a fragment-like object, warn that it should be wrapped. Ignore if we
167-
// can't determine what kind of object this is.
168-
extractIfFragment: function(fragment) {
169-
if (__DEV__) {
170-
if (canWarnForReactFragment) {
171-
// If it is the opaque type, return the keyed object.
172-
if (fragment[fragmentKey]) {
173-
return fragment[fragmentKey];
174-
}
175-
// Otherwise, check each property if it has an element, if it does
176-
// it is probably meant as a fragment, so we can warn early. Defer,
177-
// the warning to extract.
178-
for (var key in fragment) {
179-
if (fragment.hasOwnProperty(key) &&
180-
ReactElement.isValidElement(fragment[key])) {
181-
// This looks like a fragment object, we should provide an
182-
// early warning.
183-
return ReactFragment.extract(fragment);
184-
}
87+
warnedAboutNumeric = true;
18588
}
18689
}
90+
context.prefix = escapeUserProvidedKey(key) + '/';
91+
traverseAllChildren(object[key], processSingleChildWithContext, context);
18792
}
188-
return fragment;
93+
94+
return result;
18995
},
19096
};
19197

src/addons/__tests__/ReactFragment-test.js

Lines changed: 19 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -23,51 +23,23 @@ describe('ReactFragment', function() {
2323
ReactFragment = require('ReactFragment');
2424
});
2525

26-
it('should warn if a plain object is used as a child', function() {
27-
spyOn(console, 'error');
28-
var children = {
29-
x: <span />,
30-
y: <span />,
31-
};
32-
void <div>{children}</div>;
33-
expect(console.error.calls.length).toBe(1);
34-
expect(console.error.calls[0].args[0]).toContain(
35-
'Any use of a keyed object'
36-
);
37-
expect(console.error.calls[0].args[0]).toContain(
38-
'{x:object,y:object,}'
39-
);
40-
// Only warn once for the same set of children
41-
var sameChildren = {
42-
x: <span />,
43-
y: <span />,
44-
};
45-
void <div>{sameChildren}</div>;
46-
expect(console.error.calls.length).toBe(1);
47-
});
48-
49-
it('should warn if a plain object even if it is deep', function() {
50-
spyOn(console, 'error');
26+
it('should throw if a plain object is used as a child', function() {
5127
var children = {
5228
x: <span />,
5329
y: <span />,
5430
z: <span />,
5531
};
5632
var element = <div>{[children]}</div>;
57-
expect(console.error.calls.length).toBe(0);
5833
var container = document.createElement('div');
59-
ReactDOM.render(element, container);
60-
expect(console.error.calls.length).toBe(1);
61-
expect(console.error.calls[0].args[0]).toContain(
62-
'Any use of a keyed object'
63-
);
64-
expect(console.error.calls[0].args[0]).toContain(
65-
'{x:object,y:object,z:object,}'
34+
expect(() => ReactDOM.render(element, container)).toThrow(
35+
'Invariant Violation: Objects are not valid as a React child (found ' +
36+
'object with keys {x, y, z}). If you meant to render a collection of ' +
37+
'children, use an array instead or wrap the object using ' +
38+
'React.addons.createFragment(object).'
6639
);
6740
});
6841

69-
it('should warn if a plain object even if it is in an owner', function() {
70-
spyOn(console, 'error');
42+
it('should throw if a plain object even if it is in an owner', function() {
7143
class Foo {
7244
render() {
7345
var children = {
@@ -78,27 +50,23 @@ describe('ReactFragment', function() {
7850
return <div>{[children]}</div>;
7951
}
8052
}
81-
expect(console.error.calls.length).toBe(0);
8253
var container = document.createElement('div');
83-
ReactDOM.render(<Foo />, container);
84-
expect(console.error.calls.length).toBe(1);
85-
expect(console.error.calls[0].args[0]).toContain(
86-
'Any use of a keyed object'
54+
expect(() => ReactDOM.render(<Foo />, container)).toThrow(
55+
'Invariant Violation: Objects are not valid as a React child (found ' +
56+
'object with keys {a, b, c}). If you meant to render a collection of ' +
57+
'children, use an array instead or wrap the object using ' +
58+
'React.addons.createFragment(object). Check the render method of `Foo`.'
8759
);
8860
});
8961

90-
it('should warn if accessing any property on a fragment', function() {
62+
it('warns for numeric keys on objects as children', function() {
9163
spyOn(console, 'error');
92-
var children = {
93-
x: <span />,
94-
y: <span />,
95-
};
96-
var frag = ReactFragment.create(children);
97-
void frag.x;
98-
frag.y = 10;
99-
expect(console.error.calls.length).toBe(1);
100-
expect(console.error.calls[0].args[0]).toContain(
101-
'A ReactFragment is an opaque type'
64+
65+
ReactFragment.create({1: <span />, 2: <span />});
66+
67+
expect(console.error.argsForCall.length).toBe(1);
68+
expect(console.error.argsForCall[0][0]).toContain(
69+
'Child objects should have non-numeric keys so ordering is preserved.'
10270
);
10371
});
10472

src/addons/transitions/ReactTransitionChildMapping.js

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,12 @@
1212

1313
'use strict';
1414

15-
var ReactChildren = require('ReactChildren');
16-
var ReactFragment = require('ReactFragment');
15+
var flattenChildren = require('flattenChildren');
1716

1817
var ReactTransitionChildMapping = {
1918
/**
2019
* Given `this.props.children`, return an object mapping key to child. Just
21-
* simple syntactic sugar around ReactChildren.map().
20+
* simple syntactic sugar around flattenChildren().
2221
*
2322
* @param {*} children `this.props.children`
2423
* @return {object} Mapping of key to child
@@ -27,9 +26,7 @@ var ReactTransitionChildMapping = {
2726
if (!children) {
2827
return children;
2928
}
30-
return ReactFragment.extract(ReactChildren.map(children, function(child) {
31-
return child;
32-
}));
29+
return flattenChildren(children);
3330
},
3431

3532
/**

src/isomorphic/children/ReactChildren.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,7 @@ function countChildren(children, context) {
146146
}
147147

148148

149-
function pushSingleChildToArray(traverseContext, child, name, i) {
149+
function pushSingleChildToArray(traverseContext, child, name) {
150150
if (child == null) {
151151
return;
152152
}

0 commit comments

Comments
 (0)