Skip to content

Commit 7080003

Browse files
committed
setState for Fiber
Updates are scheduled by setting a work priority on the fiber and bubbling it to the root. Because the instance does not know which tree is current at any given time, the update is scheduled on both fiber alternates. Need to add more unit tests to cover edge cases.
1 parent a20b326 commit 7080003

File tree

6 files changed

+143
-15
lines changed

6 files changed

+143
-15
lines changed

src/renderers/shared/fiber/ReactFiber.js

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,10 @@ export type Fiber = Instance & {
7878
pendingProps: any, // This type will be more specific once we overload the tag.
7979
// TODO: I think that there is a way to merge pendingProps and memoizedProps.
8080
memoizedProps: any, // The props used to create the output.
81+
// Local state for class components. May need better naming to disambiguate
82+
// from stateNode.
83+
pendingState: any,
84+
memoizedState: any, // The state used to create the output.
8185
// Output is the return value of this fiber, or a linked list of return values
8286
// if this returns multiple values. Such as a fragment.
8387
output: any, // This type will be more specific once we overload the tag.
@@ -142,6 +146,8 @@ var createFiber = function(tag : TypeOfWork, key : null | string) : Fiber {
142146

143147
pendingProps: null,
144148
memoizedProps: null,
149+
pendingState: null,
150+
memoizedState: null,
145151
output: null,
146152

147153
nextEffect: null,
@@ -176,6 +182,7 @@ exports.cloneFiber = function(fiber : Fiber, priorityLevel : PriorityLevel) : Fi
176182
alt.sibling = fiber.sibling;
177183
alt.ref = alt.ref;
178184
alt.pendingProps = fiber.pendingProps;
185+
alt.pendingState = fiber.pendingState;
179186
alt.pendingWorkPriority = priorityLevel;
180187

181188
// Whenever we clone, we do so to get a new work in progress.
@@ -196,6 +203,7 @@ exports.cloneFiber = function(fiber : Fiber, priorityLevel : PriorityLevel) : Fi
196203
alt.ref = alt.ref;
197204
// pendingProps is here for symmetry but is unnecessary in practice for now.
198205
alt.pendingProps = fiber.pendingProps;
206+
alt.pendingState = fiber.pendingState;
199207
alt.pendingWorkPriority = priorityLevel;
200208

201209
alt.alternate = fiber;
@@ -232,7 +240,7 @@ function createFiberFromElementType(type : mixed, key : null | string) {
232240
throw new Error('Unknown component type: ' + typeof type);
233241
}
234242
return fiber;
235-
};
243+
}
236244

237245
exports.createFiberFromElementType = createFiberFromElementType;
238246

src/renderers/shared/fiber/ReactFiberBeginWork.js

Lines changed: 81 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,13 @@
1414

1515
import type { ReactCoroutine } from 'ReactCoroutine';
1616
import type { Fiber } from 'ReactFiber';
17+
import type { FiberRoot } from 'ReactFiberRoot';
1718
import type { HostConfig } from 'ReactFiberReconciler';
19+
import type { Scheduler } from 'ReactFiberScheduler';
20+
import type { PriorityLevel } from 'ReactPriorityLevel';
1821

1922
var { reconcileChildFibers } = require('ReactChildFiber');
23+
var { LowPriority } = require('ReactPriorityLevel');
2024
var ReactTypeOfWork = require('ReactTypeOfWork');
2125
var {
2226
IndeterminateComponent,
@@ -34,8 +38,7 @@ var {
3438
} = require('ReactPriorityLevel');
3539
var { findNextUnitOfWorkAtPriority } = require('ReactFiberPendingWork');
3640

37-
module.exports = function<T, P, I, C>(config : HostConfig<T, P, I, C>) {
38-
41+
module.exports = function<T, P, I, C>(config : HostConfig<T, P, I, C>, getScheduler: () => Scheduler) {
3942
function reconcileChildren(current, workInProgress, nextChildren) {
4043
const priority = workInProgress.pendingWorkPriority;
4144
workInProgress.child = reconcileChildFibers(
@@ -54,31 +57,88 @@ module.exports = function<T, P, I, C>(config : HostConfig<T, P, I, C>) {
5457
workInProgress.pendingWorkPriority = NoWork;
5558
}
5659

60+
function updateFiber(fiber: Fiber, state: any, priorityLevel : PriorityLevel): void {
61+
const { scheduleLowPriWork } = getScheduler();
62+
fiber.pendingState = state;
63+
64+
while (true) {
65+
if (fiber.pendingWorkPriority === NoWork ||
66+
fiber.pendingWorkPriority >= priorityLevel) {
67+
fiber.pendingWorkPriority = priorityLevel;
68+
}
69+
// Duck type root
70+
if (fiber.stateNode && fiber.stateNode.containerInfo) {
71+
const root : FiberRoot = (fiber.stateNode : any);
72+
scheduleLowPriWork(root, priorityLevel);
73+
return;
74+
}
75+
if (!fiber.return) {
76+
throw new Error('No root!');
77+
}
78+
fiber = fiber.return;
79+
}
80+
}
81+
82+
// Class component state updater
83+
const updater = {
84+
enqueueSetState(instance, partialState) {
85+
const fiber = instance._fiber;
86+
87+
const prevState = fiber.pendingState || fiber.memoizedState;
88+
const state = Object.assign({}, prevState, partialState);
89+
90+
// Must schedule an update on both alternates, because we don't know tree
91+
// is current.
92+
updateFiber(fiber, state, LowPriority);
93+
if (fiber.alternate) {
94+
updateFiber(fiber.alternate, state, LowPriority);
95+
}
96+
},
97+
};
98+
5799
function updateClassComponent(current : ?Fiber, workInProgress : Fiber) {
100+
// A class component update is the result of either new props or new state.
101+
// Account for the possibly of missing pending props or state by falling
102+
// back to the most recent props or state.
58103
var props = workInProgress.pendingProps;
104+
var state = workInProgress.pendingState;
105+
if (!props && current) {
106+
props = current.memoizedProps;
107+
}
108+
if (!state && current) {
109+
state = current.memoizedState;
110+
}
111+
59112
var instance = workInProgress.stateNode;
60113
if (!instance) {
61114
var ctor = workInProgress.type;
62115
workInProgress.stateNode = instance = new ctor(props);
116+
state = workInProgress.pendingState = instance.state || null;
117+
// The instance needs access to the fiber so that it can schedule updates
118+
instance._fiber = workInProgress;
119+
instance.updater = updater;
63120
} else if (typeof instance.shouldComponentUpdate === 'function') {
64121
if (current && current.memoizedProps) {
65-
// Revert to the last flushed props, incase we aborted an update.
122+
// Revert to the last flushed props and state, incase we aborted an update.
66123
instance.props = current.memoizedProps;
67-
if (!instance.shouldComponentUpdate(props)) {
124+
instance.state = current.memoizedState;
125+
if (!instance.shouldComponentUpdate(props, state)) {
68126
return bailoutOnCurrent(current, workInProgress);
69127
}
70128
}
71129
if (!workInProgress.hasWorkInProgress && workInProgress.memoizedProps) {
72-
// Reset the props, in case this is a ping-pong case rather than a
73-
// completed update case. For the completed update case, the instance
74-
// props will already be the memoizedProps.
130+
// Reset the props and state, in case this is a ping-pong case rather
131+
// than a completed update case. For the completed update case, the
132+
// instance props and state will already be the memoized props and state.
75133
instance.props = workInProgress.memoizedProps;
76-
if (!instance.shouldComponentUpdate(props)) {
134+
instance.state = workInProgress.memoizedState;
135+
if (!instance.shouldComponentUpdate(props, state)) {
77136
return bailoutOnAlreadyFinishedWork(workInProgress);
78137
}
79138
}
80139
}
81140
instance.props = props;
141+
instance.state = state;
82142
var nextChildren = instance.render();
83143
reconcileChildren(current, workInProgress, nextChildren);
84144
workInProgress.pendingWorkPriority = NoWork;
@@ -188,9 +248,11 @@ module.exports = function<T, P, I, C>(config : HostConfig<T, P, I, C>) {
188248
// the same props as the new one. In that case, we can just copy the output
189249
// and children from that node.
190250
workInProgress.memoizedProps = workInProgress.pendingProps;
251+
workInProgress.memoizedState = workInProgress.pendingState;
191252
workInProgress.output = current.output;
192253
const priorityLevel = workInProgress.pendingWorkPriority;
193254
workInProgress.pendingProps = null;
255+
workInProgress.pendingState = current.pendingState = null;
194256
workInProgress.pendingWorkPriority = NoWork;
195257
workInProgress.stateNode = current.stateNode;
196258
if (current.child) {
@@ -219,6 +281,10 @@ module.exports = function<T, P, I, C>(config : HostConfig<T, P, I, C>) {
219281
// looking for. In that case, we should be able to just bail out.
220282
const priorityLevel = workInProgress.pendingWorkPriority;
221283
workInProgress.pendingProps = null;
284+
workInProgress.pendingState = null;
285+
if (workInProgress.alternate) {
286+
workInProgress.alternate.pendingState = null;
287+
}
222288
workInProgress.pendingWorkPriority = NoWork;
223289

224290
workInProgress.firstEffect = null;
@@ -252,12 +318,17 @@ module.exports = function<T, P, I, C>(config : HostConfig<T, P, I, C>) {
252318
// Ideally nothing should rely on this, but relying on it here
253319
// means that we don't need an additional field on the work in
254320
// progress.
255-
if (current && workInProgress.pendingProps === current.memoizedProps) {
321+
if (current &&
322+
workInProgress.pendingProps === current.memoizedProps &&
323+
workInProgress.pendingState === current.memoizedState
324+
) {
256325
return bailoutOnCurrent(current, workInProgress);
257326
}
258327

259328
if (!workInProgress.hasWorkInProgress &&
260-
workInProgress.pendingProps === workInProgress.memoizedProps) {
329+
workInProgress.pendingProps === workInProgress.memoizedProps &&
330+
workInProgress.pendingState === workInProgress.memoizedState
331+
) {
261332
return bailoutOnAlreadyFinishedWork(workInProgress);
262333
}
263334

src/renderers/shared/fiber/ReactFiberCompleteWork.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ module.exports = function<T, P, I, C>(config : HostConfig<T, P, I, C>) {
6767
// the linked list of fibers that has the individual output values.
6868
returnFiber.output = (child && !child.sibling) ? child.output : child;
6969
returnFiber.memoizedProps = returnFiber.pendingProps;
70+
returnFiber.memoizedState = returnFiber.pendingState;
7071
}
7172

7273
function recursivelyFillYields(yields, output : ?Fiber | ?ReifiedYield) {

src/renderers/shared/fiber/ReactFiberPendingWork.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,13 +40,14 @@ exports.findNextUnitOfWorkAtPriority = function(currentRoot : Fiber, priorityLev
4040
if (current.pendingWorkPriority !== NoWork &&
4141
current.pendingWorkPriority <= priorityLevel) {
4242
// This node has work to do that fits our priority level criteria.
43-
if (current.pendingProps !== null) {
43+
if (current.pendingProps !== null || current.pendingState !== null) {
4444
// We found some work to do. We need to return the "work in progress"
4545
// of this node which will be the alternate.
4646
const workInProgress = current.alternate;
4747
if (!workInProgress) {
4848
throw new Error('Should have wip now');
4949
}
50+
// If current has pendingState, it's already set on the workInProgress.
5051
workInProgress.pendingProps = current.pendingProps;
5152
return workInProgress;
5253
}

src/renderers/shared/fiber/ReactFiberScheduler.js

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,19 @@ var {
3333

3434
var timeHeuristicForUnitOfWork = 1;
3535

36+
export type Scheduler = {
37+
scheduleLowPriWork: (root : FiberRoot, priority : PriorityLevel) => void
38+
};
39+
3640
module.exports = function<T, P, I, C>(config : HostConfig<T, P, I, C>) {
41+
// Use a closure to circumvent the circular dependency between the scheduler
42+
// and ReactFiberBeginWork. Don't know if there's a better way to do this.
43+
let scheduler;
44+
function getScheduler(): Scheduler {
45+
return scheduler;
46+
}
3747

38-
const { beginWork } = ReactFiberBeginWork(config);
48+
const { beginWork } = ReactFiberBeginWork(config, getScheduler);
3949
const { completeWork } = ReactFiberCompleteWork(config);
4050
const { commitWork } = ReactFiberCommitWork(config);
4151

@@ -119,6 +129,10 @@ module.exports = function<T, P, I, C>(config : HostConfig<T, P, I, C>) {
119129
// The work is now done. We don't need this anymore. This flags
120130
// to the system not to redo any work here.
121131
workInProgress.pendingProps = null;
132+
workInProgress.pendingState = null;
133+
if (current) {
134+
current.pendingState = null;
135+
}
122136
if (workInProgress.pendingWorkPriority === NoWork) {
123137
workInProgress.hasWorkInProgress = false;
124138
}
@@ -179,7 +193,7 @@ module.exports = function<T, P, I, C>(config : HostConfig<T, P, I, C>) {
179193

180194
function performUnitOfWork(workInProgress : Fiber) : ?Fiber {
181195
// Ignore work if there is nothing to do.
182-
if (workInProgress.pendingProps === null) {
196+
if (workInProgress.pendingProps === null && workInProgress.pendingState === null) {
183197
return completeUnitOfWork(workInProgress);
184198
}
185199
// The current, flushed, state of this fiber is the alternate.
@@ -250,7 +264,8 @@ module.exports = function<T, P, I, C>(config : HostConfig<T, P, I, C>) {
250264
}
251265
*/
252266

253-
return {
267+
scheduler = {
254268
scheduleLowPriWork: scheduleLowPriWork,
255269
};
270+
return scheduler;
256271
};

src/renderers/shared/fiber/__tests__/ReactIncremental-test.js

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -539,4 +539,36 @@ describe('ReactIncremental', function() {
539539
expect(ops).toEqual(['Content', 'Bar', 'Middle']);
540540

541541
});
542+
543+
it('can update in the middle of a tree using setState', () => {
544+
let instance;
545+
let states = [];
546+
547+
class Bar extends React.Component {
548+
constructor() {
549+
super();
550+
this.state = { string: 'a' };
551+
instance = this;
552+
}
553+
render() {
554+
states.push(this.state.string);
555+
return <div>{this.props.children}</div>;
556+
}
557+
}
558+
559+
function Foo() {
560+
return (
561+
<div>
562+
<Bar />
563+
</div>
564+
);
565+
}
566+
567+
ReactNoop.render(<Foo />);
568+
ReactNoop.flush();
569+
expect(states).toEqual(['a']);
570+
instance.setState({ string: 'b' });
571+
ReactNoop.flush();
572+
expect(states).toEqual(['a', 'b']);
573+
});
542574
});

0 commit comments

Comments
 (0)