Skip to content

[Fiber] Add more life-cycles #8015

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

Merged
merged 4 commits into from
Oct 21, 2016
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
79 changes: 26 additions & 53 deletions src/renderers/shared/fiber/ReactFiberBeginWork.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,6 @@ var {
NoWork,
OffscreenPriority,
} = require('ReactPriorityLevel');
var {
mergeUpdateQueue,
} = require('ReactFiberUpdateQueue');
var {
Placement,
} = require('ReactTypeOfSideEffect');
Expand All @@ -51,7 +48,11 @@ var ReactFiberClassComponent = require('ReactFiberClassComponent');
module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>, scheduleUpdate : (fiber: Fiber, priorityLevel : PriorityLevel) => void) {

const {
mount,
adoptClassInstance,
constructClassInstance,
mountClassInstance,
resumeMountClassInstance,
updateClassInstance,
} = ReactFiberClassComponent(scheduleUpdate);

function markChildAsProgressed(current, workInProgress, priorityLevel) {
Expand Down Expand Up @@ -156,54 +157,27 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>, s
}

function updateClassComponent(current : ?Fiber, workInProgress : Fiber) {
// A class component update is the result of either new props or new state.
// Account for the possibly of missing pending props by falling back to the
// memoized props.
var props = workInProgress.pendingProps;
if (!props) {
// If there isn't any new props, then we'll reuse the memoized props.
// This could be from already completed work.
props = workInProgress.memoizedProps;
if (!props) {
throw new Error('There should always be pending or memoized props.');
let shouldUpdate;
if (!current) {
if (!workInProgress.stateNode) {
// In the initial pass we might need to construct the instance.
constructClassInstance(workInProgress);
mountClassInstance(workInProgress);
shouldUpdate = true;
} else {
// In a resume, we'll already have an instance we can reuse.
shouldUpdate = resumeMountClassInstance(workInProgress);
}
}

// Compute the state using the memoized state and the update queue.
var updateQueue = workInProgress.updateQueue;
var previousState = workInProgress.memoizedState;
var state;
if (updateQueue) {
state = mergeUpdateQueue(updateQueue, previousState, props);
} else {
state = previousState;
shouldUpdate = updateClassInstance(current, workInProgress);
}

var instance = workInProgress.stateNode;
if (!instance) {
var ctor = workInProgress.type;
workInProgress.stateNode = instance = new ctor(props);
mount(workInProgress, instance);
state = instance.state || null;
} else if (typeof instance.shouldComponentUpdate === 'function' &&
!(updateQueue && updateQueue.isForced)) {
if (workInProgress.memoizedProps !== null) {
// Reset the props, in case this is a ping-pong case rather than a
// completed update case. For the completed update case, the instance
// props will already be the memoizedProps.
instance.props = workInProgress.memoizedProps;
instance.state = workInProgress.memoizedState;
if (!instance.shouldComponentUpdate(props, state)) {
return bailoutOnAlreadyFinishedWork(current, workInProgress);
}
}
if (!shouldUpdate) {
return bailoutOnAlreadyFinishedWork(current, workInProgress);
}

instance.props = props;
instance.state = state;
var nextChildren = instance.render();
// Rerender
const instance = workInProgress.stateNode;
const nextChildren = instance.render();
reconcileChildren(current, workInProgress, nextChildren);

return workInProgress.child;
}

Expand Down Expand Up @@ -258,22 +232,21 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>, s
}

function mountIndeterminateComponent(current, workInProgress) {
if (current) {
throw new Error('An indeterminate component should never have mounted.');
}
var fn = workInProgress.type;
var props = workInProgress.pendingProps;
var value = fn(props);
if (typeof value === 'object' && value && typeof value.render === 'function') {
// Proceed under the assumption that this is a class instance
workInProgress.tag = ClassComponent;
if (current) {
current.tag = ClassComponent;
}
adoptClassInstance(workInProgress, value);
mountClassInstance(workInProgress);
value = value.render();
} else {
// Proceed under the assumption that this is a functional component
workInProgress.tag = FunctionalComponent;
if (current) {
current.tag = FunctionalComponent;
}
}
reconcileChildren(current, workInProgress, value);
return workInProgress.child;
Expand Down
154 changes: 145 additions & 9 deletions src/renderers/shared/fiber/ReactFiberClassComponent.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ var {
createUpdateQueue,
addToQueue,
addCallbackToQueue,
mergeUpdateQueue,
} = require('ReactFiberUpdateQueue');
var ReactInstanceMap = require('ReactInstanceMap');

Expand Down Expand Up @@ -70,20 +71,155 @@ module.exports = function(scheduleUpdate : (fiber: Fiber, priorityLevel : Priori
},
};

function mount(workInProgress : Fiber, instance : any) {
const state = instance.state || null;
// The initial state must be added to the update queue in case
// setState is called before the initial render.
if (state !== null) {
workInProgress.updateQueue = createUpdateQueue(state);
}
function adoptClassInstance(workInProgress : Fiber, instance : any) : void {
instance.updater = updater;
workInProgress.stateNode = instance;
// The instance needs access to the fiber so that it can schedule updates
ReactInstanceMap.set(instance, workInProgress);
instance.updater = updater;
}

function constructClassInstance(workInProgress : Fiber) : any {
const ctor = workInProgress.type;
const props = workInProgress.pendingProps;
const instance = new ctor(props);
adoptClassInstance(workInProgress, instance);
return instance;
}

// Invokes the mount life-cycles on a previously never rendered instance.
function mountClassInstance(workInProgress : Fiber) : void {
const instance = workInProgress.stateNode;

const state = instance.state || null;

// A class component update is the result of either new props or new state.
// Account for the possibly of missing pending props by falling back to the
// memoized props.
Copy link
Collaborator

Choose a reason for hiding this comment

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

What is this comment referring to? You throw on missing pendingProps below. Copypaste?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

yea. This is copypaste.

let props = workInProgress.pendingProps;
if (!props) {
throw new Error('There must be pending props for an initial mount.');
}

instance.props = props;
instance.state = state;

if (typeof instance.componentWillMount === 'function') {
instance.componentWillMount();
// If we had additional state updates during this life-cycle, let's
// process them now.
const updateQueue = workInProgress.updateQueue;
if (updateQueue) {
instance.state = mergeUpdateQueue(updateQueue, state, props);
}
}
}

// Called on a preexisting class instance. Returns false if a resumed render
// could be reused.
function resumeMountClassInstance(workInProgress : Fiber) : boolean {
const instance = workInProgress.stateNode;
let newState = workInProgress.memoizedState;
let newProps = workInProgress.pendingProps;
if (!newProps) {
// If there isn't any new props, then we'll reuse the memoized props.
// This could be from already completed work.
newProps = workInProgress.memoizedProps;
if (!newProps) {
throw new Error('There should always be pending or memoized props.');
}
}

// TODO: Should we deal with a setState that happened after the last
// componentWillMount and before this componentWillMount? Probably
// unsupported anyway.

const updateQueue = workInProgress.updateQueue;

// If this completed, we might be able to just reuse this instance.
if (typeof instance.shouldComponentUpdate === 'function' &&
!(updateQueue && updateQueue.isForced) &&
workInProgress.memoizedProps !== null &&
!instance.shouldComponentUpdate(newProps, newState)) {
return false;
}

// If we didn't bail out we need to construct a new instance. We don't
// want to reuse one that failed to fully mount.
const newInstance = constructClassInstance(workInProgress);
newInstance.props = newProps;
newInstance.state = newState = newInstance.state || null;

if (typeof newInstance.componentWillMount === 'function') {
newInstance.componentWillMount();
// If we had additional state updates during this life-cycle, let's
// process them now.
const newUpdateQueue = workInProgress.updateQueue;
if (newUpdateQueue) {
instance.state = mergeUpdateQueue(newUpdateQueue, newState, newProps);
}
}
return true;
}

// Invokes the update life-cycles and returns false if it shouldn't rerender.
function updateClassInstance(current : Fiber, workInProgress : Fiber) : boolean {
const instance = workInProgress.stateNode;

const oldProps = current.memoizedProps;
let newProps = workInProgress.pendingProps;
if (!newProps) {
// If there aren't any new props, then we'll reuse the memoized props.
// This could be from already completed work.
newProps = workInProgress.memoizedProps;
if (!newProps) {
throw new Error('There should always be pending or memoized props.');
}
}

// Note: During these life-cycles, instance.props/instance.state are what
// ever the previously attempted to render - not the "current". However,
// during componentDidUpdate we pass the "current" props.
Copy link
Collaborator

Choose a reason for hiding this comment

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

This is the intended behavior? I wonder if this breaks any user assumptions.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Like discussed in chat, this is intended. It's a bit confusing but it's the most likely semantics that will work with current code.

The primary goal is to have a nicer upgrade path from existing code. Ease of understanding the semantics of the life-cycles can be achieved by redesigning a new API for them.


if (oldProps !== newProps) {
if (typeof instance.componentWillReceiveProps === 'function') {
instance.componentWillReceiveProps(newProps);
}
}

// Compute the next state using the memoized state and the update queue.
const updateQueue = workInProgress.updateQueue;
const previousState = workInProgress.memoizedState;
// TODO: Previous state can be null.
let newState;
if (updateQueue) {
newState = mergeUpdateQueue(updateQueue, previousState, newProps);
} else {
newState = previousState;
}

if (typeof instance.shouldComponentUpdate === 'function' &&
!(updateQueue && updateQueue.isForced) &&
workInProgress.memoizedProps !== null &&
!instance.shouldComponentUpdate(newProps, newState)) {
// TODO: Should this get the new props/state updated regardless?
Copy link
Collaborator

Choose a reason for hiding this comment

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

Do you mean whether we should reassign this.props and this.state despite sCU returning false? Is there any reason not to?

return false;
}

if (typeof instance.componentWillUpdate === 'function') {
instance.componentWillUpdate(newProps, newState);
}

instance.props = newProps;
instance.state = newState;
return true;
}

return {
mount,
adoptClassInstance,
constructClassInstance,
mountClassInstance,
resumeMountClassInstance,
updateClassInstance,
};

};