Add component stack info to key validation warnings (#6799)
* Add component stack info to key validation warnings * Add `ReactComponentTreeDevtool.getStackAddendumByID`
Keyan Zhang committed
May 20, 2016 at 14:19 UTC
47e49ae8b7e41b566db4cabb47ee2f31e5b9bf0b
11 files changed
+375
-101
src/addons/transitions/ReactTransitionChildMapping.js
+7
-1
@@ -19,12 +19,18 @@ var ReactTransitionChildMapping = {
19
* simple syntactic sugar around flattenChildren().
20
*
21
* @param {*} children `this.props.children`
22
+ * @param {number=} selfDebugID Optional debugID of the current internal instance.
23
* @return {object} Mapping of key to child
24
*/
24
- getChildMapping: function(children) {
25
+ getChildMapping: function(children, selfDebugID) {
26
if (!children) {
27
return children;
28
}
29
+
30
+ if (__DEV__) {
31
+ return flattenChildren(children, selfDebugID);
32
+ }
33
+
34
return flattenChildren(children);
35
},
36
src/addons/transitions/ReactTransitionGroup.js
+46
-12
@@ -12,6 +12,7 @@
12
'use strict';
13
14
var React = require('React');
15
+var ReactInstanceMap = require('ReactInstanceMap');
16
var ReactTransitionChildMapping = require('ReactTransitionChildMapping');
17
18
var emptyFunction = require('emptyFunction');
@@ -38,6 +39,7 @@ var ReactTransitionGroup = React.createClass({
39
40
getInitialState: function() {
41
return {
42
+ // TODO: can we get useful debug information to show at this point?
43
children: ReactTransitionChildMapping.getChildMapping(this.props.children),
44
};
45
},
@@ -58,9 +60,17 @@ var ReactTransitionGroup = React.createClass({
60
},
61
62
componentWillReceiveProps: function(nextProps) {
61
- var nextChildMapping = ReactTransitionChildMapping.getChildMapping(
62
- nextProps.children
63
- );
63
+ var nextChildMapping;
64
+ if (__DEV__) {
65
+ nextChildMapping = ReactTransitionChildMapping.getChildMapping(
66
+ nextProps.children,
67
+ ReactInstanceMap.get(this)._debugID
68
+ );
69
+ } else {
70
+ nextChildMapping = ReactTransitionChildMapping.getChildMapping(
71
+ nextProps.children
72
+ );
73
+ }
74
var prevChildMapping = this.state.children;
75
76
this.setState({
@@ -123,9 +133,17 @@ var ReactTransitionGroup = React.createClass({
133
134
delete this.currentlyTransitioningKeys[key];
135
126
- var currentChildMapping = ReactTransitionChildMapping.getChildMapping(
127
- this.props.children
128
- );
136
+ var currentChildMapping;
137
+ if (__DEV__) {
138
+ currentChildMapping = ReactTransitionChildMapping.getChildMapping(
139
+ this.props.children,
140
+ ReactInstanceMap.get(this)._debugID
141
+ );
142
+ } else {
143
+ currentChildMapping = ReactTransitionChildMapping.getChildMapping(
144
+ this.props.children
145
+ );
146
+ }
147
148
if (!currentChildMapping || !currentChildMapping.hasOwnProperty(key)) {
149
// This was removed before it had fully appeared. Remove it.
@@ -155,9 +173,17 @@ var ReactTransitionGroup = React.createClass({
173
174
delete this.currentlyTransitioningKeys[key];
175
158
- var currentChildMapping = ReactTransitionChildMapping.getChildMapping(
159
- this.props.children
160
- );
176
+ var currentChildMapping;
177
+ if (__DEV__) {
178
+ currentChildMapping = ReactTransitionChildMapping.getChildMapping(
179
+ this.props.children,
180
+ ReactInstanceMap.get(this)._debugID
181
+ );
182
+ } else {
183
+ currentChildMapping = ReactTransitionChildMapping.getChildMapping(
184
+ this.props.children
185
+ );
186
+ }
187
188
if (!currentChildMapping || !currentChildMapping.hasOwnProperty(key)) {
189
// This was removed before it had fully entered. Remove it.
@@ -188,9 +214,17 @@ var ReactTransitionGroup = React.createClass({
214
215
delete this.currentlyTransitioningKeys[key];
216
191
- var currentChildMapping = ReactTransitionChildMapping.getChildMapping(
192
- this.props.children
193
- );
217
+ var currentChildMapping;
218
+ if (__DEV__) {
219
+ currentChildMapping = ReactTransitionChildMapping.getChildMapping(
220
+ this.props.children,
221
+ ReactInstanceMap.get(this)._debugID
222
+ );
223
+ } else {
224
+ currentChildMapping = ReactTransitionChildMapping.getChildMapping(
225
+ this.props.children
226
+ );
227
+ }
228
229
if (currentChildMapping && currentChildMapping.hasOwnProperty(key)) {
230
// This entered again before it fully left. Add it again.
src/addons/transitions/__tests__/ReactTransitionGroup-test.js
+33
@@ -20,6 +20,10 @@ var ReactTransitionGroup;
20
describe('ReactTransitionGroup', function() {
21
var container;
22
23
+ function normalizeCodeLocInfo(str) {
24
+ return str.replace(/\(at .+?:\d+\)/g, '(at **)');
25
+ }
26
+
27
beforeEach(function() {
28
React = require('React');
29
ReactDOM = require('ReactDOM');
@@ -269,4 +273,33 @@ describe('ReactTransitionGroup', function() {
273
'willLeave2', 'didLeave2', 'willUnmount0', 'willUnmount1', 'willUnmount2',
274
]);
275
});
276
+
277
+ it('should warn for duplicated keys with component stack info', function() {
278
+ spyOn(console, 'error');
279
+
280
+ var Component = React.createClass({
281
+ render: function() {
282
+ var children = [<div key="1"/>, <div key="1" />];
283
+ return <ReactTransitionGroup>{children}</ReactTransitionGroup>;
284
+ },
285
+ });
286
+
287
+ ReactDOM.render(<Component />, container);
288
+
289
+ expect(console.error.argsForCall.length).toBe(2);
290
+ expect(console.error.argsForCall[0][0]).toBe(
291
+ 'Warning: flattenChildren(...): ' +
292
+ 'Encountered two children with the same key, `1`. ' +
293
+ 'Child keys must be unique; when two children share a key, ' +
294
+ 'only the first child will be used.'
295
+ );
296
+ expect(normalizeCodeLocInfo(console.error.argsForCall[1][0])).toBe(
297
+ 'Warning: flattenChildren(...): ' +
298
+ 'Encountered two children with the same key, `1`. ' +
299
+ 'Child keys must be unique; when two children share a key, ' +
300
+ 'only the first child will be used.\n' +
301
+ ' in ReactTransitionGroup (at **)\n' +
302
+ ' in Component (at **)'
303
+ );
304
+ });
305
});
src/isomorphic/classic/element/ReactElementValidator.js
+31
-48
@@ -48,11 +48,25 @@ var ownerHasKeyUseWarning = {};
48
49
var loggedTypeFailures = {};
50
51
+function getCurrentComponentErrorInfo(parentType) {
52
+ var info = getDeclarationErrorAddendum();
53
+
54
+ if (!info) {
55
+ var parentName = typeof parentType === 'string' ?
56
+ parentType : parentType.displayName || parentType.name;
57
+ if (parentName) {
58
+ info = ` Check the top-level render call using <${parentName}>.`;
59
+ }
60
+ }
61
+ return info;
62
+}
63
+
64
/**
65
* Warn if the element doesn't have an explicit key assigned to it.
66
* This element is in an array. The array could grow and shrink or be
67
* reordered. All children that haven't already been validated are required to
55
- * have a "key" property assigned to it.
68
+ * have a "key" property assigned to it. Error statuses are cached so a warning
69
+ * will only be shown once.
70
*
71
* @internal
72
* @param {ReactElement} element Element that requires a key.
@@ -64,67 +78,36 @@ function validateExplicitKey(element, parentType) {
78
}
79
element._store.validated = true;
80
67
- var addenda = getAddendaForKeyUse('uniqueKey', element, parentType);
68
- if (addenda === null) {
69
- // we already showed the warning
70
- return;
71
- }
72
- warning(
73
- false,
74
- 'Each child in an array or iterator should have a unique "key" prop.' +
75
- '%s%s%s',
76
- addenda.parentOrOwner || '',
77
- addenda.childOwner || '',
78
- addenda.url || ''
81
+ var memoizer = ownerHasKeyUseWarning.uniqueKey || (
82
+ ownerHasKeyUseWarning.uniqueKey = {}
83
);
80
-}
81
-
82
-/**
83
- * Shared warning and monitoring code for the key warnings.
84
- *
85
- * @internal
86
- * @param {string} messageType A key used for de-duping warnings.
87
- * @param {ReactElement} element Component that requires a key.
88
- * @param {*} parentType element's parent's type.
89
- * @returns {?object} A set of addenda to use in the warning message, or null
90
- * if the warning has already been shown before (and shouldn't be shown again).
91
- */
92
-function getAddendaForKeyUse(messageType, element, parentType) {
93
- var addendum = getDeclarationErrorAddendum();
94
- if (!addendum) {
95
- var parentName = typeof parentType === 'string' ?
96
- parentType : parentType.displayName || parentType.name;
97
- if (parentName) {
98
- addendum = ` Check the top-level render call using <${parentName}>.`;
99
- }
100
- }
84
102
- var memoizer = ownerHasKeyUseWarning[messageType] || (
103
- ownerHasKeyUseWarning[messageType] = {}
104
- );
105
- if (memoizer[addendum]) {
106
- return null;
85
+ var currentComponentErrorInfo = getCurrentComponentErrorInfo(parentType);
86
+ if (memoizer[currentComponentErrorInfo]) {
87
+ return;
88
}
108
- memoizer[addendum] = true;
109
-
110
- var addenda = {
111
- parentOrOwner: addendum,
112
- url: ' See https://fb.me/react-warning-keys for more information.',
113
- childOwner: null,
114
- };
89
+ memoizer[currentComponentErrorInfo] = true;
90
91
// Usually the current owner is the offender, but if it accepts children as a
92
// property, it may be the creator of the child that's responsible for
93
// assigning it a key.
94
+ var childOwner = '';
95
if (element &&
96
element._owner &&
97
element._owner !== ReactCurrentOwner.current) {
98
// Give the component that originally created this child.
123
- addenda.childOwner =
99
+ childOwner =
100
` It was passed a child from ${element._owner.getName()}.`;
101
}
102
127
- return addenda;
103
+ warning(
104
+ false,
105
+ 'Each child in an array or iterator should have a unique "key" prop.' +
106
+ '%s%s See https://fb.me/react-warning-keys for more information.%s',
107
+ currentComponentErrorInfo,
108
+ childOwner,
109
+ ReactComponentTreeDevtool.getCurrentStackAddendum(element)
110
+ );
111
}
112
113
/**
src/isomorphic/classic/element/__tests__/ReactElementValidator-test.js
+45
-4
@@ -19,6 +19,10 @@ var ReactDOM;
19
var ReactTestUtils;
20
21
describe('ReactElementValidator', function() {
22
+ function normalizeCodeLocInfo(str) {
23
+ return str.replace(/\(at .+?:\d+\)/g, '(at **)');
24
+ }
25
+
26
var ComponentClass;
27
28
beforeEach(function() {
@@ -95,9 +99,10 @@ describe('ReactElementValidator', function() {
99
ReactTestUtils.renderIntoDocument(<Anonymous>{divs}</Anonymous>);
100
101
expect(console.error.argsForCall.length).toBe(1);
98
- expect(console.error.argsForCall[0][0]).toBe(
102
+ expect(normalizeCodeLocInfo(console.error.argsForCall[0][0])).toBe(
103
'Warning: Each child in an array or iterator should have a unique ' +
100
- '"key" prop. See https://fb.me/react-warning-keys for more information.'
104
+ '"key" prop. See https://fb.me/react-warning-keys for more information.\n' +
105
+ ' in div (at **)'
106
);
107
});
108
@@ -111,10 +116,46 @@ describe('ReactElementValidator', function() {
116
ReactTestUtils.renderIntoDocument(<div>{divs}</div>);
117
118
expect(console.error.argsForCall.length).toBe(1);
114
- expect(console.error.argsForCall[0][0]).toBe(
119
+ expect(normalizeCodeLocInfo(console.error.argsForCall[0][0])).toBe(
120
'Warning: Each child in an array or iterator should have a unique ' +
121
'"key" prop. Check the top-level render call using <div>. See ' +
117
- 'https://fb.me/react-warning-keys for more information.'
122
+ 'https://fb.me/react-warning-keys for more information.\n' +
123
+ ' in div (at **)'
124
+ );
125
+ });
126
+
127
+ it('warns for keys with component stack info', function() {
128
+ spyOn(console, 'error');
129
+
130
+ var Component = React.createClass({
131
+ render: function() {
132
+ return <div>{[<div />, <div />]}</div>;
133
+ },
134
+ });
135
+
136
+ var Parent = React.createClass({
137
+ render: function() {
138
+ return React.cloneElement(this.props.child);
139
+ },
140
+ });
141
+
142
+ var GrandParent = React.createClass({
143
+ render: function() {
144
+ return <Parent child={<Component />} />;
145
+ },
146
+ });
147
+
148
+ ReactTestUtils.renderIntoDocument(<GrandParent />);
149
+
150
+ expect(console.error.argsForCall.length).toBe(1);
151
+ expect(normalizeCodeLocInfo(console.error.argsForCall[0][0])).toBe(
152
+ 'Warning: Each child in an array or iterator should have a unique ' +
153
+ '"key" prop. Check the render method of `Component`. See ' +
154
+ 'https://fb.me/react-warning-keys for more information.\n' +
155
+ ' in div (at **)\n' +
156
+ ' in Component (at **)\n' +
157
+ ' in Parent (at **)\n' +
158
+ ' in GrandParent (at **)'
159
);
160
});
161
src/renderers/shared/devtools/ReactComponentTreeDevtool.js
+29
-23
@@ -48,6 +48,28 @@ function purgeDeep(id) {
48
}
49
}
50
51
+function describeComponentFrame(name, source, ownerName) {
52
+ return '\n in ' + name + (
53
+ source ?
54
+ ' (at ' + source.fileName.replace(/^.*[\\\/]/, '') + ':' +
55
+ source.lineNumber + ')' :
56
+ ownerName ?
57
+ ' (created by ' + ownerName + ')' :
58
+ ''
59
+ );
60
+}
61
+
62
+function describeID(id) {
63
+ var name = ReactComponentTreeDevtool.getDisplayName(id);
64
+ var element = ReactComponentTreeDevtool.getElement(id);
65
+ var ownerID = ReactComponentTreeDevtool.getOwnerID(id);
66
+ var ownerName;
67
+ if (ownerID) {
68
+ ownerName = ReactComponentTreeDevtool.getDisplayName(ownerID);
69
+ }
70
+ return describeComponentFrame(name, element._source, ownerName);
71
+}
72
+
73
var ReactComponentTreeDevtool = {
74
onSetDisplayName(id, displayName) {
75
updateTree(id, item => item.displayName = displayName);
@@ -154,28 +176,6 @@ var ReactComponentTreeDevtool = {
176
},
177
178
getCurrentStackAddendum(topElement) {
157
- function describeComponentFrame(name, source, ownerName) {
158
- return '\n in ' + name + (
159
- source ?
160
- ' (at ' + source.fileName.replace(/^.*[\\\/]/, '') + ':' +
161
- source.lineNumber + ')' :
162
- ownerName ?
163
- ' (created by ' + ownerName + ')' :
164
- ''
165
- );
166
- }
167
-
168
- function describeID(id) {
169
- var name = ReactComponentTreeDevtool.getDisplayName(id);
170
- var element = ReactComponentTreeDevtool.getElement(id);
171
- var ownerID = ReactComponentTreeDevtool.getOwnerID(id);
172
- var ownerName;
173
- if (ownerID) {
174
- ownerName = ReactComponentTreeDevtool.getDisplayName(ownerID);
175
- }
176
- return describeComponentFrame(name, element._source, ownerName);
177
- }
178
-
179
var info = '';
180
if (topElement) {
181
var type = topElement.type;
@@ -192,11 +192,17 @@ var ReactComponentTreeDevtool = {
192
193
var currentOwner = ReactCurrentOwner.current;
194
var id = currentOwner && currentOwner._debugID;
195
+
196
+ info += ReactComponentTreeDevtool.getStackAddendumByID(id);
197
+ return info;
198
+ },
199
+
200
+ getStackAddendumByID(id) {
201
+ var info = '';
202
while (id) {
203
info += describeID(id);
204
id = ReactComponentTreeDevtool.getParentID(id);
205
}
199
-
206
return info;
207
},
208
src/renderers/shared/stack/reconciler/ReactChildReconciler.js
+26
-5
@@ -13,13 +13,14 @@
13
14
var ReactReconciler = require('ReactReconciler');
15
16
+var ReactComponentTreeDevtool = require('ReactComponentTreeDevtool');
17
var instantiateReactComponent = require('instantiateReactComponent');
18
var KeyEscapeUtils = require('KeyEscapeUtils');
19
var shouldUpdateReactComponent = require('shouldUpdateReactComponent');
20
var traverseAllChildren = require('traverseAllChildren');
21
var warning = require('warning');
22
22
-function instantiateChild(childInstances, child, name) {
23
+function instantiateChild(childInstances, child, name, selfDebugID) {
24
// We found a component instance.
25
var keyUnique = (childInstances[name] === undefined);
26
if (__DEV__) {
@@ -27,8 +28,9 @@ function instantiateChild(childInstances, child, name) {
28
keyUnique,
29
'flattenChildren(...): Encountered two children with the same key, ' +
30
'`%s`. Child keys must be unique; when two children share a key, only ' +
30
- 'the first child will be used.',
31
- KeyEscapeUtils.unescape(name)
31
+ 'the first child will be used.%s',
32
+ KeyEscapeUtils.unescape(name),
33
+ ReactComponentTreeDevtool.getStackAddendumByID(selfDebugID)
34
);
35
}
36
if (child != null && keyUnique) {
@@ -50,12 +52,31 @@ var ReactChildReconciler = {
52
* @return {?object} A set of child instances.
53
* @internal
54
*/
53
- instantiateChildren: function(nestedChildNodes, transaction, context) {
55
+ instantiateChildren: function(
56
+ nestedChildNodes,
57
+ transaction,
58
+ context,
59
+ selfDebugID // __DEV__ only
60
+ ) {
61
if (nestedChildNodes == null) {
62
return null;
63
}
64
var childInstances = {};
58
- traverseAllChildren(nestedChildNodes, instantiateChild, childInstances);
65
+
66
+ if (__DEV__) {
67
+ traverseAllChildren(
68
+ nestedChildNodes,
69
+ (childInsts, child, name) => instantiateChild(
70
+ childInsts,
71
+ child,
72
+ name,
73
+ selfDebugID
74
+ ),
75
+ childInstances
76
+ );
77
+ } else {
78
+ traverseAllChildren(nestedChildNodes, instantiateChild, childInstances);
79
+ }
80
return childInstances;
81
},
82
src/renderers/shared/stack/reconciler/ReactMultiChild.js
+2
-2
@@ -178,7 +178,7 @@ var ReactMultiChild = {
178
try {
179
ReactCurrentOwner.current = this._currentElement._owner;
180
return ReactChildReconciler.instantiateChildren(
181
- nestedChildren, transaction, context
181
+ nestedChildren, transaction, context, this._debugID
182
);
183
} finally {
184
ReactCurrentOwner.current = null;
@@ -202,7 +202,7 @@ var ReactMultiChild = {
202
if (this._currentElement) {
203
try {
204
ReactCurrentOwner.current = this._currentElement._owner;
205
- nextChildren = flattenChildren(nextNestedChildrenElements);
205
+ nextChildren = flattenChildren(nextNestedChildrenElements, this._debugID);
206
} finally {
207
ReactCurrentOwner.current = null;
208
}
src/renderers/shared/stack/reconciler/__tests__/ReactChildReconciler-test.js
new
+84
@@ -0,0 +1,84 @@
1
+/**
2
+ * Copyright 2013-present, 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
+// NOTE: We're explicitly not using JSX here. This is intended to test
13
+// the current stack addendum without having source location added by babel.
14
+
15
+'use strict';
16
+
17
+var React;
18
+var ReactTestUtils;
19
+
20
+describe('ReactChildReconciler', function() {
21
+ function normalizeCodeLocInfo(str) {
22
+ return str.replace(/\(at .+?:\d+\)/g, '(at **)');
23
+ }
24
+
25
+ beforeEach(function() {
26
+ jest.resetModuleRegistry();
27
+
28
+ React = require('React');
29
+ ReactTestUtils = require('ReactTestUtils');
30
+ });
31
+
32
+ it('warns for duplicated keys', function() {
33
+ spyOn(console, 'error');
34
+
35
+ var Component = React.createClass({
36
+ render() {
37
+ return <div>{[<div key="1" />, <div key="1" />]}</div>;
38
+ },
39
+ });
40
+
41
+ ReactTestUtils.renderIntoDocument(<Component />);
42
+
43
+ expect(console.error.argsForCall.length).toBe(1);
44
+ expect(console.error.argsForCall[0][0]).toContain(
45
+ 'Child keys must be unique; when two children share a key, only the first child will be used.'
46
+ );
47
+ });
48
+
49
+ it('warns for duplicated keys with component stack info', function() {
50
+ spyOn(console, 'error');
51
+
52
+ var Component = React.createClass({
53
+ render: function() {
54
+ return <div>{[<div key="1" />, <div key="1" />]}</div>;
55
+ },
56
+ });
57
+
58
+ var Parent = React.createClass({
59
+ render: function() {
60
+ return React.cloneElement(this.props.child);
61
+ },
62
+ });
63
+
64
+ var GrandParent = React.createClass({
65
+ render: function() {
66
+ return <Parent child={<Component />} />;
67
+ },
68
+ });
69
+
70
+ ReactTestUtils.renderIntoDocument(<GrandParent />);
71
+
72
+ expect(console.error.argsForCall.length).toBe(1);
73
+ expect(normalizeCodeLocInfo(console.error.argsForCall[0][0])).toBe(
74
+ 'Warning: flattenChildren(...): ' +
75
+ 'Encountered two children with the same key, `1`. ' +
76
+ 'Child keys must be unique; when two children share a key, ' +
77
+ 'only the first child will be used.\n' +
78
+ ' in div (at **)\n' +
79
+ ' in Component (at **)\n' +
80
+ ' in Parent (at **)\n' +
81
+ ' in GrandParent (at **)'
82
+ );
83
+ });
84
+});
src/renderers/shared/stack/reconciler/__tests__/ReactMultiChild-test.js
+50
-1
@@ -12,8 +12,11 @@
12
'use strict';
13
14
describe('ReactMultiChild', function() {
15
- var React;
15
+ function normalizeCodeLocInfo(str) {
16
+ return str.replace(/\(at .+?:\d+\)/g, '(at **)');
17
+ }
18
19
+ var React;
20
var ReactDOM;
21
22
beforeEach(function() {
@@ -148,5 +151,51 @@ describe('ReactMultiChild', function() {
151
expect(mockMount.mock.calls.length).toBe(2);
152
expect(mockUnmount.mock.calls.length).toBe(1);
153
});
154
+
155
+ it('should warn for duplicated keys with component stack info', function() {
156
+ spyOn(console, 'error');
157
+
158
+ var container = document.createElement('div');
159
+
160
+ var WrapperComponent = React.createClass({
161
+ render: function() {
162
+ return <div>{this.props.children}</div>;
163
+ },
164
+ });
165
+
166
+ var Parent = React.createClass({
167
+ render: function() {
168
+ return (
169
+ <div>
170
+ <WrapperComponent>
171
+ {this.props.children}
172
+ </WrapperComponent>
173
+ </div>
174
+ );
175
+ },
176
+ });
177
+
178
+ ReactDOM.render(
179
+ <Parent>{[<div key="1"/>]}</Parent>,
180
+ container
181
+ );
182
+
183
+ ReactDOM.render(
184
+ <Parent>{[<div key="1"/>, <div key="1"/>]}</Parent>,
185
+ container
186
+ );
187
+
188
+ expect(console.error.argsForCall.length).toBe(1);
189
+ expect(normalizeCodeLocInfo(console.error.argsForCall[0][0])).toBe(
190
+ 'Warning: flattenChildren(...): ' +
191
+ 'Encountered two children with the same key, `1`. ' +
192
+ 'Child keys must be unique; when two children share a key, ' +
193
+ 'only the first child will be used.\n' +
194
+ ' in div (at **)\n' +
195
+ ' in WrapperComponent (at **)\n' +
196
+ ' in div (at **)\n' +
197
+ ' in Parent (at **)'
198
+ );
199
+ });
200
});
201
});
src/shared/utils/flattenChildren.js
+22
-5
@@ -11,6 +11,7 @@
11
12
'use strict';
13
14
+var ReactComponentTreeDevtool = require('ReactComponentTreeDevtool');
15
var KeyEscapeUtils = require('KeyEscapeUtils');
16
var traverseAllChildren = require('traverseAllChildren');
17
var warning = require('warning');
@@ -19,8 +20,9 @@ var warning = require('warning');
20
* @param {function} traverseContext Context passed through traversal.
21
* @param {?ReactComponent} child React child component.
22
* @param {!string} name String name of key path to child.
23
+ * @param {number=} selfDebugID Optional debugID of the current internal instance.
24
*/
23
-function flattenSingleChildIntoContext(traverseContext, child, name) {
25
+function flattenSingleChildIntoContext(traverseContext, child, name, selfDebugID) {
26
// We found a component instance.
27
var result = traverseContext;
28
var keyUnique = (result[name] === undefined);
@@ -29,8 +31,9 @@ function flattenSingleChildIntoContext(traverseContext, child, name) {
31
keyUnique,
32
'flattenChildren(...): Encountered two children with the same key, ' +
33
'`%s`. Child keys must be unique; when two children share a key, only ' +
32
- 'the first child will be used.',
33
- KeyEscapeUtils.unescape(name)
34
+ 'the first child will be used.%s',
35
+ KeyEscapeUtils.unescape(name),
36
+ ReactComponentTreeDevtool.getStackAddendumByID(selfDebugID)
37
);
38
}
39
if (keyUnique && child != null) {
@@ -43,12 +46,26 @@ function flattenSingleChildIntoContext(traverseContext, child, name) {
46
* children will not be included in the resulting object.
47
* @return {!object} flattened children keyed by name.
48
*/
46
-function flattenChildren(children) {
49
+function flattenChildren(children, selfDebugID) {
50
if (children == null) {
51
return children;
52
}
53
var result = {};
51
- traverseAllChildren(children, flattenSingleChildIntoContext, result);
54
+
55
+ if (__DEV__) {
56
+ traverseAllChildren(
57
+ children,
58
+ (traverseContext, child, name) => flattenSingleChildIntoContext(
59
+ traverseContext,
60
+ child,
61
+ name,
62
+ selfDebugID
63
+ ),
64
+ result
65
+ );
66
+ } else {
67
+ traverseAllChildren(children, flattenSingleChildIntoContext, result);
68
+ }
69
return result;
70
}
71