New effect type: Callback
This solves the problem I had with enqueueSetState and enqueueCallback being separate.
Andrew Clark committed
Nov 1, 2016 at 17:41 UTC
c3ab541e0f73056ca1b442876da5ede24197ed28
13 files changed
+168
-115
scripts/fiber/tests-failing.txt
+3
-3
@@ -87,6 +87,9 @@ src/renderers/art/__tests__/ReactART-test.js
87
* resolves refs before componentDidMount
88
* resolves refs before componentDidUpdate
89
90
+src/renderers/dom/__tests__/ReactDOMProduction-test.js
91
+* should throw with an error code in production
92
+
93
src/renderers/dom/shared/__tests__/CSSPropertyOperations-test.js
94
* should set style attribute when styles exist
95
* should warn when using hyphenated style names
@@ -369,7 +372,6 @@ src/renderers/dom/stack/client/__tests__/ReactMount-test.js
372
* should account for escaping on a checksum mismatch
373
* should warn if render removes React-rendered children
374
* should warn if the unmounted node was rendered by another copy of React
372
-* passes the correct callback context
375
* tracks root instances
376
* marks top-level mounts
377
@@ -608,8 +610,6 @@ src/renderers/shared/stack/reconciler/__tests__/ReactUpdates-test.js
610
* throws in setState if the update callback is not a function
611
* throws in replaceState if the update callback is not a function
612
* throws in forceUpdate if the update callback is not a function
611
-* does not update one component twice in a batch (#2410)
612
-* unstable_batchedUpdates should return value from a callback
613
614
src/renderers/shared/stack/reconciler/__tests__/refs-test.js
615
* Should increase refs with an increase in divs
scripts/fiber/tests-passing.txt
+6
-1
@@ -438,7 +438,6 @@ src/renderers/dom/__tests__/ReactDOMProduction-test.js
438
* should use prod React
439
* should handle a simple flow
440
* should call lifecycle methods
441
-* should throw with an error code in production
441
442
src/renderers/dom/fiber/__tests__/ReactDOMFiber-test.js
443
* should render strings as children
@@ -692,6 +691,7 @@ src/renderers/dom/stack/client/__tests__/ReactMount-test.js
691
* should warn if mounting into dirty rendered markup
692
* should not warn if mounting into non-empty node
693
* should warn when mounting into document.body
694
+* passes the correct callback context
695
696
src/renderers/dom/stack/client/__tests__/ReactMountDestruction-test.js
697
* should destroy a react root upon request
@@ -788,6 +788,8 @@ src/renderers/shared/fiber/__tests__/ReactIncremental-test.js
788
* calls componentWill* twice if an update render is aborted
789
* does not call componentWillReceiveProps for state-only updates
790
* skips will/DidUpdate when bailing unless an update was already in progress
791
+* performs batched updates at the end of the batch
792
+* can nest batchedUpdates
793
794
src/renderers/shared/fiber/__tests__/ReactIncrementalReflection-test.js
795
* handles isMounted even when the initial render is deferred
@@ -833,6 +835,7 @@ src/renderers/shared/fiber/__tests__/ReactIncrementalSideEffects-test.js
835
* can defer side-effects and reuse them later - complex
836
* deprioritizes setStates that happens within a deprioritized tree
837
* calls callback after update is flushed
838
+* calls setState callback even if component bails out
839
* calls componentWillUnmount after a deletion, even if nested
840
* calls componentDidMount/Update after insertion/update
841
* invokes ref callbacks after insertion/update/unmount
@@ -1124,7 +1127,9 @@ src/renderers/shared/stack/reconciler/__tests__/ReactUpdates-test.js
1127
* should not reconcile children passed via props
1128
* should queue nested updates
1129
* calls componentWillReceiveProps setState callback properly
1130
+* does not update one component twice in a batch (#2410)
1131
* does not update one component twice in a batch (#6371)
1132
+* unstable_batchedUpdates should return value from a callback
1133
1134
src/renderers/shared/stack/reconciler/__tests__/Transaction-test.js
1135
* should invoke closers with/only-with init returns
src/addons/transitions/__tests__/ReactTransitionGroup-test.js
+3
-3
@@ -97,10 +97,10 @@ describe('ReactTransitionGroup', () => {
97
expect(log).toEqual(['didMount', 'willEnter', 'didEnter']);
98
99
log = [];
100
- instance.setState({count: 1}, function() {
101
- expect(log).toEqual(['willLeave', 'didLeave', 'willUnmount']);
102
- });
100
+ instance.setState({count: 1});
101
});
102
+
103
+ expect(log).toEqual(['willLeave', 'didLeave', 'willUnmount']);
104
});
105
106
it('should handle enter/leave/enter/leave correctly', () => {
src/isomorphic/classic/class/ReactClass.js
-5
@@ -728,11 +728,6 @@ var ReactClassMixin = {
728
* type signature and the only use case for this, is to avoid that.
729
*/
730
replaceState: function(newState, callback) {
731
- if (this.updater.isFiberUpdater) {
732
- this.updater.enqueueReplaceState(this, newState, callback);
733
- return;
734
- }
735
-
731
this.updater.enqueueReplaceState(this, newState);
732
if (callback) {
733
this.updater.enqueueCallback(this, callback, 'replaceState');
src/isomorphic/modern/class/ReactComponent.js
-11
@@ -65,12 +65,6 @@ ReactComponent.prototype.setState = function(partialState, callback) {
65
'setState(...): takes an object of state variables to update or a ' +
66
'function which returns an object of state variables.'
67
);
68
-
69
- if (this.updater.isFiberUpdater) {
70
- this.updater.enqueueSetState(this, partialState, callback);
71
- return;
72
- }
73
-
68
this.updater.enqueueSetState(this, partialState);
69
if (callback) {
70
this.updater.enqueueCallback(this, callback, 'setState');
@@ -92,11 +86,6 @@ ReactComponent.prototype.setState = function(partialState, callback) {
86
* @protected
87
*/
88
ReactComponent.prototype.forceUpdate = function(callback) {
95
- if (this.updater.isFiberUpdater) {
96
- this.updater.enqueueForceUpdate(this, callback);
97
- return;
98
- }
99
-
89
this.updater.enqueueForceUpdate(this);
90
if (callback) {
91
this.updater.enqueueCallback(this, callback, 'forceUpdate');
src/renderers/shared/fiber/ReactFiberBeginWork.js
+1
-1
@@ -48,7 +48,7 @@ var ReactFiberClassComponent = require('ReactFiberClassComponent');
48
49
module.exports = function<T, P, I, TI, C>(
50
config : HostConfig<T, P, I, TI, C>,
51
- scheduleUpdate : (fiber: Fiber, priorityLevel : ?PriorityLevel) => void
51
+ scheduleUpdate : (fiber: Fiber) => void
52
) {
53
54
const {
src/renderers/shared/fiber/ReactFiberClassComponent.js
+23
-15
@@ -26,7 +26,7 @@ var { isMounted } = require('ReactFiberTreeReflection');
26
var ReactInstanceMap = require('ReactInstanceMap');
27
var shallowEqual = require('shallowEqual');
28
29
-module.exports = function(scheduleUpdate : (fiber: Fiber, priorityLevel : ?PriorityLevel) => void) {
29
+module.exports = function(scheduleUpdate : (fiber: Fiber) => void) {
30
31
function scheduleUpdateQueue(fiber: Fiber, updateQueue: UpdateQueue) {
32
fiber.updateQueue = updateQueue;
@@ -41,35 +41,33 @@ module.exports = function(scheduleUpdate : (fiber: Fiber, priorityLevel : ?Prior
41
// Class component state updater
42
const updater = {
43
isMounted,
44
- enqueueSetState(instance, partialState, callback) {
44
+ enqueueSetState(instance, partialState) {
45
const fiber = ReactInstanceMap.get(instance);
46
const updateQueue = fiber.updateQueue ?
47
addToQueue(fiber.updateQueue, partialState) :
48
createUpdateQueue(partialState);
49
- if (callback) {
50
- addCallbackToQueue(updateQueue, callback);
51
- }
49
scheduleUpdateQueue(fiber, updateQueue);
50
},
54
- enqueueReplaceState(instance, state, callback) {
51
+ enqueueReplaceState(instance, state) {
52
const fiber = ReactInstanceMap.get(instance);
53
const updateQueue = createUpdateQueue(state);
54
updateQueue.isReplace = true;
58
- if (callback) {
59
- addCallbackToQueue(updateQueue, callback);
60
- }
55
scheduleUpdateQueue(fiber, updateQueue);
56
},
63
- enqueueForceUpdate(instance, callback) {
57
+ enqueueForceUpdate(instance) {
58
const fiber = ReactInstanceMap.get(instance);
59
const updateQueue = fiber.updateQueue || createUpdateQueue(null);
60
updateQueue.isForced = true;
67
- if (callback) {
68
- addCallbackToQueue(updateQueue, callback);
69
- }
61
scheduleUpdateQueue(fiber, updateQueue);
62
},
72
- isFiberUpdater: true,
63
+ enqueueCallback(instance, callback) {
64
+ const fiber = ReactInstanceMap.get(instance);
65
+ let updateQueue = fiber.updateQueue ?
66
+ fiber.updateQueue :
67
+ createUpdateQueue(null);
68
+ addCallbackToQueue(updateQueue, callback);
69
+ scheduleUpdateQueue(fiber, updateQueue);
70
+ },
71
};
72
73
function checkShouldComponentUpdate(workInProgress, oldProps, newProps, newState) {
@@ -210,11 +208,21 @@ module.exports = function(scheduleUpdate : (fiber: Fiber, priorityLevel : ?Prior
208
// TODO: Previous state can be null.
209
let newState;
210
if (updateQueue) {
213
- newState = mergeUpdateQueue(updateQueue, instance, previousState, newProps);
211
+ if (!updateQueue.hasUpdate) {
212
+ newState = previousState;
213
+ } else {
214
+ newState = mergeUpdateQueue(updateQueue, instance, previousState, newProps);
215
+ }
216
} else {
217
newState = previousState;
218
}
219
220
+ if (oldProps === newProps &&
221
+ previousState === newState &&
222
+ updateQueue && !updateQueue.isForced) {
223
+ return false;
224
+ }
225
+
226
if (!checkShouldComponentUpdate(
227
workInProgress,
228
oldProps,
src/renderers/shared/fiber/ReactFiberCommitWork.js
+27
-28
@@ -29,7 +29,8 @@ var { callCallbacks } = require('ReactFiberUpdateQueue');
29
30
var {
31
Placement,
32
- PlacementAndUpdate,
32
+ Update,
33
+ Callback,
34
} = require('ReactTypeOfSideEffect');
35
36
module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) {
@@ -102,8 +103,7 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) {
103
// If it is not host node and, we might have a host node inside it.
104
// Try to search down until we find one.
105
// TODO: For coroutines, this will have to search the stateNode.
105
- if (node.effectTag === Placement ||
106
- node.effectTag === PlacementAndUpdate) {
106
+ if (node.effectTag & Placement) {
107
// If we don't have a child, try the siblings instead.
108
continue siblings;
109
}
@@ -114,8 +114,7 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) {
114
}
115
}
116
// Check if this host node is stable or about to be placed.
117
- if (node.effectTag !== Placement &&
118
- node.effectTag !== PlacementAndUpdate) {
117
+ if (!(node.effectTag & Placement)) {
118
// Found it!
119
return node.stateNode;
120
}
@@ -329,41 +328,41 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) {
328
case ClassComponent: {
329
const instance = finishedWork.stateNode;
330
let error = null;
332
- if (!current) {
333
- if (typeof instance.componentDidMount === 'function') {
334
- error = tryCallComponentDidMount(instance);
335
- }
336
- } else {
337
- if (typeof instance.componentDidUpdate === 'function') {
338
- const prevProps = current.memoizedProps;
339
- const prevState = current.memoizedState;
340
- error = tryCallComponentDidUpdate(instance, prevProps, prevState);
331
+ if (finishedWork.effectTag & Update) {
332
+ if (!current) {
333
+ if (typeof instance.componentDidMount === 'function') {
334
+ error = tryCallComponentDidMount(instance);
335
+ }
336
+ } else {
337
+ if (typeof instance.componentDidUpdate === 'function') {
338
+ const prevProps = current.memoizedProps;
339
+ const prevState = current.memoizedState;
340
+ error = tryCallComponentDidUpdate(instance, prevProps, prevState);
341
+ }
342
}
343
+ attachRef(current, finishedWork, instance);
344
}
343
- // Clear updates from current fiber. This must go before the callbacks
344
- // are reset, in case an update is triggered from inside a callback. Is
345
- // this safe? Relies on the assumption that work is only committed if
346
- // the update queue is empty.
345
+ // Clear updates from current fiber.
346
if (finishedWork.alternate) {
347
finishedWork.alternate.updateQueue = null;
348
}
350
- if (finishedWork.callbackList) {
351
- const { callbackList } = finishedWork;
352
- finishedWork.callbackList = null;
353
- callCallbacks(callbackList, instance);
349
+ if (finishedWork.effectTag & Callback) {
350
+ if (finishedWork.callbackList) {
351
+ callCallbacks(finishedWork.callbackList, instance);
352
+ finishedWork.callbackList = null;
353
+ }
354
}
355
- attachRef(current, finishedWork, instance);
355
if (error) {
356
return trapError(finishedWork, error);
357
}
358
return null;
359
}
360
case HostContainer: {
362
- const instance = finishedWork.stateNode;
363
- if (instance.callbackList) {
364
- const { callbackList } = instance;
365
- instance.callbackList = null;
366
- callCallbacks(callbackList, instance.current.child.stateNode);
361
+ const rootFiber = finishedWork.stateNode;
362
+ if (rootFiber.callbackList) {
363
+ const { callbackList } = rootFiber;
364
+ rootFiber.callbackList = null;
365
+ callCallbacks(callbackList, rootFiber.current.child.stateNode);
366
}
367
}
368
case HostComponent: {
src/renderers/shared/fiber/ReactFiberCompleteWork.js
+15
-4
@@ -34,6 +34,7 @@ var {
34
} = ReactTypeOfWork;
35
var {
36
Update,
37
+ Callback,
38
} = ReactTypeOfSideEffect;
39
40
module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) {
@@ -48,6 +49,11 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) {
49
workInProgress.effectTag |= Update;
50
}
51
52
+ function markCallback(workInProgress : Fiber) {
53
+ // Tag the fiber with a callback effect.
54
+ workInProgress.effectTag |= Callback;
55
+ }
56
+
57
function transferOutput(child : ?Fiber, returnFiber : Fiber) {
58
// If we have a single result, we just pass that through as the output to
59
// avoid unnecessary traversal. When we have multiple output, we just pass
@@ -124,19 +130,24 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) {
130
// Also need to transfer the props, because pendingProps will be null
131
// in the case of an update
132
const { state, props } = workInProgress.stateNode;
133
+ const updateQueue = workInProgress.updateQueue;
134
workInProgress.memoizedState = state;
135
workInProgress.memoizedProps = props;
129
- // Transfer update queue to callbackList field so callbacks can be
130
- // called during commit phase.
131
- workInProgress.callbackList = workInProgress.updateQueue;
136
if (current) {
137
if (current.memoizedProps !== workInProgress.memoizedProps ||
134
- current.memoizedState !== workInProgress.memoizedState) {
138
+ current.memoizedState !== workInProgress.memoizedState ||
139
+ updateQueue && updateQueue.isForced) {
140
markUpdate(workInProgress);
141
}
142
} else {
143
markUpdate(workInProgress);
144
}
145
+ if (updateQueue && updateQueue.hasCallback) {
146
+ // Transfer update queue to callbackList field so callbacks can be
147
+ // called during commit phase.
148
+ workInProgress.callbackList = updateQueue;
149
+ markCallback(workInProgress);
150
+ }
151
return null;
152
case HostContainer:
153
transferOutput(workInProgress.child, workInProgress);
src/renderers/shared/fiber/ReactFiberScheduler.js
+37
-36
@@ -39,6 +39,11 @@ var {
39
Update,
40
PlacementAndUpdate,
41
Deletion,
42
+ Callback,
43
+ PlacementAndCallback,
44
+ UpdateAndCallback,
45
+ PlacementAndUpdateAndCallback,
46
+ DeletionAndCallback,
47
} = require('ReactTypeOfSideEffect');
48
49
var {
@@ -52,9 +57,6 @@ if (__DEV__) {
57
var timeHeuristicForUnitOfWork = 1;
58
59
module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) {
55
- // Use a closure to circumvent the circular dependency between the scheduler
56
- // and ReactFiberBeginWork. Don't know if there's a better way to do this.
57
-
60
const { beginWork } = ReactFiberBeginWork(config, scheduleUpdate);
61
const { completeWork } = ReactFiberCompleteWork(config);
62
const { commitInsertion, commitDeletion, commitWork, commitLifeCycles } =
@@ -139,28 +141,34 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) {
141
let effectfulFiber = finishedWork.firstEffect;
142
while (effectfulFiber) {
143
switch (effectfulFiber.effectTag) {
142
- case Placement: {
144
+ case Placement:
145
+ case PlacementAndCallback: {
146
commitInsertion(effectfulFiber);
144
- // Clear the effect tag so that we know that this is inserted, before
147
+ // Clear the "placement" from effect tag so that we know that this is inserted, before
148
// any life-cycles like componentDidMount gets called.
149
effectfulFiber.effectTag = NoEffect;
150
break;
151
}
149
- case PlacementAndUpdate: {
152
+ case PlacementAndUpdate:
153
+ case PlacementAndUpdateAndCallback: {
154
+ // Placement
155
commitInsertion(effectfulFiber);
151
- const current = effectfulFiber.alternate;
152
- commitWork(current, effectfulFiber);
156
// Clear the "placement" from effect tag so that we know that this is inserted, before
157
// any life-cycles like componentDidMount gets called.
158
effectfulFiber.effectTag = Update;
159
+
160
+ // Update
161
+ const current = effectfulFiber.alternate;
162
+ commitWork(current, effectfulFiber);
163
break;
164
}
158
- case Update: {
165
+ case Update:
166
+ case UpdateAndCallback:
167
const current = effectfulFiber.alternate;
168
commitWork(current, effectfulFiber);
169
break;
162
- }
163
- case Deletion: {
170
+ case Deletion:
171
+ case DeletionAndCallback:
172
// Deletion might cause an error in componentWillUnmount().
173
// We will continue nevertheless and handle those later on.
174
const trappedErrors = commitDeletion(effectfulFiber);
@@ -176,8 +184,8 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) {
184
}
185
}
186
break;
179
- }
187
}
188
+
189
effectfulFiber = effectfulFiber.nextEffect;
190
}
191
@@ -186,8 +194,7 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) {
194
// already been invoked.
195
effectfulFiber = finishedWork.firstEffect;
196
while (effectfulFiber) {
189
- if (effectfulFiber.effectTag === Update ||
190
- effectfulFiber.effectTag === PlacementAndUpdate) {
197
+ if (effectfulFiber.effectTag & (Update | Callback)) {
198
const current = effectfulFiber.alternate;
199
const trappedError = commitLifeCycles(current, effectfulFiber);
200
if (trappedError) {
@@ -540,22 +547,22 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) {
547
}
548
}
549
543
- function performAndHandleErrors(priorityLevel : PriorityLevel, deadline : ?Deadline) {
550
+ function performAndHandleErrors(priorityLevel : PriorityLevel, deadline : null | Deadline) {
551
// The exact priority level doesn't matter, so long as it's in range of the
552
// work (sync, animation, deferred) being performed.
553
try {
554
if (priorityLevel === SynchronousPriority) {
555
performSynchronousWorkUnsafe();
549
- }
550
- if (priorityLevel > AnimationPriority) {
556
+ } else if (priorityLevel > AnimationPriority) {
557
if (!deadline) {
558
throw new Error('No deadline');
559
} else {
560
performDeferredWorkUnsafe(deadline);
561
}
562
return;
563
+ } else {
564
+ performAnimationWorkUnsafe();
565
}
558
- performAnimationWorkUnsafe();
566
} catch (error) {
567
const failedUnitOfWork = nextUnitOfWork;
568
// Reset because it points to the error boundary:
@@ -639,31 +646,25 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) {
646
}
647
}
648
642
- function scheduleWork(root : FiberRoot, priorityLevel : ?PriorityLevel) {
643
- if (priorityLevel == null) {
644
- priorityLevel = priorityContext;
645
- }
646
-
647
- if (priorityLevel === SynchronousPriority) {
648
- scheduleSynchronousWork(root);
649
- }
649
+ function scheduleWork(root : FiberRoot) {
650
+ scheduleWorkAtPriority(root, priorityContext);
651
+ }
652
653
+ function scheduleWorkAtPriority(root : FiberRoot, priorityLevel : PriorityLevel) {
654
if (priorityLevel === NoWork) {
655
return;
653
- }
654
- if (priorityLevel > AnimationPriority) {
656
+ } else if (priorityLevel === SynchronousPriority) {
657
+ scheduleSynchronousWork(root);
658
+ } else if (priorityLevel <= AnimationPriority) {
659
+ scheduleAnimationWork(root, priorityLevel);
660
+ } else {
661
scheduleDeferredWork(root, priorityLevel);
662
return;
663
}
658
- scheduleAnimationWork(root, priorityLevel);
664
}
665
661
- function scheduleUpdate(fiber: Fiber, priorityLevel : ?PriorityLevel): void {
662
- // Use priority context if no priority is provided
663
- if (priorityLevel == null) {
664
- priorityLevel = priorityContext;
665
- }
666
-
666
+ function scheduleUpdate(fiber : Fiber) {
667
+ const priorityLevel = priorityContext;
668
while (true) {
669
if (fiber.pendingWorkPriority === NoWork ||
670
fiber.pendingWorkPriority >= priorityLevel) {
@@ -678,7 +679,7 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) {
679
if (!fiber.return) {
680
if (fiber.tag === HostContainer) {
681
const root : FiberRoot = (fiber.stateNode : any);
681
- scheduleWork(root, priorityLevel);
682
+ scheduleWorkAtPriority(root, priorityLevel);
683
return;
684
} else {
685
throw new Error('Invalid root');
src/renderers/shared/fiber/ReactFiberUpdateQueue.js
+13
-2
@@ -22,6 +22,8 @@ type UpdateQueueNode = {
22
export type UpdateQueue = UpdateQueueNode & {
23
isReplace: boolean,
24
isForced: boolean,
25
+ hasUpdate: boolean,
26
+ hasCallback: boolean,
27
tail: UpdateQueueNode
28
};
29
@@ -33,6 +35,8 @@ exports.createUpdateQueue = function(partialState : mixed) : UpdateQueue {
35
next: null,
36
isReplace: false,
37
isForced: false,
38
+ hasUpdate: partialState != null,
39
+ hasCallback: false,
40
tail: (null : any),
41
};
42
queue.tail = queue;
@@ -48,6 +52,7 @@ function addToQueue(queue : UpdateQueue, partialState : mixed) : UpdateQueue {
52
};
53
queue.tail.next = node;
54
queue.tail = node;
55
+ queue.hasUpdate = queue.hasUpdate || (partialState == null);
56
return queue;
57
}
58
@@ -59,15 +64,21 @@ exports.addCallbackToQueue = function(queue : UpdateQueue, callback: Function) :
64
addToQueue(queue, null);
65
}
66
queue.tail.callback = callback;
67
+ queue.hasCallback = true;
68
return queue;
69
};
70
71
exports.callCallbacks = function(queue : UpdateQueue, context : any) {
72
let node : ?UpdateQueueNode = queue;
73
while (node) {
68
- if (node.callback && !node.callbackWasCalled) {
74
+ const callback = node.callback;
75
+ if (callback && !node.callbackWasCalled) {
76
node.callbackWasCalled = true;
70
- node.callback.call(context);
77
+ if (typeof context !== 'undefined') {
78
+ callback.call(context);
79
+ } else {
80
+ callback();
81
+ }
82
}
83
node = node.next;
84
}
src/renderers/shared/fiber/ReactTypeOfSideEffect.js
+11
-6
@@ -12,12 +12,17 @@
12
13
'use strict';
14
15
-export type TypeOfSideEffect = 0 | 1 | 2 | 3 | 4;
15
+export type TypeOfSideEffect = 0 | 1 | 2 | 3 | 4 | 8 | 9 | 10 | 11 | 12;
16
17
module.exports = {
18
- NoEffect: 0,
19
- Placement: 1,
20
- Update: 2,
21
- PlacementAndUpdate: 3,
22
- Deletion: 4,
18
+ NoEffect: 0, // 0b0000
19
+ Placement: 1, // 0b0001
20
+ Update: 2, // 0b0010
21
+ PlacementAndUpdate: 3, // 0b0011
22
+ Deletion: 4, // 0b0100
23
+ Callback: 8, // 0b1000
24
+ PlacementAndCallback: 9, // 0b1001
25
+ UpdateAndCallback: 10, // 0b1010
26
+ PlacementAndUpdateAndCallback: 11, // 0b1011
27
+ DeletionAndCallback: 12, // 0b1100
28
};
src/renderers/shared/fiber/__tests__/ReactIncrementalSideEffects-test.js
+29
@@ -860,6 +860,35 @@ describe('ReactIncrementalSideEffects', () => {
860
expect(called).toBe(true);
861
});
862
863
+ it('calls setState callback even if component bails out', () => {
864
+ let instance;
865
+ class Foo extends React.Component {
866
+ constructor() {
867
+ super();
868
+ instance = this;
869
+ this.state = { text: 'foo' };
870
+ }
871
+ shouldComponentUpdate(nextProps, nextState) {
872
+ return this.state.text !== nextState.text;
873
+ }
874
+ render() {
875
+ return <span prop={this.state.text} />;
876
+ }
877
+ }
878
+
879
+ ReactNoop.render(<Foo />);
880
+ ReactNoop.flush();
881
+ expect(ReactNoop.getChildren()).toEqual([
882
+ span('foo'),
883
+ ]);
884
+ let called = false;
885
+ instance.setState({}, () => {
886
+ called = true;
887
+ });
888
+ ReactNoop.flush();
889
+ expect(called).toBe(true);
890
+ });
891
+
892
// TODO: Test that callbacks are not lost if an update is preempted.
893
894
it('calls componentWillUnmount after a deletion, even if nested', () => {