Skip to content

Commit 9f81d8c

Browse files
committed
Process completion callbacks immediately after completing a root
We need to process completion callbacks in two places. The first is intuitive: right after a root completes. It might seem like that is sufficient. But if a completion callback is scheduled on an already completed root, it's possible we won't complete that root again. So we also need to process completion callbacks whenever we skip over an already completed root.
1 parent 98341e1 commit 9f81d8c

File tree

2 files changed

+93
-36
lines changed

2 files changed

+93
-36
lines changed

src/renderers/shared/fiber/ReactFiberScheduler.js

Lines changed: 35 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -398,33 +398,10 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
398398
// no longer blocked, return the time at which it completed so that we
399399
// can commit it.
400400
if (isRootBlocked(root, completedAt)) {
401-
// Process pending completion callbacks so that they are called at
402-
// the end of the current batch.
403-
const completionCallbacks = root.completionCallbacks;
404-
if (completionCallbacks !== null) {
405-
processUpdateQueue(
406-
completionCallbacks,
407-
null,
408-
null,
409-
null,
410-
completedAt,
411-
);
412-
const callbackList = completionCallbacks.callbackList;
413-
if (callbackList !== null) {
414-
// Add new callbacks to list of completion callbacks
415-
if (rootCompletionCallbackList === null) {
416-
rootCompletionCallbackList = callbackList;
417-
} else {
418-
for (let i = 0; i < callbackList.length; i++) {
419-
rootCompletionCallbackList.push(callbackList[i]);
420-
}
421-
}
422-
completionCallbacks.callbackList = null;
423-
if (completionCallbacks.first === null) {
424-
root.completionCallbacks = null;
425-
}
426-
}
427-
}
401+
// We usually process completion callbacks right after a root is
402+
// completed. But this root already completed, and it's possible that
403+
// we received new completion callbacks since then.
404+
processCompletionCallbacks(root, completedAt);
428405
return Done;
429406
}
430407

@@ -434,6 +411,33 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
434411
return expirationTime;
435412
}
436413

414+
function processCompletionCallbacks(
415+
root: FiberRoot,
416+
completedAt: ExpirationTime,
417+
) {
418+
// Process pending completion callbacks so that they are called at
419+
// the end of the current batch.
420+
const completionCallbacks = root.completionCallbacks;
421+
if (completionCallbacks !== null) {
422+
processUpdateQueue(completionCallbacks, null, null, null, completedAt);
423+
const callbackList = completionCallbacks.callbackList;
424+
if (callbackList !== null) {
425+
// Add new callbacks to list of completion callbacks
426+
if (rootCompletionCallbackList === null) {
427+
rootCompletionCallbackList = callbackList;
428+
} else {
429+
for (let i = 0; i < callbackList.length; i++) {
430+
rootCompletionCallbackList.push(callbackList[i]);
431+
}
432+
}
433+
completionCallbacks.callbackList = null;
434+
if (completionCallbacks.first === null) {
435+
root.completionCallbacks = null;
436+
}
437+
}
438+
}
439+
}
440+
437441
function commitAllHostEffects() {
438442
while (nextEffect !== null) {
439443
if (__DEV__) {
@@ -819,6 +823,7 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
819823
// The root is not blocked, so we can commit it now.
820824
pendingCommit = workInProgress;
821825
}
826+
processCompletionCallbacks(root, nextRenderExpirationTime);
822827
return null;
823828
}
824829
}
@@ -1601,12 +1606,9 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
16011606
}
16021607
break;
16031608
case TaskPriority:
1604-
invariant(
1605-
isBatchingUpdates,
1606-
'Task updates can only be scheduled as a nested update or ' +
1607-
'inside batchedUpdates. This error is likely caused by a ' +
1608-
'bug in React. Please file an issue.',
1609-
);
1609+
if (!isPerformingWork && !isBatchingUpdates) {
1610+
performWork(TaskPriority, null);
1611+
}
16101612
break;
16111613
default:
16121614
// This update is async. Schedule a callback.

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

Lines changed: 58 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,62 @@ describe('ReactIncrementalRoot', () => {
9595
expect(root.getChildren()).toEqual([]);
9696
});
9797

98-
it(
99-
'does not work on on a blocked tree if the expiration time is greater than the blocked update',
100-
);
98+
it('does not work on on a blocked tree if the expiration time is greater than the blocked update', () => {
99+
let ops = [];
100+
function Foo(props) {
101+
ops.push('Foo: ' + props.children);
102+
return <span prop={props.children} />;
103+
}
104+
const root = ReactNoop.createRoot();
105+
root.prerender(<Foo>A</Foo>);
106+
ReactNoop.flush();
107+
108+
expect(ops).toEqual(['Foo: A']);
109+
expect(root.getChildren()).toEqual([]);
110+
111+
// workB has a later expiration time
112+
ReactNoop.expire(1000);
113+
root.prerender(<Foo>B</Foo>);
114+
ReactNoop.flush();
115+
116+
// Should not have re-rendered the root at the later expiration time
117+
expect(ops).toEqual(['Foo: A']);
118+
expect(root.getChildren()).toEqual([]);
119+
});
120+
121+
it('commits earlier work without committing later work', () => {
122+
const root = ReactNoop.createRoot();
123+
const work1 = root.prerender(<span prop="A" />);
124+
ReactNoop.flush();
125+
126+
expect(root.getChildren()).toEqual([]);
127+
128+
// Second prerender has a later expiration time
129+
ReactNoop.expire(1000);
130+
root.prerender(<span prop="B" />);
131+
132+
work1.commit();
133+
134+
// Should not have re-rendered the root at the later expiration time
135+
expect(root.getChildren()).toEqual([span('A')]);
136+
});
137+
138+
it('flushes ealier work if later work is committed', () => {
139+
let ops = [];
140+
const root = ReactNoop.createRoot();
141+
const work1 = root.prerender(<span prop="A" />);
142+
// Second prerender has a later expiration time
143+
ReactNoop.expire(1000);
144+
const work2 = root.prerender(<span prop="B" />);
145+
146+
work1.then(() => ops.push('complete 1'));
147+
work2.then(() => ops.push('complete 2'));
148+
149+
work2.commit();
150+
151+
// Because the later prerender was committed, the earlier one should have
152+
// committed, too.
153+
expect(root.getChildren()).toEqual([span('B')]);
154+
expect(ops).toEqual(['complete 1', 'complete 2']);
155+
});
101156
});

0 commit comments

Comments
 (0)