@samitouri / QOS-React-2 / commits / 90294ead4c

Warn for callback refs on functional components (Stack + Fiber) (#8635)

* Fiber: warn for refs on SFCs * Stateless refs: update warning to use component stack * Warn for function refs (stack and fiber) * Add owner reference to ref warnings * Centralize stack ref warnings in ReactRef.attachRef * Fiber stateless comp ref warning should only do work when necessary * ReactDebugCurrentFiber maybe FunctionalComponents should act this way instead * (chore) scripts/fiber/record-tests * Add component._compositeType to ReactInstance Flow definition * Don't handle 'stack inside fiber' case in the warning We don't have a test for it. It's easy to mess it up and print the wrong thing so instead of verifying it I'll just remove this bit. * Revert the change to getCurrentFiberOwnerName This happened to work, but it is just a coincidence. This change didn’t really match what the function was supposed to be doing. I’m not sure what the correct fix would be yet so this commit breaks tests. * Add component indirection to the tests using owner name This passes in Stack. It helps ensure we supply the correct owner name. * Invalid type invariant should read owner from element This brings the Fiber behavior in line with how Stack does it. The Fiber test was passing accidentally but broke down in more complicated cases (such as when we have an <Indirection> between the owner and the failing element). Now we can also remove the weird cases from getCurrentFiberOwnerName() that didn't really make sense but helped get the (incomplete) tests pass in the past. * Fiber should throw on a string ref inside functional component

Dustan Kasten committed Jan 9, 2017 at 18:09 UTC 90294ead4c627715cb70f20ff448bb0d34ee4c1b
17 files changed +179 -76
scripts/fiber/tests-failing.txt
-1
@@ -66,7 +66,6 @@ src/renderers/shared/shared/__tests__/ReactMultiChildText-test.js
66
67 src/renderers/shared/shared/__tests__/ReactStatelessComponent-test.js
68 * should warn when stateless component returns array
69 -* should throw on string refs in pure functions
69 * should warn when using non-React functions in JSX
70
71 src/renderers/shared/shared/__tests__/ReactUpdates-test.js
scripts/fiber/tests-passing-except-dev.txt
-1
@@ -116,4 +116,3 @@ src/renderers/shared/shared/__tests__/ReactMultiChild-test.js
116
117 src/renderers/shared/shared/__tests__/ReactStatelessComponent-test.js
118 * should warn for childContextTypes on a functional component
119 -* should warn when given a ref
scripts/fiber/tests-passing.txt
+3
@@ -1535,6 +1535,9 @@ src/renderers/shared/shared/__tests__/ReactStatelessComponent-test.js
1535 * should update stateless component
1536 * should unmount stateless component
1537 * should pass context thru stateless component
1538 +* should throw on string refs in pure functions
1539 +* should warn when given a string ref
1540 +* should warn when given a function ref
1541 * should provide a null ref
1542 * should use correct name in key warning
1543 * should support default props and prop types
src/renderers/shared/fiber/ReactChildFiber.js
+7 -5
@@ -78,14 +78,16 @@ function coerceRef(current: ?Fiber, element: ReactElement) {
78 let mixedRef = element.ref;
79 if (mixedRef != null && typeof mixedRef !== 'function') {
80 if (element._owner) {
81 - const ownerFiber : ?(Fiber | ReactInstance) = (element._owner : any);
81 + const owner : ?(Fiber | ReactInstance) = (element._owner : any);
82 let inst;
83 - if (ownerFiber) {
84 - if ((ownerFiber : any).tag === ClassComponent) {
85 - inst = (ownerFiber : any).stateNode;
83 + if (owner) {
84 + if (typeof owner.tag === 'number') {
85 + const ownerFiber = ((owner : any) : Fiber);
86 + invariant(ownerFiber.tag === ClassComponent, 'Stateless function components cannot have refs.');
87 + inst = ownerFiber.stateNode;
88 } else {
89 // Stack
88 - inst = (ownerFiber : any).getPublicInstance();
90 + inst = (owner : any).getPublicInstance();
91 }
92 }
93 invariant(inst, 'Missing owner for string ref %s', mixedRef);
src/renderers/shared/fiber/ReactDebugCurrentFiber.js
+2 -18
@@ -15,12 +15,6 @@
15 import type { Fiber } from 'ReactFiber';
16
17 if (__DEV__) {
18 - var {
19 - IndeterminateComponent,
20 - FunctionalComponent,
21 - ClassComponent,
22 - HostComponent,
23 - } = require('ReactTypeOfWork');
18 var getComponentName = require('getComponentName');
19 var { getStackAddendumByWorkInProgressFiber } = require('ReactComponentTreeHook');
20 }
@@ -31,18 +25,8 @@ function getCurrentFiberOwnerName() : string | null {
25 if (fiber == null) {
26 return null;
27 }
34 - switch (fiber.tag) {
35 - case IndeterminateComponent:
36 - case FunctionalComponent:
37 - case ClassComponent:
38 - return getComponentName(fiber);
39 - case HostComponent:
40 - if (fiber._debugOwner != null) {
41 - return getComponentName(fiber._debugOwner);
42 - }
43 - return null;
44 - default:
45 - return null;
28 + if (fiber._debugOwner != null) {
29 + return getComponentName(fiber._debugOwner);
30 }
31 }
32 return null;
src/renderers/shared/fiber/ReactFiber.js
+9 -4
@@ -50,7 +50,7 @@ var {
50 var invariant = require('invariant');
51
52 if (__DEV__) {
53 - var { getCurrentFiberOwnerName } = require('ReactDebugCurrentFiber');
53 + var getComponentName = require('getComponentName');
54 }
55
56 // A Fiber is work on a Component that needs to be done or was done. There can
@@ -300,7 +300,12 @@ exports.createHostRootFiber = function() : Fiber {
300 };
301
302 exports.createFiberFromElement = function(element : ReactElement, priorityLevel : PriorityLevel) : Fiber {
303 - const fiber = createFiberFromElementType(element.type, element.key);
303 + let owner = null;
304 + if (__DEV__) {
305 + owner = element._owner;
306 + }
307 +
308 + const fiber = createFiberFromElementType(element.type, element.key, owner);
309 fiber.pendingProps = element.props;
310 fiber.pendingWorkPriority = priorityLevel;
311
@@ -328,7 +333,7 @@ exports.createFiberFromText = function(content : string, priorityLevel : Priorit
333 return fiber;
334 };
335
331 -function createFiberFromElementType(type : mixed, key : null | string) : Fiber {
336 +function createFiberFromElementType(type : mixed, key : null | string, debugOwner : null | Fiber | ReactInstance) : Fiber {
337 let fiber;
338 if (typeof type === 'function') {
339 fiber = shouldConstruct(type) ?
@@ -363,7 +368,7 @@ function createFiberFromElementType(type : mixed, key : null | string) : Fiber {
368 ' You likely forgot to export your component from the file ' +
369 'it\'s defined in.';
370 }
366 - const ownerName = getCurrentFiberOwnerName();
371 + const ownerName = debugOwner ? getComponentName(debugOwner) : null;
372 if (ownerName) {
373 info += ' Check the render method of `' + ownerName + '`.';
374 }
src/renderers/shared/fiber/ReactFiberBeginWork.js
+18
@@ -63,6 +63,7 @@ var {
63 } = require('ReactTypeOfSideEffect');
64 var ReactCurrentOwner = require('ReactCurrentOwner');
65 var ReactFiberClassComponent = require('ReactFiberClassComponent');
66 +var warning = require('warning');
67
68 if (__DEV__) {
69 var ReactDebugCurrentFiber = require('ReactDebugCurrentFiber');
@@ -455,6 +456,23 @@ module.exports = function<T, P, I, TI, C, CX, CI>(
456 } else {
457 // Proceed under the assumption that this is a functional component
458 workInProgress.tag = FunctionalComponent;
459 + if (__DEV__) {
460 + if (workInProgress.ref != null) {
461 + let info = '';
462 + const ownerName = ReactDebugCurrentFiber.getCurrentFiberOwnerName();
463 + if (ownerName) {
464 + info += ' Check the render method of `' + ownerName + '`.';
465 + }
466 +
467 + warning(
468 + false,
469 + 'Stateless function components cannot be given refs. ' +
470 + 'Attempts to access this ref will fail.%s%s',
471 + info,
472 + ReactDebugCurrentFiber.getCurrentFiberStackAddendum()
473 + );
474 + }
475 + }
476 reconcileChildren(current, workInProgress, value);
477 return workInProgress.child;
478 }
src/renderers/shared/fiber/ReactReifiedYield.js
+4 -2
@@ -22,7 +22,8 @@ export type ReifiedYield = { continuation: Fiber, props: Object };
22 exports.createReifiedYield = function(yieldNode : ReactYield) : ReifiedYield {
23 var fiber = createFiberFromElementType(
24 yieldNode.continuation,
25 - yieldNode.key
25 + yieldNode.key,
26 + null // debugOwner
27 );
28 return {
29 continuation: fiber,
@@ -35,7 +36,8 @@ exports.createUpdatedReifiedYield = function(previousYield : ReifiedYield, yield
36 if (fiber.type !== yieldNode.continuation) {
37 fiber = createFiberFromElementType(
38 yieldNode.continuation,
38 - yieldNode.key
39 + yieldNode.key,
40 + null // debugOwner
41 );
42 }
43 return {
src/renderers/shared/fiber/__tests__/ReactIncrementalErrorHandling-test.js
+2 -4
@@ -739,9 +739,8 @@ describe('ReactIncrementalErrorHandling', () => {
739 }
740 }
741 const InvalidType = undefined;
742 - const brokenElement = <InvalidType />;
742 function BrokenRender(props) {
744 - return brokenElement;
743 + return <InvalidType />;
744 }
745
746 ReactNoop.render(
@@ -776,9 +775,8 @@ describe('ReactIncrementalErrorHandling', () => {
775 }
776
777 const InvalidType = undefined;
779 - const brokenElement = <InvalidType />;
778 function BrokenRender(props) {
781 - return props.fail ? brokenElement : <span />;
779 + return props.fail ? <InvalidType /> : <span />;
780 }
781
782 ReactNoop.render(
src/renderers/shared/fiber/__tests__/ReactIncrementalSideEffects-test.js
+13 -1
@@ -21,6 +21,10 @@ describe('ReactIncrementalSideEffects', () => {
21 ReactNoop = require('ReactNoop');
22 });
23
24 + function normalizeCodeLocInfo(str) {
25 + return str && str.replace(/\(at .+?:\d+\)/g, '(at **)');
26 + }
27 +
28 function div(...children) {
29 children = children.map(c => typeof c === 'string' ? { text: c } : c);
30 return { type: 'div', children, prop: undefined };
@@ -1069,7 +1073,7 @@ describe('ReactIncrementalSideEffects', () => {
1073 });
1074
1075 it('invokes ref callbacks after insertion/update/unmount', () => {
1072 -
1076 + spyOn(console, 'error');
1077 var classInstance = null;
1078
1079 var ops = [];
@@ -1129,6 +1133,14 @@ describe('ReactIncrementalSideEffects', () => {
1133 null,
1134 ]);
1135
1136 + expectDev(normalizeCodeLocInfo(console.error.calls.argsFor(0)[0])).toBe(
1137 + 'Warning: Stateless function components cannot be given refs. ' +
1138 + 'Attempts to access this ref will fail. Check the render method ' +
1139 + 'of `Foo`.\n' +
1140 + ' in FunctionalComponent (at **)\n' +
1141 + ' in div (at **)\n' +
1142 + ' in Foo (at **)'
1143 + );
1144 });
1145
1146 // TODO: Test that mounts, updates, refs, unmounts and deletions happen in the
src/renderers/shared/shared/__tests__/ReactComponent-test.js
+12 -3
@@ -352,16 +352,25 @@ describe('ReactComponent', () => {
352 it('includes owner name in the error about badly-typed elements', () => {
353 spyOn(console, 'error');
354
355 + var X = undefined;
356 +
357 + function Indirection(props) {
358 + return <div>{props.children}</div>;
359 + }
360 +
361 + function Bar() {
362 + return <Indirection><X /></Indirection>;
363 + }
364 +
365 function Foo() {
356 - var X = undefined;
357 - return <X />;
366 + return <Bar />;
367 }
368
369 expect(() => ReactTestUtils.renderIntoDocument(<Foo />)).toThrowError(
370 'Element type is invalid: expected a string (for built-in components) ' +
371 'or a class/function (for composite components) but got: undefined. ' +
372 'You likely forgot to export your component from the file it\'s ' +
364 - 'defined in. Check the render method of `Foo`.'
373 + 'defined in. Check the render method of `Bar`.'
374 );
375
376 // One warning for each element creation
src/renderers/shared/shared/__tests__/ReactStatelessComponent-test.js
+46 -7
@@ -20,6 +20,9 @@ function StatelessComponent(props) {
20 }
21
22 describe('ReactStatelessComponent', () => {
23 + function normalizeCodeLocInfo(str) {
24 + return str && str.replace(/\(at .+?:\d+\)/g, '(at **)');
25 + }
26
27 beforeEach(() => {
28 React = require('React');
@@ -146,24 +149,60 @@ describe('ReactStatelessComponent', () => {
149 );
150 });
151
149 - it('should warn when given a ref', () => {
152 + it('should warn when given a string ref', () => {
153 spyOn(console, 'error');
154
155 + function Indirection(props) {
156 + return <div>{props.children}</div>;
157 + }
158 +
159 class Parent extends React.Component {
153 - static displayName = 'Parent';
160 + render() {
161 + return <Indirection><StatelessComponent name="A" ref="stateless"/></Indirection>;
162 + }
163 + }
164
165 + ReactTestUtils.renderIntoDocument(<Parent/>);
166 +
167 + expectDev(console.error.calls.count()).toBe(1);
168 + expectDev(normalizeCodeLocInfo(console.error.calls.argsFor(0)[0])).toBe(
169 + 'Warning: Stateless function components cannot be given refs. ' +
170 + 'Attempts to access this ref will fail. Check the render method ' +
171 + 'of `Parent`.\n' +
172 + ' in StatelessComponent (at **)\n' +
173 + ' in div (at **)\n' +
174 + ' in Indirection (at **)\n' +
175 + ' in Parent (at **)'
176 + );
177 + });
178 +
179 + it('should warn when given a function ref', () => {
180 + spyOn(console, 'error');
181 + var ref = jasmine.createSpy().and.callFake((arg) => {
182 + expect(arg).toBe(null);
183 + });
184 +
185 + function Indirection(props) {
186 + return <div>{props.children}</div>;
187 + }
188 +
189 + class Parent extends React.Component {
190 render() {
156 - return <StatelessComponent name="A" ref="stateless"/>;
191 + return <Indirection><StatelessComponent name="A" ref={ref} /></Indirection>;
192 }
193 }
194
195 ReactTestUtils.renderIntoDocument(<Parent/>);
196
197 expectDev(console.error.calls.count()).toBe(1);
163 - expectDev(console.error.calls.argsFor(0)[0]).toContain(
164 - 'Stateless function components cannot be given refs ' +
165 - '(See ref "stateless" in StatelessComponent created by Parent). ' +
166 - 'Attempts to access this ref will fail.'
198 + expectDev(normalizeCodeLocInfo(console.error.calls.argsFor(0)[0])).toBe(
199 + 'Warning: Stateless function components cannot be given refs. ' +
200 + 'Attempts to access this ref will fail. Check the render method ' +
201 + 'of `Parent`.\n' +
202 + ' in StatelessComponent (at **)\n' +
203 + ' in div (at **)\n' +
204 + ' in Indirection (at **)\n' +
205 + ' in Parent (at **)'
206 );
207 });
208
src/renderers/shared/stack/reconciler/ReactCompositeComponent.js
+7 -26
@@ -13,6 +13,7 @@
13
14 var React = require('React');
15 var ReactComponentEnvironment = require('ReactComponentEnvironment');
16 +var ReactCompositeComponentTypes = require('ReactCompositeComponentTypes');
17 var ReactCurrentOwner = require('ReactCurrentOwner');
18 var ReactErrorUtils = require('ReactErrorUtils');
19 var ReactFeatureFlags = require('ReactFeatureFlags');
@@ -33,12 +34,6 @@ var warning = require('warning');
34
35 import type { ReactPropTypeLocations } from 'ReactPropTypeLocations';
36
36 -var CompositeTypes = {
37 - ImpureClass: 0,
38 - PureClass: 1,
39 - StatelessFunctional: 2,
40 -};
41 -
37 function StatelessComponent(Component) {
38 }
39 StatelessComponent.prototype.render = function() {
@@ -220,12 +215,12 @@ var ReactCompositeComponent = {
215 Component.displayName || Component.name || 'Component'
216 );
217 inst = new StatelessComponent(Component);
223 - this._compositeType = CompositeTypes.StatelessFunctional;
218 + this._compositeType = ReactCompositeComponentTypes.StatelessFunctional;
219 } else {
220 if (isPureComponent(Component)) {
226 - this._compositeType = CompositeTypes.PureClass;
221 + this._compositeType = ReactCompositeComponentTypes.PureClass;
222 } else {
228 - this._compositeType = CompositeTypes.ImpureClass;
223 + this._compositeType = ReactCompositeComponentTypes.ImpureClass;
224 }
225 }
226
@@ -881,7 +876,7 @@ var ReactCompositeComponent = {
876 shouldUpdate = inst.shouldComponentUpdate(nextProps, nextState, nextContext);
877 }
878 } else {
884 - if (this._compositeType === CompositeTypes.PureClass) {
879 + if (this._compositeType === ReactCompositeComponentTypes.PureClass) {
880 shouldUpdate =
881 !shallowEqual(prevProps, nextProps) ||
882 !shallowEqual(inst.state, nextState);
@@ -1215,7 +1210,7 @@ var ReactCompositeComponent = {
1210 */
1211 _renderValidatedComponent: function() {
1212 var renderedElement;
1218 - if (__DEV__ || this._compositeType !== CompositeTypes.StatelessFunctional) {
1213 + if (__DEV__ || this._compositeType !== ReactCompositeComponentTypes.StatelessFunctional) {
1214 ReactCurrentOwner.current = this;
1215 try {
1216 renderedElement =
@@ -1251,20 +1246,6 @@ var ReactCompositeComponent = {
1246 var inst = this.getPublicInstance();
1247 invariant(inst != null, 'Stateless function components cannot have refs.');
1248 var publicComponentInstance = component.getPublicInstance();
1254 - if (__DEV__) {
1255 - var componentName = component && component.getName ?
1256 - component.getName() : 'a component';
1257 - warning(
1258 - publicComponentInstance != null ||
1259 - component._compositeType !== CompositeTypes.StatelessFunctional,
1260 - 'Stateless function components cannot be given refs ' +
1261 - '(See ref "%s" in %s created by %s). ' +
1262 - 'Attempts to access this ref will fail.',
1263 - ref,
1264 - componentName,
1265 - this.getName()
1266 - );
1267 - }
1249 var refs = inst.refs === emptyObject ? (inst.refs = {}) : inst.refs;
1250 refs[ref] = publicComponentInstance;
1251 },
@@ -1307,7 +1288,7 @@ var ReactCompositeComponent = {
1288 */
1289 getPublicInstance: function() {
1290 var inst = this._instance;
1310 - if (this._compositeType === CompositeTypes.StatelessFunctional) {
1291 + if (this._compositeType === ReactCompositeComponentTypes.StatelessFunctional) {
1292 return null;
1293 }
1294 return inst;
src/renderers/shared/stack/reconciler/ReactCompositeComponentTypes.js new
+19
@@ -0,0 +1,19 @@
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 ReactCompositeComponentTypes
10 + * @flow
11 + */
12 +
13 +export type CompositeComponentTypes = 0 | 1 | 2;
14 +
15 +module.exports = {
16 + ImpureClass: 0,
17 + PureClass: 1,
18 + StatelessFunctional: 2,
19 +};
src/renderers/shared/stack/reconciler/ReactInstanceType.js
+2
@@ -13,6 +13,7 @@
13 'use strict';
14
15 import type {ReactElement} from 'ReactElementType';
16 +import type {CompositeComponentTypes} from 'ReactCompositeComponentTypes';
17
18 export type DebugID = number;
19
@@ -34,6 +35,7 @@ export type ReactInstance = {
35 attachRef: (ref: string, component: ReactInstance) => void,
36 detachRef: (ref: string) => void,
37 _rootNodeID: number,
38 + _compositeType: CompositeComponentTypes,
39
40 // ReactDOMComponent
41 _tag: string,
src/renderers/shared/stack/reconciler/ReactRef.js
+27
@@ -19,7 +19,34 @@ import type { ReactElement } from 'ReactElementType';
19
20 var ReactRef = {};
21
22 +if (__DEV__) {
23 + var ReactCompositeComponentTypes = require('ReactCompositeComponentTypes');
24 + var ReactComponentTreeHook = require('ReactComponentTreeHook');
25 + var warning = require('warning');
26 +}
27 +
28 function attachRef(ref, component, owner) {
29 + if (__DEV__) {
30 + let info = '';
31 + if (owner) {
32 + let ownerName;
33 + if (typeof owner.getName === 'function') {
34 + ownerName = owner.getName();
35 + }
36 + if (ownerName) {
37 + info += ' Check the render method of `' + ownerName + '`.';
38 + }
39 + }
40 +
41 + warning(
42 + component._compositeType !== ReactCompositeComponentTypes.StatelessFunctional,
43 + 'Stateless function components cannot be given refs. ' +
44 + 'Attempts to access this ref will fail.%s%s',
45 + info,
46 + ReactComponentTreeHook.getStackAddendumByID(component._debugID)
47 + );
48 + }
49 +
50 if (typeof ref === 'function') {
51 ref(component.getPublicInstance());
52 } else {
src/renderers/testing/__tests__/ReactTestRenderer-test.js
+8 -4
@@ -15,6 +15,9 @@ var React = require('React');
15 var ReactTestRenderer = require('ReactTestRenderer');
16
17 describe('ReactTestRenderer', () => {
18 + function normalizeCodeLocInfo(str) {
19 + return str && str.replace(/\(at .+?:\d+\)/g, '(at **)');
20 + }
21
22 it('renders a simple component', () => {
23 function Link() {
@@ -231,10 +234,11 @@ describe('ReactTestRenderer', () => {
234 ReactTestRenderer.create(<Baz />);
235 ReactTestRenderer.create(<Foo />);
236 expectDev(console.error.calls.count()).toBe(1);
234 - expectDev(console.error.calls.argsFor(0)[0]).toContain(
235 - 'Stateless function components cannot be given refs ' +
236 - '(See ref "foo" in Bar created by Foo). ' +
237 - 'Attempts to access this ref will fail.'
237 + expectDev(normalizeCodeLocInfo(console.error.calls.argsFor(0)[0])).toBe(
238 + 'Warning: Stateless function components cannot be given refs. Attempts ' +
239 + 'to access this ref will fail. Check the render method of `Foo`.\n' +
240 + ' in Bar (at **)\n' +
241 + ' in Foo (at **)'
242 );
243 });
244