Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Fiber] Component lifecycle tests #8949

Merged
merged 5 commits into from
Feb 22, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 0 additions & 3 deletions scripts/fiber/tests-failing.txt
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
src/isomorphic/classic/__tests__/ReactContextValidator-test.js
* should pass previous context to lifecycles

src/renderers/__tests__/ReactComponentLifeCycle-test.js
* should carry through each of the phases of setup

src/renderers/__tests__/ReactComponentTreeHook-test.js
* can be retrieved by ID

Expand Down
7 changes: 0 additions & 7 deletions scripts/fiber/tests-passing-except-dev.txt
Original file line number Diff line number Diff line change
@@ -1,8 +1,3 @@
src/renderers/__tests__/ReactComponentLifeCycle-test.js
* should correctly determine if a component is mounted
* should correctly determine if a null component is mounted
* warns if findDOMNode is used inside render

src/renderers/__tests__/ReactComponentTreeHook-test.js
* uses displayName or Unknown for classic components
* uses displayName, name, or ReactComponent for modern components
Expand Down Expand Up @@ -62,8 +57,6 @@ src/renderers/__tests__/ReactComponentTreeHook-test.js
* works

src/renderers/__tests__/ReactCompositeComponent-test.js
* should warn about `forceUpdate` on unmounted components
* should warn about `setState` on unmounted components
* should warn about `setState` in render
* should warn about `setState` in getChildContext
* should disallow nested render calls
Expand Down
6 changes: 6 additions & 0 deletions scripts/fiber/tests-passing.txt
Original file line number Diff line number Diff line change
Expand Up @@ -547,7 +547,11 @@ src/renderers/__tests__/ReactComponentLifeCycle-test.js
* throws when accessing state in componentWillMount
* should allow update state inside of componentWillMount
* should not allow update state inside of getInitialState
* should correctly determine if a component is mounted
* should correctly determine if a null component is mounted
* isMounted should return false when unmounted
* warns if findDOMNode is used inside render
* should carry through each of the phases of setup
* should not throw when updating an auxiliary component
* should allow state updates in componentDidMount
* should call nested lifecycle methods in the right order
Expand Down Expand Up @@ -618,6 +622,8 @@ src/renderers/__tests__/ReactCompositeComponent-test.js
* should not pass this to getDefaultProps
* should use default values for undefined props
* should not mutate passed-in props object
* should warn about `forceUpdate` on unmounted components
* should warn about `setState` on unmounted components
* should silently allow `setState`, not call cb on unmounting components
* should cleanup even if render() fatals
* should call componentWillUnmount before unmounting
Expand Down
25 changes: 14 additions & 11 deletions src/renderers/__tests__/ReactComponentLifeCycle-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@

var React;
var ReactDOM;
var ReactInstanceMap;
var ReactTestUtils;

var clone = function(o) {
Expand Down Expand Up @@ -78,12 +77,9 @@ type ComponentLifeCycle =
'UNMOUNTED';

function getLifeCycleState(instance): ComponentLifeCycle {
var internalInstance = ReactInstanceMap.get(instance);
// Once a component gets mounted, it has an internal instance, once it
// gets unmounted, it loses that internal instance.
return internalInstance ?
'MOUNTED' :
'UNMOUNTED';
return instance.updater.isMounted(instance) ?
'MOUNTED' :
'UNMOUNTED';
}

/**
Expand All @@ -99,7 +95,6 @@ describe('ReactComponentLifeCycle', () => {
React = require('React');
ReactDOM = require('ReactDOM');
ReactTestUtils = require('ReactTestUtils');
ReactInstanceMap = require('ReactInstanceMap');
});

it('should not reuse an instance when it has been unmounted', () => {
Expand Down Expand Up @@ -336,6 +331,9 @@ describe('ReactComponentLifeCycle', () => {
});

it('should carry through each of the phases of setup', () => {
spyOn(console, 'error');


class LifeCycleComponent extends React.Component {
constructor(props, context) {
super(props, context);
Expand Down Expand Up @@ -410,7 +408,7 @@ describe('ReactComponentLifeCycle', () => {
instance._testJournal.returnedFromGetInitialState
);
expect(instance._testJournal.lifeCycleAtStartOfWillMount)
.toBe('MOUNTED');
.toBe('UNMOUNTED');

// componentDidMount
expect(instance._testJournal.stateAtStartOfDidMount)
Expand All @@ -419,11 +417,11 @@ describe('ReactComponentLifeCycle', () => {
'MOUNTED'
);

// render
// initial render
expect(instance._testJournal.stateInInitialRender)
.toEqual(INIT_RENDER_STATE);
expect(instance._testJournal.lifeCycleInInitialRender).toBe(
'MOUNTED'
'UNMOUNTED'
);

expect(getLifeCycleState(instance)).toBe('MOUNTED');
Expand Down Expand Up @@ -452,6 +450,11 @@ describe('ReactComponentLifeCycle', () => {
// But the current lifecycle of the component is unmounted.
expect(getLifeCycleState(instance)).toBe('UNMOUNTED');
expect(instance.state).toEqual(POST_WILL_UNMOUNT_STATE);

expectDev(console.error.calls.count()).toBe(1);
expectDev(console.error.calls.argsFor(0)[0]).toContain(
'LifeCycleComponent is accessing isMounted inside its render() function'
);
});

it('should not throw when updating an auxiliary component', () => {
Expand Down
27 changes: 13 additions & 14 deletions src/renderers/__tests__/ReactCompositeComponent-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -277,10 +277,10 @@ describe('ReactCompositeComponent', () => {

instance.forceUpdate();
expectDev(console.error.calls.count()).toBe(1);
expectDev(console.error.calls.argsFor(0)[0]).toBe(
'Warning: forceUpdate(...): Can only update a mounted or ' +
'mounting component. This usually means you called forceUpdate() on an ' +
'unmounted component. This is a no-op.\n\nPlease check the code for the ' +
expectDev(console.error.calls.argsFor(0)[0]).toContain(
'Can only update a mounted or mounting component. This usually means ' +
'you called setState, replaceState, or forceUpdate on an unmounted ' +
'component. This is a no-op.\n\nPlease check the code for the ' +
'Component component.'
);
});
Expand Down Expand Up @@ -321,10 +321,10 @@ describe('ReactCompositeComponent', () => {
expect(renders).toBe(2);

expectDev(console.error.calls.count()).toBe(1);
expectDev(console.error.calls.argsFor(0)[0]).toBe(
'Warning: setState(...): Can only update a mounted or ' +
'mounting component. This usually means you called setState() on an ' +
'unmounted component. This is a no-op.\n\nPlease check the code for the ' +
expectDev(console.error.calls.argsFor(0)[0]).toContain(
'Can only update a mounted or mounting component. This usually means ' +
'you called setState, replaceState, or forceUpdate on an unmounted ' +
'component. This is a no-op.\n\nPlease check the code for the ' +
'Component component.'
);
});
Expand Down Expand Up @@ -384,12 +384,11 @@ describe('ReactCompositeComponent', () => {
var instance = ReactDOM.render(<Component />, container);

expectDev(console.error.calls.count()).toBe(1);
expectDev(console.error.calls.argsFor(0)[0]).toBe(
'Warning: setState(...): Cannot update during an existing state ' +
'transition (such as within `render` or another component\'s ' +
'constructor). Render methods should be a pure function of props and ' +
'state; constructor side-effects are an anti-pattern, but can be moved ' +
'to `componentWillMount`.'
expectDev(console.error.calls.argsFor(0)[0]).toContain(
'Cannot update during an existing state transition (such as within ' +
'`render` or another component\'s constructor). Render methods should ' +
'be a pure function of props and state; constructor side-effects are ' +
'an anti-pattern, but can be moved to `componentWillMount`.'
);

// The setState call is queued and then executed as a second pass. This
Expand Down
16 changes: 12 additions & 4 deletions src/renderers/dom/shared/findDOMNode.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,18 +26,26 @@ let findStack = function(arg) {

const findDOMNode = function(componentOrElement : Element | ?ReactComponent<any, any, any>) : null | Element | Text {
if (__DEV__) {
var owner = ReactCurrentOwner.current;
if (owner !== null && '_warnedAboutRefsInRender' in owner) {
var owner = (ReactCurrentOwner.current : any);
if (owner !== null) {
var isFiber = typeof owner.tag === 'number';
var warnedAboutRefsInRender = isFiber ?
owner.stateNode._warnedAboutRefsInRender :
owner._warnedAboutRefsInRender;
warning(
(owner: any)._warnedAboutRefsInRender,
warnedAboutRefsInRender,
'%s is accessing findDOMNode inside its render(). ' +
'render() should be a pure function of props and state. It should ' +
'never access something that requires stale data from the previous ' +
'render, such as refs. Move this logic to componentDidMount and ' +
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oops. Should we preventExtensions on fibers? Like we do in Stack instantiate.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yea. Let's add it in the constructor in DEV.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But expandos on the class isn't really better neither.

'componentDidUpdate instead.',
getComponentName(owner) || 'A component'
);
(owner: any)._warnedAboutRefsInRender = true;
if (isFiber) {
owner.stateNode._warnedAboutRefsInRender = true;
} else {
owner._warnedAboutRefsInRender = true;
}
}
}
if (componentOrElement == null) {
Expand Down
22 changes: 21 additions & 1 deletion src/renderers/shared/fiber/ReactFiberScheduler.js
Original file line number Diff line number Diff line change
Expand Up @@ -86,8 +86,21 @@ var {
var invariant = require('invariant');

if (__DEV__) {
var warning = require('warning');
var ReactFiberInstrumentation = require('ReactFiberInstrumentation');
var ReactDebugCurrentFiber = require('ReactDebugCurrentFiber');

var warnAboutUpdateOnUnmounted = function(instance : ReactClass<any>) {
const ctor = instance.constructor;
warning(
false,
'Can only update a mounted or mounting component. This usually means ' +
'you called setState, replaceState, or forceUpdate on an unmounted ' +
'component. This is a no-op.\n\nPlease check the code for the ' +
'%s component.',
ctor && (ctor.displayName || ctor.name) || 'ReactClass'
);
};
}

var timeHeuristicForUnitOfWork = 1;
Expand Down Expand Up @@ -349,6 +362,9 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(config : HostConfig<T, P,
'in React. Please file an issue.'
);

// Reset this to null before calling lifecycles
ReactCurrentOwner.current = null;

// Updates that occur during the commit phase should have Task priority
const previousPriorityContext = priorityContext;
priorityContext = TaskPriority;
Expand Down Expand Up @@ -1134,7 +1150,11 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(config : HostConfig<T, P,
return;
}
} else {
// TODO: Warn about setting state on an unmounted component.
if (__DEV__) {
if (fiber.tag === ClassComponent) {
warnAboutUpdateOnUnmounted(fiber.stateNode);
}
}
return;
}
}
Expand Down
29 changes: 27 additions & 2 deletions src/renderers/shared/fiber/ReactFiberTreeReflection.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,19 @@
import type { Fiber } from 'ReactFiber';

var ReactInstanceMap = require('ReactInstanceMap');
var ReactCurrentOwner = require('ReactCurrentOwner');

var invariant = require('invariant');

if (__DEV__) {
var warning = require('warning');
}

var {
HostRoot,
HostComponent,
HostText,
ClassComponent,
} = require('ReactTypeOfWork');

var {
Expand Down Expand Up @@ -66,6 +72,24 @@ exports.isFiberMounted = function(fiber : Fiber) : boolean {
};

exports.isMounted = function(component : ReactComponent<any, any, any>) : boolean {
if (__DEV__) {
const owner = (ReactCurrentOwner.current : any);
if (owner !== null && owner.tag === ClassComponent) {
const ownerFiber : Fiber = owner;
const instance = ownerFiber.stateNode;
warning(
instance._warnedAboutRefsInRender,
'%s is accessing isMounted inside its render() function. ' +
'render() should be a pure function of props and state. It should ' +
'never access something that requires stale data from the previous ' +
'render, such as refs. Move this logic to componentDidMount and ' +
'componentDidUpdate instead.',
getComponentName(ownerFiber)
);
instance._warnedAboutRefsInRender = true;
}
}

var fiber : ?Fiber = ReactInstanceMap.get(component);
if (!fiber) {
return false;
Expand Down Expand Up @@ -243,7 +267,7 @@ exports.findCurrentHostFiber = function(parent : Fiber) : Fiber | null {
return null;
};

exports.getComponentName = function(fiber: Fiber): string {
function getComponentName(fiber: Fiber): string {
const type = fiber.type;
const instance = fiber.stateNode;
const constructor = instance && instance.constructor;
Expand All @@ -252,4 +276,5 @@ exports.getComponentName = function(fiber: Fiber): string {
type.name || (constructor && constructor.name) ||
'A Component'
);
};
}
exports.getComponentName = getComponentName;
39 changes: 12 additions & 27 deletions src/renderers/shared/stack/reconciler/ReactUpdateQueue.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,16 +28,12 @@ function getInternalInstanceReadyForUpdate(publicInstance, callerName) {
if (!internalInstance) {
if (__DEV__) {
var ctor = publicInstance.constructor;
// Only warn when we have a callerName. Otherwise we should be silent.
// We're probably calling from enqueueCallback. We don't want to warn
// there because we already warned for the corresponding lifecycle method.
warning(
!callerName,
'%s(...): Can only update a mounted or mounting component. ' +
'This usually means you called %s() on an unmounted component. ' +
'This is a no-op.\n\nPlease check the code for the %s component.',
callerName,
callerName,
false,
'Can only update a mounted or mounting component. This usually means ' +
'you called setState, replaceState, or forceUpdate on an unmounted ' +
'component. This is a no-op.\n\nPlease check the code for the ' +
'%s component.',
ctor && (ctor.displayName || ctor.name) || 'ReactClass'
);
}
Expand All @@ -47,12 +43,10 @@ function getInternalInstanceReadyForUpdate(publicInstance, callerName) {
if (__DEV__) {
warning(
ReactCurrentOwner.current == null,
'%s(...): Cannot update during an existing state transition (such as ' +
'within `render` or another component\'s constructor). Render methods ' +
'should be a pure function of props and state; constructor ' +
'side-effects are an anti-pattern, but can be moved to ' +
'`componentWillMount`.',
callerName
'Cannot update during an existing state transition (such as within ' +
'`render` or another component\'s constructor). Render methods should ' +
'be a pure function of props and state; constructor side-effects are ' +
'an anti-pattern, but can be moved to `componentWillMount`.',
);
}

Expand Down Expand Up @@ -124,10 +118,7 @@ var ReactUpdateQueue = {
* @internal
*/
enqueueForceUpdate: function(publicInstance, callback, callerName) {
var internalInstance = getInternalInstanceReadyForUpdate(
publicInstance,
'forceUpdate'
);
var internalInstance = getInternalInstanceReadyForUpdate(publicInstance);

if (!internalInstance) {
return;
Expand Down Expand Up @@ -161,10 +152,7 @@ var ReactUpdateQueue = {
* @internal
*/
enqueueReplaceState: function(publicInstance, completeState, callback, callerName) {
var internalInstance = getInternalInstanceReadyForUpdate(
publicInstance,
'replaceState'
);
var internalInstance = getInternalInstanceReadyForUpdate(publicInstance);

if (!internalInstance) {
return;
Expand Down Expand Up @@ -207,10 +195,7 @@ var ReactUpdateQueue = {
);
}

var internalInstance = getInternalInstanceReadyForUpdate(
publicInstance,
'setState'
);
var internalInstance = getInternalInstanceReadyForUpdate(publicInstance);

if (!internalInstance) {
return;
Expand Down