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.
Andrew Clark committed
Jul 23, 2016 at 18:59 UTC
94d2317b4b9c5c8edb078bb2692e26969a19f0ae
5 files changed
+121
-5
src/renderers/shared/fiber/ReactFiber.js
+8
@@ -76,6 +76,10 @@ export type Fiber = Instance & {
76
pendingProps: any, // This type will be more specific once we overload the tag.
77
// TODO: I think that there is a way to merge pendingProps and memoizedProps.
78
memoizedProps: any, // The props used to create the output.
79
+ // Local state for class components. May need better naming to disambiguate
80
+ // from stateNode.
81
+ pendingState: any,
82
+ memoizedState: any, // The state used to create the output.
83
// Output is the return value of this fiber, or a linked list of return values
84
// if this returns multiple values. Such as a fragment.
85
output: any, // This type will be more specific once we overload the tag.
@@ -151,6 +155,8 @@ var createFiber = function(tag : TypeOfWork, key : null | string) : Fiber {
155
156
pendingProps: null,
157
memoizedProps: null,
158
+ pendingState: null,
159
+ memoizedState: null,
160
output: null,
161
162
nextEffect: null,
@@ -192,6 +198,7 @@ exports.cloneFiber = function(fiber : Fiber, priorityLevel : PriorityLevel) : Fi
198
alt.sibling = fiber.sibling; // This should always be overridden. TODO: null
199
alt.ref = fiber.ref;
200
alt.pendingProps = fiber.pendingProps; // TODO: Pass as argument.
201
+ alt.pendingState = fiber.pendingState;
202
alt.pendingWorkPriority = priorityLevel;
203
204
alt.child = fiber.child;
@@ -217,6 +224,7 @@ exports.cloneFiber = function(fiber : Fiber, priorityLevel : PriorityLevel) : Fi
224
// pendingProps is here for symmetry but is unnecessary in practice for now.
225
// TODO: Pass in the new pendingProps as an argument maybe?
226
alt.pendingProps = fiber.pendingProps;
227
+ alt.pendingState = fiber.pendingState;
228
alt.pendingWorkPriority = priorityLevel;
229
230
alt.memoizedProps = fiber.memoizedProps;
src/renderers/shared/fiber/ReactFiberBeginWork.js
+63
-3
@@ -14,7 +14,9 @@
14
15
import type { ReactCoroutine } from 'ReactCoroutine';
16
import type { Fiber } from 'ReactFiber';
17
+import type { FiberRoot } from 'ReactFiberRoot';
18
import type { HostConfig } from 'ReactFiberReconciler';
19
+import type { Scheduler } from 'ReactFiberScheduler';
20
import type { PriorityLevel } from 'ReactPriorityLevel';
21
22
var {
@@ -22,6 +24,7 @@ var {
24
reconcileChildFibersInPlace,
25
cloneChildFibers,
26
} = require('ReactChildFiber');
27
+var { LowPriority } = require('ReactPriorityLevel');
28
var ReactTypeOfWork = require('ReactTypeOfWork');
29
var {
30
IndeterminateComponent,
@@ -38,7 +41,7 @@ var {
41
OffscreenPriority,
42
} = require('ReactPriorityLevel');
43
41
-module.exports = function<T, P, I, C>(config : HostConfig<T, P, I, C>) {
44
+module.exports = function<T, P, I, C>(config : HostConfig<T, P, I, C>, getScheduler: () => Scheduler) {
45
46
function markChildAsProgressed(current, workInProgress, priorityLevel) {
47
// We now have clones. Let's store them as the currently progressed work.
@@ -105,25 +108,81 @@ module.exports = function<T, P, I, C>(config : HostConfig<T, P, I, C>) {
108
return workInProgress.child;
109
}
110
111
+ function updateFiber(fiber: Fiber, state: any, priorityLevel : PriorityLevel): void {
112
+ const { scheduleLowPriWork } = getScheduler();
113
+ fiber.pendingState = state;
114
+
115
+ while (true) {
116
+ if (fiber.pendingWorkPriority === NoWork ||
117
+ fiber.pendingWorkPriority >= priorityLevel) {
118
+ fiber.pendingWorkPriority = priorityLevel;
119
+ }
120
+ // Duck type root
121
+ if (fiber.stateNode && fiber.stateNode.containerInfo) {
122
+ const root : FiberRoot = (fiber.stateNode : any);
123
+ scheduleLowPriWork(root, priorityLevel);
124
+ return;
125
+ }
126
+ if (!fiber.return) {
127
+ throw new Error('No root!');
128
+ }
129
+ fiber = fiber.return;
130
+ }
131
+ }
132
+
133
+ // Class component state updater
134
+ const updater = {
135
+ enqueueSetState(instance, partialState) {
136
+ const fiber = instance._fiber;
137
+
138
+ const prevState = fiber.pendingState || fiber.memoizedState;
139
+ const state = Object.assign({}, prevState, partialState);
140
+
141
+ // Must schedule an update on both alternates, because we don't know tree
142
+ // is current.
143
+ updateFiber(fiber, state, LowPriority);
144
+ if (fiber.alternate) {
145
+ updateFiber(fiber.alternate, state, LowPriority);
146
+ }
147
+ },
148
+ };
149
+
150
function updateClassComponent(current : ?Fiber, workInProgress : Fiber) {
151
+ // A class component update is the result of either new props or new state.
152
+ // Account for the possibly of missing pending props or state by falling
153
+ // back to the most recent props or state.
154
var props = workInProgress.pendingProps;
155
+ var state = workInProgress.pendingState;
156
+ if (!props && current) {
157
+ props = current.memoizedProps;
158
+ }
159
+ if (!state && current) {
160
+ state = current.memoizedState;
161
+ }
162
+
163
var instance = workInProgress.stateNode;
164
if (!instance) {
165
var ctor = workInProgress.type;
166
workInProgress.stateNode = instance = new ctor(props);
167
+ state = workInProgress.pendingState = instance.state || null;
168
+ // The instance needs access to the fiber so that it can schedule updates
169
+ instance._fiber = workInProgress;
170
+ instance.updater = updater;
171
} else if (typeof instance.shouldComponentUpdate === 'function') {
172
if (workInProgress.memoizedProps !== null) {
173
// Reset the props, in case this is a ping-pong case rather than a
174
// completed update case. For the completed update case, the instance
175
// props will already be the memoizedProps.
176
instance.props = workInProgress.memoizedProps;
120
- if (!instance.shouldComponentUpdate(props)) {
177
+ instance.state = workInProgress.memoizedState;
178
+ if (!instance.shouldComponentUpdate(props, state)) {
179
return bailoutOnAlreadyFinishedWork(current, workInProgress);
180
}
181
}
182
}
183
184
instance.props = props;
185
+ instance.state = state;
186
var nextChildren = instance.render();
187
reconcileChildren(current, workInProgress, nextChildren);
188
@@ -253,7 +312,8 @@ module.exports = function<T, P, I, C>(config : HostConfig<T, P, I, C>) {
312
313
if (workInProgress.pendingProps === null || (
314
workInProgress.memoizedProps !== null &&
256
- workInProgress.pendingProps === workInProgress.memoizedProps
315
+ workInProgress.pendingProps === workInProgress.memoizedProps &&
316
+ workInProgress.pendingState === workInProgress.memoizedState
317
)) {
318
return bailoutOnAlreadyFinishedWork(current, workInProgress);
319
}
src/renderers/shared/fiber/ReactFiberCompleteWork.js
+1
@@ -67,6 +67,7 @@ module.exports = function<T, P, I, C>(config : HostConfig<T, P, I, C>) {
67
// the linked list of fibers that has the individual output values.
68
returnFiber.output = (child && !child.sibling) ? child.output : child;
69
returnFiber.memoizedProps = returnFiber.pendingProps;
70
+ returnFiber.memoizedState = returnFiber.pendingState;
71
}
72
73
function recursivelyFillYields(yields, output : ?Fiber | ?ReifiedYield) {
src/renderers/shared/fiber/ReactFiberScheduler.js
+17
-2
@@ -29,9 +29,19 @@ var {
29
30
var timeHeuristicForUnitOfWork = 1;
31
32
+export type Scheduler = {
33
+ scheduleLowPriWork: (root : FiberRoot, priority : PriorityLevel) => void
34
+};
35
+
36
module.exports = function<T, P, I, C>(config : HostConfig<T, P, I, C>) {
37
+ // Use a closure to circumvent the circular dependency between the scheduler
38
+ // and ReactFiberBeginWork. Don't know if there's a better way to do this.
39
+ let scheduler;
40
+ function getScheduler(): Scheduler {
41
+ return scheduler;
42
+ }
43
34
- const { beginWork } = ReactFiberBeginWork(config);
44
+ const { beginWork } = ReactFiberBeginWork(config, getScheduler);
45
const { completeWork } = ReactFiberCompleteWork(config);
46
const { commitWork } = ReactFiberCommitWork(config);
47
@@ -133,6 +143,10 @@ module.exports = function<T, P, I, C>(config : HostConfig<T, P, I, C>) {
143
// The work is now done. We don't need this anymore. This flags
144
// to the system not to redo any work here.
145
workInProgress.pendingProps = null;
146
+ workInProgress.pendingState = null;
147
+ if (current) {
148
+ current.pendingState = null;
149
+ }
150
151
const returnFiber = workInProgress.return;
152
@@ -259,7 +273,8 @@ module.exports = function<T, P, I, C>(config : HostConfig<T, P, I, C>) {
273
}
274
*/
275
262
- return {
276
+ scheduler = {
277
scheduleLowPriWork: scheduleLowPriWork,
278
};
279
+ return scheduler;
280
};
src/renderers/shared/fiber/__tests__/ReactIncremental-test.js
+32
@@ -556,4 +556,36 @@ describe('ReactIncremental', () => {
556
expect(ops).toEqual(['Content', 'Bar', 'Middle']);
557
558
});
559
+
560
+ it('can update in the middle of a tree using setState', () => {
561
+ let instance;
562
+ let states = [];
563
+
564
+ class Bar extends React.Component {
565
+ constructor() {
566
+ super();
567
+ this.state = { string: 'a' };
568
+ instance = this;
569
+ }
570
+ render() {
571
+ states.push(this.state.string);
572
+ return <div>{this.props.children}</div>;
573
+ }
574
+ }
575
+
576
+ function Foo() {
577
+ return (
578
+ <div>
579
+ <Bar />
580
+ </div>
581
+ );
582
+ }
583
+
584
+ ReactNoop.render(<Foo />);
585
+ ReactNoop.flush();
586
+ expect(states).toEqual(['a']);
587
+ instance.setState({ string: 'b' });
588
+ ReactNoop.flush();
589
+ expect(states).toEqual(['a', 'b']);
590
+ });
591
});