Skip to content
Merged
Prev Previous commit
Next Next commit
Call getDerivedStateFromProps every render, even if props did not change
Rather than enqueue a new setState updater for every props change, we
can skip the update queue entirely and merge the result into state at
the end. This makes more sense, since "receiving props" is not an event
that should be observed. It's still a bit weird, since eventually we do
persist the derived state (in other words, it accumulates).
  • Loading branch information
acdlite committed Apr 21, 2018
commit febf9fc9eb644ace90d50ebcb32313bda15878a7
Original file line number Diff line number Diff line change
Expand Up @@ -264,7 +264,6 @@ describe('createSubscription', () => {

it('should ignore values emitted by a new subscribable until the commit phase', () => {
const log = [];
let parentInstance;

function Child({value}) {
ReactNoop.yield('Child: ' + value);
Expand Down Expand Up @@ -301,8 +300,6 @@ describe('createSubscription', () => {
}

render() {
parentInstance = this;

return (
<Subscription source={this.state.observed}>
{(value = 'default') => {
Expand Down Expand Up @@ -331,8 +328,8 @@ describe('createSubscription', () => {
observableB.next('b-2');
observableB.next('b-3');

// Mimic a higher-priority interruption
parentInstance.setState({observed: observableA});
// Update again
ReactNoop.render(<Parent observed={observableA} />);

// Flush everything and ensure that the correct subscribable is used
// We expect the last emitted update to be rendered (because of the commit phase value check)
Expand All @@ -354,7 +351,6 @@ describe('createSubscription', () => {

it('should not drop values emitted between updates', () => {
const log = [];
let parentInstance;

function Child({value}) {
ReactNoop.yield('Child: ' + value);
Expand Down Expand Up @@ -391,8 +387,6 @@ describe('createSubscription', () => {
}

render() {
parentInstance = this;

return (
<Subscription source={this.state.observed}>
{(value = 'default') => {
Expand Down Expand Up @@ -420,8 +414,8 @@ describe('createSubscription', () => {
observableA.next('a-1');
observableA.next('a-2');

// Mimic a higher-priority interruption
parentInstance.setState({observed: observableA});
// Update again
ReactNoop.render(<Parent observed={observableA} />);

// Flush everything and ensure that the correct subscribable is used
// We expect the new subscribable to finish rendering,
Expand Down
14 changes: 5 additions & 9 deletions packages/react-reconciler/src/ReactFiberBeginWork.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,14 +55,14 @@ import ReactDebugCurrentFiber from './ReactDebugCurrentFiber';
import {cancelWorkTimer} from './ReactDebugFiberPerf';

import ReactFiberClassComponent, {
createGetDerivedStateFromPropsUpdate,
applyDerivedStateFromProps,
} from './ReactFiberClassComponent';
import {
mountChildFibers,
reconcileChildFibers,
cloneChildFibers,
} from './ReactChildFiber';
import {enqueueRenderPhaseUpdate, processUpdateQueue} from './ReactUpdateQueue';
import {processUpdateQueue} from './ReactUpdateQueue';
import {NoWork, Never} from './ReactFiberExpirationTime';
import {AsyncMode, StrictMode} from './ReactTypeOfMode';
import MAX_SIGNED_31_BIT_INT from './maxSigned31BitInt';
Expand Down Expand Up @@ -592,15 +592,11 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(

const getDerivedStateFromProps = Component.getDerivedStateFromProps;
if (typeof getDerivedStateFromProps === 'function') {
const update = createGetDerivedStateFromPropsUpdate(
applyDerivedStateFromProps(
workInProgress,
getDerivedStateFromProps,
renderExpirationTime,
props,
);
enqueueRenderPhaseUpdate(workInProgress, update, renderExpirationTime);
const updateQueue = workInProgress.updateQueue;
if (updateQueue !== null) {
processUpdateQueue(workInProgress, updateQueue, renderExpirationTime);
}
}

// Push context providers early to prevent context stack mismatches.
Expand Down
119 changes: 61 additions & 58 deletions packages/react-reconciler/src/ReactFiberClassComponent.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,10 @@ import {startPhaseTimer, stopPhaseTimer} from './ReactDebugFiberPerf';
import {StrictMode} from './ReactTypeOfMode';
import {
enqueueUpdate,
enqueueRenderPhaseUpdate,
processUpdateQueue,
createUpdate,
} from './ReactUpdateQueue';
import {NoWork} from './ReactFiberExpirationTime';

const fakeInternalInstance = {};
const isArray = Array.isArray;
Expand Down Expand Up @@ -109,32 +109,40 @@ if (__DEV__) {
Object.freeze(fakeInternalInstance);
}

export function createGetDerivedStateFromPropsUpdate(
export function applyDerivedStateFromProps(
workInProgress: Fiber,
getDerivedStateFromProps: (props: any, state: any) => any,
renderExpirationTime: ExpirationTime,
nextProps: any,
) {
const update = createUpdate(renderExpirationTime);
update.process = (nextWorkInProgress, prevState) => {
const nextProps = nextWorkInProgress.pendingProps;
const prevState = workInProgress.memoizedState;

if (
debugRenderPhaseSideEffects ||
(debugRenderPhaseSideEffectsForStrictMode &&
nextWorkInProgress.mode & StrictMode)
) {
// Invoke the function an extra time to help detect side-effects.
getDerivedStateFromProps(nextProps, prevState);
}
if (
debugRenderPhaseSideEffects ||
(debugRenderPhaseSideEffectsForStrictMode &&
workInProgress.mode & StrictMode)
) {
// Invoke the function an extra time to help detect side-effects.
getDerivedStateFromProps(nextProps, prevState);
}

const partialState = getDerivedStateFromProps(nextProps, prevState);
const partialState = getDerivedStateFromProps(nextProps, prevState);

if (__DEV__) {
warnOnUndefinedDerivedState(nextWorkInProgress, partialState);
}
// Merge the partial state and the previous state.
return Object.assign({}, prevState, partialState);
};
return update;
if (__DEV__) {
warnOnUndefinedDerivedState(workInProgress, partialState);
}
// Merge the partial state and the previous state.
const memoizedState =
partialState === null || partialState === undefined
? prevState
: Object.assign({}, prevState, partialState);
workInProgress.memoizedState = memoizedState;

// Once the update queue is empty, persist the derived state onto the
// base state.
const updateQueue = workInProgress.updateQueue;
if (updateQueue !== null && updateQueue.expirationTime === NoWork) {
updateQueue.baseState = memoizedState;
}
}

export default function(
Expand Down Expand Up @@ -742,19 +750,20 @@ export default function(
}
}

let updateQueue = workInProgress.updateQueue;
if (updateQueue !== null) {
processUpdateQueue(workInProgress, updateQueue, renderExpirationTime);
instance.state = workInProgress.memoizedState;
}

const getDerivedStateFromProps =
workInProgress.type.getDerivedStateFromProps;
if (typeof getDerivedStateFromProps === 'function') {
const update = createGetDerivedStateFromPropsUpdate(
applyDerivedStateFromProps(
workInProgress,
getDerivedStateFromProps,
renderExpirationTime,
props,
);
enqueueRenderPhaseUpdate(workInProgress, update, renderExpirationTime);
}

let updateQueue = workInProgress.updateQueue;
if (updateQueue !== null) {
processUpdateQueue(workInProgress, updateQueue, renderExpirationTime);
instance.state = workInProgress.memoizedState;
}

Expand Down Expand Up @@ -796,8 +805,9 @@ export default function(
const newUnmaskedContext = getUnmaskedContext(workInProgress);
const newContext = getMaskedContext(workInProgress, newUnmaskedContext);

const getDerivedStateFromProps = ctor.getDerivedStateFromProps;
const hasNewLifecycles =
typeof ctor.getDerivedStateFromProps === 'function' ||
typeof getDerivedStateFromProps === 'function' ||
typeof instance.getSnapshotBeforeUpdate === 'function';

// Note: During these life-cycles, instance.props/instance.state are what
Expand All @@ -821,19 +831,6 @@ export default function(
}
}

// Only call getDerivedStateFromProps if the props have changed
if (oldProps !== newProps) {
const getDerivedStateFromProps =
workInProgress.type.getDerivedStateFromProps;
if (typeof getDerivedStateFromProps === 'function') {
const update = createGetDerivedStateFromPropsUpdate(
getDerivedStateFromProps,
renderExpirationTime,
);
enqueueRenderPhaseUpdate(workInProgress, update, renderExpirationTime);
}
}

const oldState = workInProgress.memoizedState;
let newState = (instance.state = oldState);
let updateQueue = workInProgress.updateQueue;
Expand All @@ -842,6 +839,15 @@ export default function(
newState = workInProgress.memoizedState;
}

if (typeof getDerivedStateFromProps === 'function') {
applyDerivedStateFromProps(
workInProgress,
getDerivedStateFromProps,
newProps,
);
newState = workInProgress.memoizedState;
}

if (
oldProps === newProps &&
oldState === newState &&
Expand Down Expand Up @@ -927,8 +933,9 @@ export default function(
const newUnmaskedContext = getUnmaskedContext(workInProgress);
const newContext = getMaskedContext(workInProgress, newUnmaskedContext);

const getDerivedStateFromProps = ctor.getDerivedStateFromProps;
const hasNewLifecycles =
typeof ctor.getDerivedStateFromProps === 'function' ||
typeof getDerivedStateFromProps === 'function' ||
typeof instance.getSnapshotBeforeUpdate === 'function';

// Note: During these life-cycles, instance.props/instance.state are what
Expand All @@ -952,19 +959,6 @@ export default function(
}
}

// Only call getDerivedStateFromProps if the props have changed
if (oldProps !== newProps) {
const getDerivedStateFromProps =
workInProgress.type.getDerivedStateFromProps;
if (typeof getDerivedStateFromProps === 'function') {
const update = createGetDerivedStateFromPropsUpdate(
getDerivedStateFromProps,
renderExpirationTime,
);
enqueueRenderPhaseUpdate(workInProgress, update, renderExpirationTime);
}
}

const oldState = workInProgress.memoizedState;
let newState = (instance.state = oldState);
let updateQueue = workInProgress.updateQueue;
Expand All @@ -973,6 +967,15 @@ export default function(
newState = workInProgress.memoizedState;
}

if (typeof getDerivedStateFromProps === 'function') {
applyDerivedStateFromProps(
workInProgress,
getDerivedStateFromProps,
newProps,
);
newState = workInProgress.memoizedState;
}

if (
oldProps === newProps &&
oldState === newState &&
Expand Down
Loading