Warn if people mutate children. (#7001)
Jim committed
Jun 15, 2016 at 22:22 UTC
49238b944046abf086ba80ab5e984ce8a8175a1d
6 files changed
+142
src/isomorphic/classic/element/ReactElement.js
+8
@@ -97,6 +97,7 @@ var ReactElement = function(type, key, ref, self, source, owner, props) {
97
// This can be replaced with a WeakMap once they are implemented in
98
// commonly used development environments.
99
element._store = {};
100
+ var shadowChildren = Array.isArray(props.children) ? props.children.slice(0) : props.children;
101
102
// To make comparing ReactElements easier for testing purposes, we make
103
// the validation flag non-enumerable (where possible, which should
@@ -116,6 +117,12 @@ var ReactElement = function(type, key, ref, self, source, owner, props) {
117
writable: false,
118
value: self,
119
});
120
+ Object.defineProperty(element, '_shadowChildren', {
121
+ configurable: false,
122
+ enumerable: false,
123
+ writable: false,
124
+ value: shadowChildren,
125
+ });
126
// Two elements created in two different places should be considered
127
// equal for testing purposes and therefore we hide it from enumeration.
128
Object.defineProperty(element, '_source', {
@@ -127,6 +134,7 @@ var ReactElement = function(type, key, ref, self, source, owner, props) {
134
} else {
135
element._store.validated = false;
136
element._self = self;
137
+ element._shadowChildren = shadowChildren;
138
element._source = source;
139
}
140
if (Object.freeze) {
src/renderers/dom/shared/ReactDOMComponent.js
+14
@@ -713,6 +713,13 @@ ReactDOMComponent.Mixin = {
713
break;
714
}
715
716
+ if (__DEV__) {
717
+ if (this._debugID) {
718
+ var callback = () => ReactInstrumentation.debugTool.onComponentHasMounted(this._debugID);
719
+ transaction.getReactMountReady().enqueue(callback, this);
720
+ }
721
+ }
722
+
723
return mountImage;
724
},
725
@@ -933,6 +940,13 @@ ReactDOMComponent.Mixin = {
940
// reconciliation
941
transaction.getReactMountReady().enqueue(postUpdateSelectWrapper, this);
942
}
943
+
944
+ if (__DEV__) {
945
+ if (this._debugID) {
946
+ var callback = () => ReactInstrumentation.debugTool.onComponentHasUpdated(this._debugID);
947
+ transaction.getReactMountReady().enqueue(callback, this);
948
+ }
949
+ }
950
},
951
952
/**
src/renderers/shared/ReactDebugTool.js
+10
@@ -258,6 +258,14 @@ var ReactDebugTool = {
258
checkDebugID(debugID);
259
emitEvent('onHostOperation', debugID, type, payload);
260
},
261
+ onComponentHasMounted(debugID) {
262
+ checkDebugID(debugID);
263
+ emitEvent('onComponentHasMounted', debugID);
264
+ },
265
+ onComponentHasUpdated(debugID) {
266
+ checkDebugID(debugID);
267
+ emitEvent('onComponentHasUpdated', debugID);
268
+ },
269
onSetState() {
270
emitEvent('onSetState');
271
},
@@ -314,9 +322,11 @@ if (__DEV__) {
322
var ReactInvalidSetStateWarningDevTool = require('ReactInvalidSetStateWarningDevTool');
323
var ReactHostOperationHistoryDevtool = require('ReactHostOperationHistoryDevtool');
324
var ReactComponentTreeDevtool = require('ReactComponentTreeDevtool');
325
+ var ReactChildrenMutationWarningDevtool = require('ReactChildrenMutationWarningDevtool');
326
ReactDebugTool.addDevtool(ReactInvalidSetStateWarningDevTool);
327
ReactDebugTool.addDevtool(ReactComponentTreeDevtool);
328
ReactDebugTool.addDevtool(ReactHostOperationHistoryDevtool);
329
+ ReactDebugTool.addDevtool(ReactChildrenMutationWarningDevtool);
330
var url = (ExecutionEnvironment.canUseDOM && window.location.href) || '';
331
if ((/[?&]react_perf\b/).test(url)) {
332
ReactDebugTool.beginProfiling();
src/renderers/shared/devtools/ReactChildrenMutationWarningDevtool.js
new
+66
@@ -0,0 +1,66 @@
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
+ * @providesModule ReactChildrenMutationWarningDevtool
10
+ */
11
+
12
+'use strict';
13
+
14
+var ReactComponentTreeDevtool = require('ReactComponentTreeDevtool');
15
+
16
+var warning = require('warning');
17
+
18
+var elements = {};
19
+
20
+function handleElement(debugID, element) {
21
+ if (element == null) {
22
+ return;
23
+ }
24
+ if (element._shadowChildren === undefined) {
25
+ return;
26
+ }
27
+ if (element._shadowChildren === element.props.children) {
28
+ return;
29
+ }
30
+ var isMutated = false;
31
+ if (Array.isArray(element._shadowChildren)) {
32
+ if (element._shadowChildren.length === element.props.children.length) {
33
+ for (var i = 0; i < element._shadowChildren.length; i++) {
34
+ if (element._shadowChildren[i] !== element.props.children[i]) {
35
+ isMutated = true;
36
+ }
37
+ }
38
+ } else {
39
+ isMutated = true;
40
+ }
41
+ }
42
+ warning(
43
+ Array.isArray(element._shadowChildren) && !isMutated,
44
+ 'Component\'s children should not be mutated.%s',
45
+ ReactComponentTreeDevtool.getStackAddendumByID(debugID),
46
+ );
47
+}
48
+
49
+var ReactDOMUnknownPropertyDevtool = {
50
+ onBeforeMountComponent(debugID, element) {
51
+ elements[debugID] = element;
52
+ },
53
+ onBeforeUpdateComponent(debugID, element) {
54
+ elements[debugID] = element;
55
+ },
56
+ onComponentHasMounted(debugID) {
57
+ handleElement(debugID, elements[debugID]);
58
+ delete elements[debugID];
59
+ },
60
+ onComponentHasUpdated(debugID) {
61
+ handleElement(debugID, elements[debugID]);
62
+ delete elements[debugID];
63
+ },
64
+};
65
+
66
+module.exports = ReactDOMUnknownPropertyDevtool;
src/renderers/shared/stack/reconciler/ReactCompositeComponent.js
+14
@@ -342,6 +342,13 @@ var ReactCompositeComponentMixin = {
342
}
343
}
344
345
+ if (__DEV__) {
346
+ if (this._debugID) {
347
+ var callback = (component) => ReactInstrumentation.debugTool.onComponentHasMounted(this._debugID);
348
+ transaction.getReactMountReady().enqueue(callback, this);
349
+ }
350
+ }
351
+
352
return markup;
353
},
354
@@ -952,6 +959,13 @@ var ReactCompositeComponentMixin = {
959
);
960
}
961
}
962
+
963
+ if (__DEV__) {
964
+ if (this._debugID) {
965
+ var callback = () => ReactInstrumentation.debugTool.onComponentHasUpdated(this._debugID);
966
+ transaction.getReactMountReady().enqueue(callback, this);
967
+ }
968
+ }
969
},
970
971
/**
src/renderers/shared/stack/reconciler/__tests__/ReactComponent-test.js
+30
@@ -15,6 +15,10 @@ var React;
15
var ReactDOM;
16
var ReactTestUtils;
17
18
+function normalizeCodeLocInfo(str) {
19
+ return str.replace(/\(at .+?:\d+\)/g, '(at **)');
20
+}
21
+
22
describe('ReactComponent', function() {
23
beforeEach(function() {
24
React = require('React');
@@ -45,6 +49,32 @@ describe('ReactComponent', function() {
49
}).toThrow();
50
});
51
52
+ it('should warn when children are mutated before render', function() {
53
+ spyOn(console, 'error');
54
+ var children = [<span key={0} />, <span key={1} />, <span key={2} />];
55
+ var element = <div>{children}</div>;
56
+ children[1] = <p key={1} />; // Mutation is illegal
57
+ ReactTestUtils.renderIntoDocument(element);
58
+ expect(console.error.calls.count()).toBe(1);
59
+ expect(normalizeCodeLocInfo(console.error.calls.argsFor(0)[0])).toBe(
60
+ 'Warning: Component\'s children should not be mutated.\n in div (at **)'
61
+ );
62
+ });
63
+
64
+ it('should warn when children are mutated', function() {
65
+ spyOn(console, 'error');
66
+ var children = [<span key={0} />, <span key={1} />, <span key={2} />];
67
+ function Wrapper(props) {
68
+ props.children[1] = <p key={1} />; // Mutation is illegal
69
+ return <div>{props.children}</div>;
70
+ }
71
+ ReactTestUtils.renderIntoDocument(<Wrapper>{children}</Wrapper>);
72
+ expect(console.error.calls.count()).toBe(1);
73
+ expect(normalizeCodeLocInfo(console.error.calls.argsFor(0)[0])).toBe(
74
+ 'Warning: Component\'s children should not be mutated.\n in Wrapper (at **)'
75
+ );
76
+ });
77
+
78
it('should support refs on owned components', function() {
79
var innerObj = {};
80
var outerObj = {};