@samitouri / QOS-React / commits / 47e49ae8b7

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