Skip to content

Commit b073a2e

Browse files
committed
Allow useReducer to bail out of rendering by returning previous state
This is conceptually similar to `shouldComponentUpdate`, except because there could be multiple useReducer (or useState) Hooks in a single component, we can only bail out if none of the Hooks produce a new value. We also can't bail out if any the other types of inputs — state and context — have changed. These optimizations rely on the constraint that components are pure functions of props, state, and context. In some cases, we can bail out without entering the render phase by eagerly computing the next state and comparing it to the current one. This only works if we are absolutely certain that the queue is empty at the time of the update. In concurrent mode, this is difficult to determine, because there could be multiple copies of the queue and we don't know which one is current without doing lots of extra work, which would defeat the purpose of the optimization. However, in our implementation, there are at most only two copies of the queue, and if *both* are empty then we know that the current queue must be.
1 parent 3e15b1c commit b073a2e

7 files changed

+563
-102
lines changed

packages/react-reconciler/src/ReactFiber.js

+5-5
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import type {TypeOfMode} from './ReactTypeOfMode';
1414
import type {SideEffectTag} from 'shared/ReactSideEffectTags';
1515
import type {ExpirationTime} from './ReactFiberExpirationTime';
1616
import type {UpdateQueue} from './ReactUpdateQueue';
17-
import type {ContextDependency} from './ReactFiberNewContext';
17+
import type {ContextDependencyList} from './ReactFiberNewContext';
1818

1919
import invariant from 'shared/invariant';
2020
import warningWithoutStack from 'shared/warningWithoutStack';
@@ -141,7 +141,7 @@ export type Fiber = {|
141141
memoizedState: any,
142142

143143
// A linked-list of contexts that this fiber depends on
144-
firstContextDependency: ContextDependency<mixed> | null,
144+
contextDependencies: ContextDependencyList | null,
145145

146146
// Bitfield that describes properties about the fiber and its subtree. E.g.
147147
// the ConcurrentMode flag indicates whether the subtree should be async-by-
@@ -237,7 +237,7 @@ function FiberNode(
237237
this.memoizedProps = null;
238238
this.updateQueue = null;
239239
this.memoizedState = null;
240-
this.firstContextDependency = null;
240+
this.contextDependencies = null;
241241

242242
this.mode = mode;
243243

@@ -403,7 +403,7 @@ export function createWorkInProgress(
403403
workInProgress.memoizedProps = current.memoizedProps;
404404
workInProgress.memoizedState = current.memoizedState;
405405
workInProgress.updateQueue = current.updateQueue;
406-
workInProgress.firstContextDependency = current.firstContextDependency;
406+
workInProgress.contextDependencies = current.contextDependencies;
407407

408408
// These will be overridden during the parent's reconciliation
409409
workInProgress.sibling = current.sibling;
@@ -704,7 +704,7 @@ export function assignFiberPropertiesInDEV(
704704
target.memoizedProps = source.memoizedProps;
705705
target.updateQueue = source.updateQueue;
706706
target.memoizedState = source.memoizedState;
707-
target.firstContextDependency = source.firstContextDependency;
707+
target.contextDependencies = source.contextDependencies;
708708
target.mode = source.mode;
709709
target.effectTag = source.effectTag;
710710
target.nextEffect = source.nextEffect;

packages/react-reconciler/src/ReactFiberBeginWork.js

+77-27
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ import {
9090
prepareToReadContext,
9191
calculateChangedBits,
9292
} from './ReactFiberNewContext';
93-
import {prepareToUseHooks, finishHooks, resetHooks} from './ReactFiberHooks';
93+
import {resetHooks, renderWithHooks} from './ReactFiberHooks';
9494
import {stopProfilerTimerIfRunning} from './ReactProfilerTimer';
9595
import {
9696
getMaskedContext,
@@ -237,19 +237,37 @@ function updateForwardRef(
237237
// The rest is a fork of updateFunctionComponent
238238
let nextChildren;
239239
prepareToReadContext(workInProgress, renderExpirationTime);
240-
prepareToUseHooks(current, workInProgress, renderExpirationTime);
241240
if (__DEV__) {
242241
ReactCurrentOwner.current = workInProgress;
243242
setCurrentPhase('render');
244-
nextChildren = render(nextProps, ref);
243+
nextChildren = renderWithHooks(
244+
current,
245+
workInProgress,
246+
render,
247+
nextProps,
248+
ref,
249+
renderExpirationTime,
250+
);
245251
setCurrentPhase(null);
246252
} else {
247-
nextChildren = render(nextProps, ref);
253+
nextChildren = renderWithHooks(
254+
current,
255+
workInProgress,
256+
render,
257+
nextProps,
258+
ref,
259+
renderExpirationTime,
260+
);
261+
}
262+
263+
if ((workInProgress.effectTag & PerformedWork) === NoEffect) {
264+
return bailoutOnAlreadyFinishedWork(
265+
current,
266+
workInProgress,
267+
renderExpirationTime,
268+
);
248269
}
249-
nextChildren = finishHooks(render, nextProps, nextChildren, ref);
250270

251-
// React DevTools reads this flag.
252-
workInProgress.effectTag |= PerformedWork;
253271
reconcileChildren(
254272
current,
255273
workInProgress,
@@ -350,8 +368,6 @@ function updateMemoComponent(
350368
);
351369
}
352370
}
353-
// React DevTools reads this flag.
354-
workInProgress.effectTag |= PerformedWork;
355371
let newChild = createWorkInProgress(
356372
currentChild,
357373
nextProps,
@@ -506,19 +522,37 @@ function updateFunctionComponent(
506522

507523
let nextChildren;
508524
prepareToReadContext(workInProgress, renderExpirationTime);
509-
prepareToUseHooks(current, workInProgress, renderExpirationTime);
510525
if (__DEV__) {
511526
ReactCurrentOwner.current = workInProgress;
512527
setCurrentPhase('render');
513-
nextChildren = Component(nextProps, context);
528+
nextChildren = renderWithHooks(
529+
current,
530+
workInProgress,
531+
Component,
532+
nextProps,
533+
context,
534+
renderExpirationTime,
535+
);
514536
setCurrentPhase(null);
515537
} else {
516-
nextChildren = Component(nextProps, context);
538+
nextChildren = renderWithHooks(
539+
current,
540+
workInProgress,
541+
Component,
542+
nextProps,
543+
context,
544+
renderExpirationTime,
545+
);
546+
}
547+
548+
if ((workInProgress.effectTag & PerformedWork) === NoEffect) {
549+
return bailoutOnAlreadyFinishedWork(
550+
current,
551+
workInProgress,
552+
renderExpirationTime,
553+
);
517554
}
518-
nextChildren = finishHooks(Component, nextProps, nextChildren, context);
519555

520-
// React DevTools reads this flag.
521-
workInProgress.effectTag |= PerformedWork;
522556
reconcileChildren(
523557
current,
524558
workInProgress,
@@ -1063,7 +1097,6 @@ function mountIndeterminateComponent(
10631097
const context = getMaskedContext(workInProgress, unmaskedContext);
10641098

10651099
prepareToReadContext(workInProgress, renderExpirationTime);
1066-
prepareToUseHooks(null, workInProgress, renderExpirationTime);
10671100

10681101
let value;
10691102

@@ -1091,12 +1124,24 @@ function mountIndeterminateComponent(
10911124
}
10921125

10931126
ReactCurrentOwner.current = workInProgress;
1094-
value = Component(props, context);
1127+
value = renderWithHooks(
1128+
null,
1129+
workInProgress,
1130+
Component,
1131+
props,
1132+
context,
1133+
renderExpirationTime,
1134+
);
10951135
} else {
1096-
value = Component(props, context);
1136+
value = renderWithHooks(
1137+
null,
1138+
workInProgress,
1139+
Component,
1140+
props,
1141+
context,
1142+
renderExpirationTime,
1143+
);
10971144
}
1098-
// React DevTools reads this flag.
1099-
workInProgress.effectTag |= PerformedWork;
11001145

11011146
if (
11021147
typeof value === 'object' &&
@@ -1147,7 +1192,6 @@ function mountIndeterminateComponent(
11471192
} else {
11481193
// Proceed under the assumption that this is a function component
11491194
workInProgress.tag = FunctionComponent;
1150-
value = finishHooks(Component, props, value, context);
11511195
reconcileChildren(null, workInProgress, value, renderExpirationTime);
11521196
if (__DEV__) {
11531197
validateFunctionComponentInDev(workInProgress, Component);
@@ -1647,9 +1691,11 @@ function bailoutOnAlreadyFinishedWork(
16471691

16481692
if (current !== null) {
16491693
// Reuse previous context list
1650-
workInProgress.firstContextDependency = current.firstContextDependency;
1694+
workInProgress.contextDependencies = current.contextDependencies;
16511695
}
16521696

1697+
workInProgress.effectTag &= ~PerformedWork;
1698+
16531699
if (enableProfilerTimer) {
16541700
// Don't update "base" render times for bailouts.
16551701
stopProfilerTimerIfRunning(workInProgress);
@@ -1680,11 +1726,12 @@ function beginWork(
16801726
if (current !== null) {
16811727
const oldProps = current.memoizedProps;
16821728
const newProps = workInProgress.pendingProps;
1683-
if (
1684-
oldProps === newProps &&
1685-
!hasLegacyContextChanged() &&
1686-
updateExpirationTime < renderExpirationTime
1687-
) {
1729+
1730+
if (oldProps !== newProps || hasLegacyContextChanged()) {
1731+
// If props or context changed, mark the fiber as having performed work.
1732+
// This may be unset if the props are determined to be equal later (memo).
1733+
workInProgress.effectTag |= PerformedWork;
1734+
} else if (updateExpirationTime < renderExpirationTime) {
16881735
// This fiber does not have any pending work. Bailout without entering
16891736
// the begin phase. There's still some bookkeeping we that needs to be done
16901737
// in this optimized path, mostly pushing stuff onto the stack.
@@ -1767,6 +1814,9 @@ function beginWork(
17671814
renderExpirationTime,
17681815
);
17691816
}
1817+
} else {
1818+
// No bailouts on initial mount.
1819+
workInProgress.effectTag |= PerformedWork;
17701820
}
17711821

17721822
// Before entering the begin phase, clear the expiration time.

packages/react-reconciler/src/ReactFiberCompleteWork.js

+4-13
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,6 @@ import {
8282
prepareToHydrateHostTextInstance,
8383
popHydrationState,
8484
} from './ReactFiberHydrationContext';
85-
import {ConcurrentMode, NoContext} from './ReactTypeOfMode';
8685

8786
function markUpdate(workInProgress: Fiber) {
8887
// Tag the fiber with an update effect. This turns a Placement into
@@ -728,18 +727,10 @@ function completeWork(
728727
}
729728
}
730729

731-
// The children either timed out after previously being visible, or
732-
// were restored after previously being hidden. Schedule an effect
733-
// to update their visiblity.
734-
if (
735-
//
736-
nextDidTimeout !== prevDidTimeout ||
737-
// Outside concurrent mode, the primary children commit in an
738-
// inconsistent state, even if they are hidden. So if they are hidden,
739-
// we need to schedule an effect to re-hide them, just in case.
740-
((workInProgress.effectTag & ConcurrentMode) === NoContext &&
741-
nextDidTimeout)
742-
) {
730+
if (nextDidTimeout || prevDidTimeout) {
731+
// If the children are hidden, or if they were previous hidden, schedule
732+
// an effect to toggle their visibility. This is also used to attach a
733+
// retry listener to the promise.
743734
workInProgress.effectTag |= Update;
744735
}
745736
break;

0 commit comments

Comments
 (0)