@samitouri / QOS-React / commits / 49238b9440

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 = {};