Skip to content

Commit 11d0781

Browse files
committed
Defer useEffect until after paint
Effects scheduled by useEffect should not fire until after the browser has had a chance to paint. However, they should be fired before any subsequent mutations. Also adds useMutationEffect and useLayoutEffect. useMutationEffect fires during the host update phase. useLayoutEffect fires during the post- update phase (the same phase as componentDidMount and componentDidUpdate).
1 parent 105f2de commit 11d0781

11 files changed

Lines changed: 966 additions & 253 deletions

packages/react-reconciler/src/ReactFiberCommitWork.js

Lines changed: 65 additions & 114 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,16 @@ import {
8686
requestCurrentTime,
8787
scheduleWork,
8888
} from './ReactFiberScheduler';
89+
import {
90+
NoEffect as NoHookEffect,
91+
UnmountSnapshot,
92+
UnmountMutation,
93+
MountMutation,
94+
UnmountLayout,
95+
MountLayout,
96+
UnmountPassive,
97+
MountPassive,
98+
} from './ReactHookEffectTags';
8999

90100
let didWarnAboutUndefinedSnapshotBeforeUpdate: Set<mixed> | null = null;
91101
if (__DEV__) {
@@ -204,6 +214,11 @@ function commitBeforeMutationLifeCycles(
204214
finishedWork: Fiber,
205215
): void {
206216
switch (finishedWork.tag) {
217+
case FunctionComponent:
218+
case ForwardRef: {
219+
commitHookEffectList(UnmountSnapshot, NoHookEffect, finishedWork);
220+
return;
221+
}
207222
case ClassComponent: {
208223
if (finishedWork.effectTag & Snapshot) {
209224
if (current !== null) {
@@ -254,26 +269,39 @@ function commitBeforeMutationLifeCycles(
254269
}
255270
}
256271

257-
function destroyRemainingEffects(firstToDestroy, stopAt) {
258-
let effect = firstToDestroy;
259-
do {
260-
const destroy = effect.value;
261-
if (destroy !== null) {
262-
destroy();
263-
}
264-
effect = effect.next;
265-
} while (effect !== stopAt);
272+
function commitHookEffectList(
273+
unmountTag: number,
274+
mountTag: number,
275+
finishedWork: Fiber,
276+
) {
277+
const updateQueue: FunctionComponentUpdateQueue | null = (finishedWork.updateQueue: any);
278+
let lastEffect = updateQueue !== null ? updateQueue.lastEffect : null;
279+
if (lastEffect !== null) {
280+
const firstEffect = lastEffect.next;
281+
let effect = firstEffect;
282+
do {
283+
if ((effect.tag & unmountTag) !== NoHookEffect) {
284+
// Unmount
285+
const destroy = effect.destroy;
286+
effect.destroy = null;
287+
if (destroy !== null) {
288+
destroy();
289+
}
290+
}
291+
if ((effect.tag & mountTag) !== NoHookEffect) {
292+
// Mount
293+
const create = effect.create;
294+
const destroy = create();
295+
effect.destroy = typeof destroy === 'function' ? destroy : null;
296+
}
297+
effect = effect.next;
298+
} while (effect !== firstEffect);
299+
}
266300
}
267301

268-
function destroyMountedEffects(current) {
269-
const oldUpdateQueue: FunctionComponentUpdateQueue | null = (current.updateQueue: any);
270-
if (oldUpdateQueue !== null) {
271-
const oldLastEffect = oldUpdateQueue.lastEffect;
272-
if (oldLastEffect !== null) {
273-
const oldFirstEffect = oldLastEffect.next;
274-
destroyRemainingEffects(oldFirstEffect, oldFirstEffect);
275-
}
276-
}
302+
export function commitPassiveHookEffects(finishedWork: Fiber): void {
303+
commitHookEffectList(UnmountPassive, NoHookEffect, finishedWork);
304+
commitHookEffectList(NoHookEffect, MountPassive, finishedWork);
277305
}
278306

279307
function commitLifeCycles(
@@ -285,98 +313,12 @@ function commitLifeCycles(
285313
switch (finishedWork.tag) {
286314
case FunctionComponent:
287315
case ForwardRef: {
288-
const updateQueue: FunctionComponentUpdateQueue | null = (finishedWork.updateQueue: any);
289-
if (updateQueue !== null) {
290-
// Mount new effects and destroy the old ones by comparing to the
291-
// current list of effects. This could be a bit simpler if we avoided
292-
// the need to compare to the previous effect list by transferring the
293-
// old `destroy` method to the new effect during the render phase.
294-
// That's how I originally implemented it, but it requires an additional
295-
// field on the effect object.
296-
//
297-
// This supports removing effects from the end of the list. If we adopt
298-
// the constraint that hooks are append only, that would also save a bit
299-
// on code size.
300-
const newLastEffect = updateQueue.lastEffect;
301-
if (newLastEffect !== null) {
302-
const newFirstEffect = newLastEffect.next;
303-
let oldLastEffect = null;
304-
if (current !== null) {
305-
const oldUpdateQueue: FunctionComponentUpdateQueue | null = (current.updateQueue: any);
306-
if (oldUpdateQueue !== null) {
307-
oldLastEffect = oldUpdateQueue.lastEffect;
308-
}
309-
}
310-
if (oldLastEffect !== null) {
311-
const oldFirstEffect = oldLastEffect.next;
312-
let newEffect = newFirstEffect;
313-
let oldEffect = oldFirstEffect;
314-
315-
// Before mounting the new effects, unmount all the old ones.
316-
do {
317-
if (oldEffect !== null) {
318-
if (newEffect.inputs !== oldEffect.inputs) {
319-
const destroy = oldEffect.value;
320-
if (destroy !== null) {
321-
destroy();
322-
}
323-
}
324-
oldEffect = oldEffect.next;
325-
if (oldEffect === oldFirstEffect) {
326-
oldEffect = null;
327-
}
328-
}
329-
newEffect = newEffect.next;
330-
} while (newEffect !== newFirstEffect);
331-
332-
// Unmount any remaining effects in the old list that do not
333-
// appear in the new one.
334-
if (oldEffect !== null) {
335-
destroyRemainingEffects(oldEffect, oldFirstEffect);
336-
}
337-
338-
// Now loop through the list again to mount the new effects
339-
oldEffect = oldFirstEffect;
340-
do {
341-
const create = newEffect.value;
342-
if (oldEffect !== null) {
343-
if (newEffect.inputs !== oldEffect.inputs) {
344-
const newDestroy = create();
345-
newEffect.value =
346-
typeof newDestroy === 'function' ? newDestroy : null;
347-
} else {
348-
newEffect.value = oldEffect.value;
349-
}
350-
oldEffect = oldEffect.next;
351-
if (oldEffect === oldFirstEffect) {
352-
oldEffect = null;
353-
}
354-
} else {
355-
const newDestroy = create();
356-
newEffect.value =
357-
typeof newDestroy === 'function' ? newDestroy : null;
358-
}
359-
newEffect = newEffect.next;
360-
} while (newEffect !== newFirstEffect);
361-
} else {
362-
let newEffect = newFirstEffect;
363-
do {
364-
const create = newEffect.value;
365-
const newDestroy = create();
366-
newEffect.value =
367-
typeof newDestroy === 'function' ? newDestroy : null;
368-
newEffect = newEffect.next;
369-
} while (newEffect !== newFirstEffect);
370-
}
371-
} else if (current !== null) {
372-
// There are no effects, which means all current effects must
373-
// be destroyed
374-
destroyMountedEffects(current);
375-
}
376-
377-
const callbackList = updateQueue.callbackList;
316+
commitHookEffectList(UnmountLayout, MountLayout, finishedWork);
317+
const newUpdateQueue: FunctionComponentUpdateQueue | null = (finishedWork.updateQueue: any);
318+
if (newUpdateQueue !== null) {
319+
const callbackList = newUpdateQueue.callbackList;
378320
if (callbackList !== null) {
379-
updateQueue.callbackList = null;
321+
newUpdateQueue.callbackList = null;
380322
for (let i = 0; i < callbackList.length; i++) {
381323
const update = callbackList[i];
382324
// Assume this is non-null, since otherwise it would not be part
@@ -386,10 +328,6 @@ function commitLifeCycles(
386328
callback();
387329
}
388330
}
389-
} else if (current !== null) {
390-
// There are no effects, which means all current effects must
391-
// be destroyed
392-
destroyMountedEffects(current);
393331
}
394332
break;
395333
}
@@ -656,7 +594,7 @@ function commitUnmount(current: Fiber): void {
656594
const firstEffect = lastEffect.next;
657595
let effect = firstEffect;
658596
do {
659-
const destroy = effect.value;
597+
const destroy = effect.destroy;
660598
if (destroy !== null) {
661599
safelyCallDestroy(current, destroy);
662600
}
@@ -1039,11 +977,24 @@ function commitDeletion(current: Fiber): void {
1039977

1040978
function commitWork(current: Fiber | null, finishedWork: Fiber): void {
1041979
if (!supportsMutation) {
980+
switch (finishedWork.tag) {
981+
case FunctionComponent:
982+
case ForwardRef: {
983+
commitHookEffectList(UnmountMutation, MountMutation, finishedWork);
984+
return;
985+
}
986+
}
987+
1042988
commitContainer(finishedWork);
1043989
return;
1044990
}
1045991

1046992
switch (finishedWork.tag) {
993+
case FunctionComponent:
994+
case ForwardRef: {
995+
commitHookEffectList(UnmountMutation, MountMutation, finishedWork);
996+
return;
997+
}
1047998
case ClassComponent: {
1048999
return;
10491000
}

packages/react-reconciler/src/ReactFiberDispatcher.js

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,22 +9,28 @@
99

1010
import {readContext} from './ReactFiberNewContext';
1111
import {
12-
useState,
13-
useReducer,
14-
useEffect,
12+
useAPI,
1513
useCallback,
14+
useContext,
15+
useEffect,
16+
useLayoutEffect,
1617
useMemo,
18+
useMutationEffect,
19+
useReducer,
1720
useRef,
18-
useAPI,
21+
useState,
1922
} from './ReactFiberHooks';
2023

2124
export const Dispatcher = {
2225
readContext,
23-
useState,
24-
useReducer,
25-
useEffect,
26+
useAPI,
2627
useCallback,
28+
useContext,
29+
useEffect,
30+
useLayoutEffect,
2731
useMemo,
32+
useMutationEffect,
33+
useReducer,
2834
useRef,
29-
useAPI,
35+
useState,
3036
};

0 commit comments

Comments
 (0)