Skip to content

Commit

Permalink
Warn for callback refs on functional components (Stack + Fiber) (face…
Browse files Browse the repository at this point in the history
…book#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
  • Loading branch information
iamdustan authored and gaearon committed Jan 9, 2017
1 parent b2cc91e commit 90294ea
Show file tree
Hide file tree
Showing 17 changed files with 179 additions and 76 deletions.
1 change: 0 additions & 1 deletion scripts/fiber/tests-failing.txt
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,6 @@ src/renderers/shared/shared/__tests__/ReactMultiChildText-test.js

src/renderers/shared/shared/__tests__/ReactStatelessComponent-test.js
* should warn when stateless component returns array
* should throw on string refs in pure functions
* should warn when using non-React functions in JSX

src/renderers/shared/shared/__tests__/ReactUpdates-test.js
Expand Down
1 change: 0 additions & 1 deletion scripts/fiber/tests-passing-except-dev.txt
Original file line number Diff line number Diff line change
Expand Up @@ -116,4 +116,3 @@ src/renderers/shared/shared/__tests__/ReactMultiChild-test.js

src/renderers/shared/shared/__tests__/ReactStatelessComponent-test.js
* should warn for childContextTypes on a functional component
* should warn when given a ref
3 changes: 3 additions & 0 deletions scripts/fiber/tests-passing.txt
Original file line number Diff line number Diff line change
Expand Up @@ -1535,6 +1535,9 @@ src/renderers/shared/shared/__tests__/ReactStatelessComponent-test.js
* should update stateless component
* should unmount stateless component
* should pass context thru stateless component
* should throw on string refs in pure functions
* should warn when given a string ref
* should warn when given a function ref
* should provide a null ref
* should use correct name in key warning
* should support default props and prop types
Expand Down
12 changes: 7 additions & 5 deletions src/renderers/shared/fiber/ReactChildFiber.js
Original file line number Diff line number Diff line change
Expand Up @@ -78,14 +78,16 @@ function coerceRef(current: ?Fiber, element: ReactElement) {
let mixedRef = element.ref;
if (mixedRef != null && typeof mixedRef !== 'function') {
if (element._owner) {
const ownerFiber : ?(Fiber | ReactInstance) = (element._owner : any);
const owner : ?(Fiber | ReactInstance) = (element._owner : any);
let inst;
if (ownerFiber) {
if ((ownerFiber : any).tag === ClassComponent) {
inst = (ownerFiber : any).stateNode;
if (owner) {
if (typeof owner.tag === 'number') {
const ownerFiber = ((owner : any) : Fiber);
invariant(ownerFiber.tag === ClassComponent, 'Stateless function components cannot have refs.');
inst = ownerFiber.stateNode;
} else {
// Stack
inst = (ownerFiber : any).getPublicInstance();
inst = (owner : any).getPublicInstance();
}
}
invariant(inst, 'Missing owner for string ref %s', mixedRef);
Expand Down
20 changes: 2 additions & 18 deletions src/renderers/shared/fiber/ReactDebugCurrentFiber.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,6 @@
import type { Fiber } from 'ReactFiber';

if (__DEV__) {
var {
IndeterminateComponent,
FunctionalComponent,
ClassComponent,
HostComponent,
} = require('ReactTypeOfWork');
var getComponentName = require('getComponentName');
var { getStackAddendumByWorkInProgressFiber } = require('ReactComponentTreeHook');
}
Expand All @@ -31,18 +25,8 @@ function getCurrentFiberOwnerName() : string | null {
if (fiber == null) {
return null;
}
switch (fiber.tag) {
case IndeterminateComponent:
case FunctionalComponent:
case ClassComponent:
return getComponentName(fiber);
case HostComponent:
if (fiber._debugOwner != null) {
return getComponentName(fiber._debugOwner);
}
return null;
default:
return null;
if (fiber._debugOwner != null) {
return getComponentName(fiber._debugOwner);
}
}
return null;
Expand Down
13 changes: 9 additions & 4 deletions src/renderers/shared/fiber/ReactFiber.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ var {
var invariant = require('invariant');

if (__DEV__) {
var { getCurrentFiberOwnerName } = require('ReactDebugCurrentFiber');
var getComponentName = require('getComponentName');
}

// A Fiber is work on a Component that needs to be done or was done. There can
Expand Down Expand Up @@ -300,7 +300,12 @@ exports.createHostRootFiber = function() : Fiber {
};

exports.createFiberFromElement = function(element : ReactElement, priorityLevel : PriorityLevel) : Fiber {
const fiber = createFiberFromElementType(element.type, element.key);
let owner = null;
if (__DEV__) {
owner = element._owner;
}

const fiber = createFiberFromElementType(element.type, element.key, owner);
fiber.pendingProps = element.props;
fiber.pendingWorkPriority = priorityLevel;

Expand Down Expand Up @@ -328,7 +333,7 @@ exports.createFiberFromText = function(content : string, priorityLevel : Priorit
return fiber;
};

function createFiberFromElementType(type : mixed, key : null | string) : Fiber {
function createFiberFromElementType(type : mixed, key : null | string, debugOwner : null | Fiber | ReactInstance) : Fiber {
let fiber;
if (typeof type === 'function') {
fiber = shouldConstruct(type) ?
Expand Down Expand Up @@ -363,7 +368,7 @@ function createFiberFromElementType(type : mixed, key : null | string) : Fiber {
' You likely forgot to export your component from the file ' +
'it\'s defined in.';
}
const ownerName = getCurrentFiberOwnerName();
const ownerName = debugOwner ? getComponentName(debugOwner) : null;
if (ownerName) {
info += ' Check the render method of `' + ownerName + '`.';
}
Expand Down
18 changes: 18 additions & 0 deletions src/renderers/shared/fiber/ReactFiberBeginWork.js
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ var {
} = require('ReactTypeOfSideEffect');
var ReactCurrentOwner = require('ReactCurrentOwner');
var ReactFiberClassComponent = require('ReactFiberClassComponent');
var warning = require('warning');

if (__DEV__) {
var ReactDebugCurrentFiber = require('ReactDebugCurrentFiber');
Expand Down Expand Up @@ -455,6 +456,23 @@ module.exports = function<T, P, I, TI, C, CX, CI>(
} else {
// Proceed under the assumption that this is a functional component
workInProgress.tag = FunctionalComponent;
if (__DEV__) {
if (workInProgress.ref != null) {
let info = '';
const ownerName = ReactDebugCurrentFiber.getCurrentFiberOwnerName();
if (ownerName) {
info += ' Check the render method of `' + ownerName + '`.';
}

warning(
false,
'Stateless function components cannot be given refs. ' +
'Attempts to access this ref will fail.%s%s',
info,
ReactDebugCurrentFiber.getCurrentFiberStackAddendum()
);
}
}
reconcileChildren(current, workInProgress, value);
return workInProgress.child;
}
Expand Down
6 changes: 4 additions & 2 deletions src/renderers/shared/fiber/ReactReifiedYield.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ export type ReifiedYield = { continuation: Fiber, props: Object };
exports.createReifiedYield = function(yieldNode : ReactYield) : ReifiedYield {
var fiber = createFiberFromElementType(
yieldNode.continuation,
yieldNode.key
yieldNode.key,
null // debugOwner
);
return {
continuation: fiber,
Expand All @@ -35,7 +36,8 @@ exports.createUpdatedReifiedYield = function(previousYield : ReifiedYield, yield
if (fiber.type !== yieldNode.continuation) {
fiber = createFiberFromElementType(
yieldNode.continuation,
yieldNode.key
yieldNode.key,
null // debugOwner
);
}
return {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -739,9 +739,8 @@ describe('ReactIncrementalErrorHandling', () => {
}
}
const InvalidType = undefined;
const brokenElement = <InvalidType />;
function BrokenRender(props) {
return brokenElement;
return <InvalidType />;
}

ReactNoop.render(
Expand Down Expand Up @@ -776,9 +775,8 @@ describe('ReactIncrementalErrorHandling', () => {
}

const InvalidType = undefined;
const brokenElement = <InvalidType />;
function BrokenRender(props) {
return props.fail ? brokenElement : <span />;
return props.fail ? <InvalidType /> : <span />;
}

ReactNoop.render(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ describe('ReactIncrementalSideEffects', () => {
ReactNoop = require('ReactNoop');
});

function normalizeCodeLocInfo(str) {
return str && str.replace(/\(at .+?:\d+\)/g, '(at **)');
}

function div(...children) {
children = children.map(c => typeof c === 'string' ? { text: c } : c);
return { type: 'div', children, prop: undefined };
Expand Down Expand Up @@ -1069,7 +1073,7 @@ describe('ReactIncrementalSideEffects', () => {
});

it('invokes ref callbacks after insertion/update/unmount', () => {

spyOn(console, 'error');
var classInstance = null;

var ops = [];
Expand Down Expand Up @@ -1129,6 +1133,14 @@ describe('ReactIncrementalSideEffects', () => {
null,
]);

expectDev(normalizeCodeLocInfo(console.error.calls.argsFor(0)[0])).toBe(
'Warning: Stateless function components cannot be given refs. ' +
'Attempts to access this ref will fail. Check the render method ' +
'of `Foo`.\n' +
' in FunctionalComponent (at **)\n' +
' in div (at **)\n' +
' in Foo (at **)'
);
});

// TODO: Test that mounts, updates, refs, unmounts and deletions happen in the
Expand Down
15 changes: 12 additions & 3 deletions src/renderers/shared/shared/__tests__/ReactComponent-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -352,16 +352,25 @@ describe('ReactComponent', () => {
it('includes owner name in the error about badly-typed elements', () => {
spyOn(console, 'error');

var X = undefined;

function Indirection(props) {
return <div>{props.children}</div>;
}

function Bar() {
return <Indirection><X /></Indirection>;
}

function Foo() {
var X = undefined;
return <X />;
return <Bar />;
}

expect(() => ReactTestUtils.renderIntoDocument(<Foo />)).toThrowError(
'Element type is invalid: expected a string (for built-in components) ' +
'or a class/function (for composite components) but got: undefined. ' +
'You likely forgot to export your component from the file it\'s ' +
'defined in. Check the render method of `Foo`.'
'defined in. Check the render method of `Bar`.'
);

// One warning for each element creation
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ function StatelessComponent(props) {
}

describe('ReactStatelessComponent', () => {
function normalizeCodeLocInfo(str) {
return str && str.replace(/\(at .+?:\d+\)/g, '(at **)');
}

beforeEach(() => {
React = require('React');
Expand Down Expand Up @@ -146,24 +149,60 @@ describe('ReactStatelessComponent', () => {
);
});

it('should warn when given a ref', () => {
it('should warn when given a string ref', () => {
spyOn(console, 'error');

function Indirection(props) {
return <div>{props.children}</div>;
}

class Parent extends React.Component {
static displayName = 'Parent';
render() {
return <Indirection><StatelessComponent name="A" ref="stateless"/></Indirection>;
}
}

ReactTestUtils.renderIntoDocument(<Parent/>);

expectDev(console.error.calls.count()).toBe(1);
expectDev(normalizeCodeLocInfo(console.error.calls.argsFor(0)[0])).toBe(
'Warning: Stateless function components cannot be given refs. ' +
'Attempts to access this ref will fail. Check the render method ' +
'of `Parent`.\n' +
' in StatelessComponent (at **)\n' +
' in div (at **)\n' +
' in Indirection (at **)\n' +
' in Parent (at **)'
);
});

it('should warn when given a function ref', () => {
spyOn(console, 'error');
var ref = jasmine.createSpy().and.callFake((arg) => {
expect(arg).toBe(null);
});

function Indirection(props) {
return <div>{props.children}</div>;
}

class Parent extends React.Component {
render() {
return <StatelessComponent name="A" ref="stateless"/>;
return <Indirection><StatelessComponent name="A" ref={ref} /></Indirection>;
}
}

ReactTestUtils.renderIntoDocument(<Parent/>);

expectDev(console.error.calls.count()).toBe(1);
expectDev(console.error.calls.argsFor(0)[0]).toContain(
'Stateless function components cannot be given refs ' +
'(See ref "stateless" in StatelessComponent created by Parent). ' +
'Attempts to access this ref will fail.'
expectDev(normalizeCodeLocInfo(console.error.calls.argsFor(0)[0])).toBe(
'Warning: Stateless function components cannot be given refs. ' +
'Attempts to access this ref will fail. Check the render method ' +
'of `Parent`.\n' +
' in StatelessComponent (at **)\n' +
' in div (at **)\n' +
' in Indirection (at **)\n' +
' in Parent (at **)'
);
});

Expand Down
Loading

0 comments on commit 90294ea

Please sign in to comment.