Updater form of setState

Add support for setState((state, props) => newState).

Rename pendingState to stateQueue.
This commit is contained in:
Andrew Clark
2016-07-25 09:09:45 -07:00
committed by Sebastian Markbage
parent 8b200c5e78
commit 25d199abe0
7 changed files with 118 additions and 74 deletions
+6 -7
View File
@@ -15,7 +15,7 @@
import type { ReactCoroutine, ReactYield } from 'ReactCoroutine';
import type { TypeOfWork } from 'ReactTypeOfWork';
import type { PriorityLevel } from 'ReactPriorityLevel';
import type { PendingState } from 'ReactFiberPendingState';
import type { StateQueue } from 'ReactFiberStateQueue';
var ReactTypeOfWork = require('ReactTypeOfWork');
var {
@@ -77,9 +77,8 @@ export type Fiber = Instance & {
pendingProps: any, // This type will be more specific once we overload the tag.
// TODO: I think that there is a way to merge pendingProps and memoizedProps.
memoizedProps: any, // The props used to create the output.
// Local state for class components. Either null or a linked list of partial
// state objects.
pendingState: PendingState,
// A queue of local state updates.
stateQueue: StateQueue,
// The state used to create the output. This is a full state object.
memoizedState: any,
// Output is the return value of this fiber, or a linked list of return values
@@ -157,7 +156,7 @@ var createFiber = function(tag : TypeOfWork, key : null | string) : Fiber {
pendingProps: null,
memoizedProps: null,
pendingState: null,
stateQueue: null,
memoizedState: null,
output: null,
@@ -200,7 +199,7 @@ exports.cloneFiber = function(fiber : Fiber, priorityLevel : PriorityLevel) : Fi
alt.sibling = fiber.sibling; // This should always be overridden. TODO: null
alt.ref = fiber.ref;
alt.pendingProps = fiber.pendingProps; // TODO: Pass as argument.
alt.pendingState = fiber.pendingState;
alt.stateQueue = fiber.stateQueue;
alt.pendingWorkPriority = priorityLevel;
alt.child = fiber.child;
@@ -226,7 +225,7 @@ exports.cloneFiber = function(fiber : Fiber, priorityLevel : PriorityLevel) : Fi
// pendingProps is here for symmetry but is unnecessary in practice for now.
// TODO: Pass in the new pendingProps as an argument maybe?
alt.pendingProps = fiber.pendingProps;
alt.pendingState = fiber.pendingState;
alt.stateQueue = fiber.stateQueue;
alt.pendingWorkPriority = priorityLevel;
alt.memoizedProps = fiber.memoizedProps;
@@ -18,7 +18,7 @@ import type { FiberRoot } from 'ReactFiberRoot';
import type { HostConfig } from 'ReactFiberReconciler';
import type { Scheduler } from 'ReactFiberScheduler';
import type { PriorityLevel } from 'ReactPriorityLevel';
import type { PendingState } from 'ReactFiberPendingState';
import type { StateQueue } from 'ReactFiberStateQueue';
var {
reconcileChildFibers,
@@ -42,11 +42,12 @@ var {
OffscreenPriority,
} = require('ReactPriorityLevel');
var {
createPendingState,
mergePendingState,
} = require('ReactFiberPendingState');
createStateQueue,
addToQueue,
mergeStateQueue,
} = require('ReactFiberStateQueue');
module.exports = function<T, P, I, C>(config : HostConfig<T, P, I, C>, getScheduler: () => Scheduler) {
module.exports = function<T, P, I, C>(config : HostConfig<T, P, I, C>, getScheduler : () => Scheduler) {
function markChildAsProgressed(current, workInProgress, priorityLevel) {
// We now have clones. Let's store them as the currently progressed work.
@@ -113,9 +114,9 @@ module.exports = function<T, P, I, C>(config : HostConfig<T, P, I, C>, getSchedu
return workInProgress.child;
}
function scheduleUpdate(fiber: Fiber, pendingState: PendingState, priorityLevel : PriorityLevel): void {
function scheduleUpdate(fiber: Fiber, stateQueue: StateQueue, priorityLevel : PriorityLevel): void {
const { scheduleLowPriWork } = getScheduler();
fiber.pendingState = pendingState;
fiber.stateQueue = stateQueue;
while (true) {
if (fiber.pendingWorkPriority === NoWork ||
fiber.pendingWorkPriority >= priorityLevel) {
@@ -138,26 +139,20 @@ module.exports = function<T, P, I, C>(config : HostConfig<T, P, I, C>, getSchedu
const updater = {
enqueueSetState(instance, partialState) {
const fiber = instance._fiber;
let pendingState = fiber.pendingState;
let stateQueue = fiber.stateQueue;
// Append to pending state queue
const nextPendingStateNode = createPendingState(partialState);
if (pendingState === null) {
pendingState = nextPendingStateNode;
if (stateQueue === null) {
stateQueue = createStateQueue(partialState);
} else {
if (pendingState.tail === null) {
pendingState.next = nextPendingStateNode;
} else {
pendingState.tail.next = nextPendingStateNode;
}
pendingState.tail = nextPendingStateNode;
addToQueue(stateQueue, partialState);
}
// Must schedule an update on both alternates, because we don't know tree
// is current.
scheduleUpdate(fiber, pendingState, LowPriority);
scheduleUpdate(fiber, stateQueue, LowPriority);
if (fiber.alternate) {
scheduleUpdate(fiber.alternate, pendingState, LowPriority);
scheduleUpdate(fiber.alternate, stateQueue, LowPriority);
}
},
};
@@ -171,12 +166,12 @@ module.exports = function<T, P, I, C>(config : HostConfig<T, P, I, C>, getSchedu
props = current.memoizedProps;
}
// Compute the state using the memoized state and the pending state queue.
var pendingState = workInProgress.pendingState;
var stateQueue = workInProgress.stateQueue;
var state;
if (!current) {
state = mergePendingState(null, pendingState);
state = mergeStateQueue(null, props, stateQueue);
} else {
state = mergePendingState(current.memoizedState, pendingState);
state = mergeStateQueue(current.memoizedState, props, stateQueue);
}
var instance = workInProgress.stateNode;
@@ -186,7 +181,7 @@ module.exports = function<T, P, I, C>(config : HostConfig<T, P, I, C>, getSchedu
state = instance.state || null;
// The initial state must be added to the pending state queue in case
// setState is called before the initial render.
workInProgress.pendingState = createPendingState(state);
workInProgress.stateQueue = createStateQueue(state);
// The instance needs access to the fiber so that it can schedule updates
instance._fiber = workInProgress;
instance.updater = updater;
@@ -334,9 +329,9 @@ module.exports = function<T, P, I, C>(config : HostConfig<T, P, I, C>, getSchedu
if ((workInProgress.pendingProps === null || (
workInProgress.memoizedProps !== null &&
workInProgress.pendingProps === workInProgress.memoizedProps &&
workInProgress.pendingProps === workInProgress.memoizedProps
)) &&
workInProgress.pendingState === null
workInProgress.stateQueue === null
) {
return bailoutOnAlreadyFinishedWork(current, workInProgress);
}
@@ -132,8 +132,8 @@ module.exports = function<T, P, I, C>(config : HostConfig<T, P, I, C>) {
return null;
case ClassComponent:
transferOutput(workInProgress.child, workInProgress);
// Don't use the pending state queue to compute the memoized state. We
// already merged it and assigned it to the instance. Copy it from there.
// Don't use the state queue to compute the memoized state. We already
// merged it and assigned it to the instance. Copy it from there.
const state = workInProgress.stateNode.state;
workInProgress.memoizedState = state;
return null;
@@ -1,38 +0,0 @@
/**
* Copyright 2013-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
* @providesModule ReactFiberPendingState
* @flow
*/
'use strict';
export type PendingState = {
partialState: any,
next: PendingState,
tail: PendingState
} | null;
exports.createPendingState = function(partialState: mixed): PendingState {
return {
partialState,
next: null,
tail: null,
};
};
exports.mergePendingState = function(prevState: any, queue: PendingState): any {
if (queue === null) {
return prevState;
}
let state = Object.assign({}, prevState, queue.partialState);
while (queue = queue.next) {
state = Object.assign(state, queue.partialState);
}
return state;
};
@@ -143,9 +143,9 @@ module.exports = function<T, P, I, C>(config : HostConfig<T, P, I, C>) {
// The work is now done. We don't need this anymore. This flags
// to the system not to redo any work here.
workInProgress.pendingProps = null;
workInProgress.pendingState = null;
workInProgress.stateQueue = null;
if (current) {
current.pendingState = null;
current.stateQueue = null;
}
const returnFiber = workInProgress.return;
@@ -0,0 +1,51 @@
/**
* Copyright 2013-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
* @providesModule ReactFiberStateQueue
* @flow
*/
'use strict';
export type StateQueue = {
partialState: any,
next: StateQueue,
tail: StateQueue
} | null;
exports.createStateQueue = function(partialState : mixed) : StateQueue {
return {
partialState,
next: null,
tail: null,
};
};
exports.addToQueue = function(queue : StateQueue, partialState : mixed) {
const node = exports.createStateQueue(partialState);
if (queue.tail === null) {
queue.next = node;
} else {
queue.tail.next = node;
}
queue.tail = node;
}
exports.mergeStateQueue = function(prevState : any, props : any, queue : StateQueue) : any {
if (queue === null) {
return prevState;
}
let state = Object.assign({}, prevState);
do {
const partialState = typeof queue.partialState === 'function' ?
queue.partialState(state, props) :
queue.partialState;
state = Object.assign(state, partialState);
} while (queue = queue.next);
return state;
};
@@ -616,4 +616,41 @@ describe('ReactIncremental', () => {
ReactNoop.flush();
expect(instance.state).toEqual({ a: 'a', b: 'b', c: 'c', d: 'd' });
});
it('can use updater form of setState', () => {
let instance;
class Bar extends React.Component {
constructor() {
super();
this.state = { num: 1 };
instance = this;
}
render() {
return <div>{this.props.children}</div>;
}
}
function Foo({ multiplier }) {
return (
<div>
<Bar multiplier={multiplier} />
</div>
);
}
function updater(state, props) {
return { num: state.num * props.multiplier };
}
ReactNoop.render(<Foo multiplier={2} />);
ReactNoop.flush();
expect(instance.state.num).toEqual(1);
instance.setState(updater);
ReactNoop.flush();
expect(instance.state.num).toEqual(2);
ReactNoop.render(<Foo multiplier={3} />);
instance.setState(updater);
ReactNoop.flush();
expect(instance.state.num).toEqual(6);
});
});