Skip to content

Commit 790c8ef

Browse files
authored
Allow useReducer to bail out of rendering by returning previous state (#14569)
* 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. * Add test for context consumers inside hidden subtree Should not bail out during subsequent update. (This isn't directly related to this PR because we should have had this test, anyway.) * Refactor to use module-level variable instead of effect bit * Add test combining state bailout and props bailout (memo)
1 parent 8a12009 commit 790c8ef

7 files changed

+673
-101
lines changed

Diff for: 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;

Diff for: packages/react-reconciler/src/ReactFiberBeginWork.js

+93-26
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, bailoutHooks} from './ReactFiberHooks';
9494
import {stopProfilerTimerIfRunning} from './ReactProfilerTimer';
9595
import {
9696
getMaskedContext,
@@ -128,6 +128,8 @@ import {
128128

129129
const ReactCurrentOwner = ReactSharedInternals.ReactCurrentOwner;
130130

131+
let didReceiveUpdate: boolean = false;
132+
131133
let didWarnAboutBadClass;
132134
let didWarnAboutContextTypeOnFunctionComponent;
133135
let didWarnAboutGetDerivedStateOnFunctionComponent;
@@ -237,16 +239,37 @@ function updateForwardRef(
237239
// The rest is a fork of updateFunctionComponent
238240
let nextChildren;
239241
prepareToReadContext(workInProgress, renderExpirationTime);
240-
prepareToUseHooks(current, workInProgress, renderExpirationTime);
241242
if (__DEV__) {
242243
ReactCurrentOwner.current = workInProgress;
243244
setCurrentPhase('render');
244-
nextChildren = render(nextProps, ref);
245+
nextChildren = renderWithHooks(
246+
current,
247+
workInProgress,
248+
render,
249+
nextProps,
250+
ref,
251+
renderExpirationTime,
252+
);
245253
setCurrentPhase(null);
246254
} else {
247-
nextChildren = render(nextProps, ref);
255+
nextChildren = renderWithHooks(
256+
current,
257+
workInProgress,
258+
render,
259+
nextProps,
260+
ref,
261+
renderExpirationTime,
262+
);
263+
}
264+
265+
if (current !== null && !didReceiveUpdate) {
266+
bailoutHooks(current, workInProgress, renderExpirationTime);
267+
return bailoutOnAlreadyFinishedWork(
268+
current,
269+
workInProgress,
270+
renderExpirationTime,
271+
);
248272
}
249-
nextChildren = finishHooks(render, nextProps, nextChildren, ref);
250273

251274
// React DevTools reads this flag.
252275
workInProgress.effectTag |= PerformedWork;
@@ -395,17 +418,20 @@ function updateSimpleMemoComponent(
395418
// Inner propTypes will be validated in the function component path.
396419
}
397420
}
398-
if (current !== null && updateExpirationTime < renderExpirationTime) {
421+
if (current !== null) {
399422
const prevProps = current.memoizedProps;
400423
if (
401424
shallowEqual(prevProps, nextProps) &&
402425
current.ref === workInProgress.ref
403426
) {
404-
return bailoutOnAlreadyFinishedWork(
405-
current,
406-
workInProgress,
407-
renderExpirationTime,
408-
);
427+
didReceiveUpdate = false;
428+
if (updateExpirationTime < renderExpirationTime) {
429+
return bailoutOnAlreadyFinishedWork(
430+
current,
431+
workInProgress,
432+
renderExpirationTime,
433+
);
434+
}
409435
}
410436
}
411437
return updateFunctionComponent(
@@ -506,16 +532,37 @@ function updateFunctionComponent(
506532

507533
let nextChildren;
508534
prepareToReadContext(workInProgress, renderExpirationTime);
509-
prepareToUseHooks(current, workInProgress, renderExpirationTime);
510535
if (__DEV__) {
511536
ReactCurrentOwner.current = workInProgress;
512537
setCurrentPhase('render');
513-
nextChildren = Component(nextProps, context);
538+
nextChildren = renderWithHooks(
539+
current,
540+
workInProgress,
541+
Component,
542+
nextProps,
543+
context,
544+
renderExpirationTime,
545+
);
514546
setCurrentPhase(null);
515547
} else {
516-
nextChildren = Component(nextProps, context);
548+
nextChildren = renderWithHooks(
549+
current,
550+
workInProgress,
551+
Component,
552+
nextProps,
553+
context,
554+
renderExpirationTime,
555+
);
556+
}
557+
558+
if (current !== null && !didReceiveUpdate) {
559+
bailoutHooks(current, workInProgress, renderExpirationTime);
560+
return bailoutOnAlreadyFinishedWork(
561+
current,
562+
workInProgress,
563+
renderExpirationTime,
564+
);
517565
}
518-
nextChildren = finishHooks(Component, nextProps, nextChildren, context);
519566

520567
// React DevTools reads this flag.
521568
workInProgress.effectTag |= PerformedWork;
@@ -850,7 +897,7 @@ function updateHostComponent(current, workInProgress, renderExpirationTime) {
850897
shouldDeprioritizeSubtree(type, nextProps)
851898
) {
852899
// Schedule this fiber to re-render at offscreen priority. Then bailout.
853-
workInProgress.expirationTime = Never;
900+
workInProgress.expirationTime = workInProgress.childExpirationTime = Never;
854901
return null;
855902
}
856903

@@ -1063,7 +1110,6 @@ function mountIndeterminateComponent(
10631110
const context = getMaskedContext(workInProgress, unmaskedContext);
10641111

10651112
prepareToReadContext(workInProgress, renderExpirationTime);
1066-
prepareToUseHooks(null, workInProgress, renderExpirationTime);
10671113

10681114
let value;
10691115

@@ -1091,9 +1137,23 @@ function mountIndeterminateComponent(
10911137
}
10921138

10931139
ReactCurrentOwner.current = workInProgress;
1094-
value = Component(props, context);
1140+
value = renderWithHooks(
1141+
null,
1142+
workInProgress,
1143+
Component,
1144+
props,
1145+
context,
1146+
renderExpirationTime,
1147+
);
10951148
} else {
1096-
value = Component(props, context);
1149+
value = renderWithHooks(
1150+
null,
1151+
workInProgress,
1152+
Component,
1153+
props,
1154+
context,
1155+
renderExpirationTime,
1156+
);
10971157
}
10981158
// React DevTools reads this flag.
10991159
workInProgress.effectTag |= PerformedWork;
@@ -1147,7 +1207,6 @@ function mountIndeterminateComponent(
11471207
} else {
11481208
// Proceed under the assumption that this is a function component
11491209
workInProgress.tag = FunctionComponent;
1150-
value = finishHooks(Component, props, value, context);
11511210
reconcileChildren(null, workInProgress, value, renderExpirationTime);
11521211
if (__DEV__) {
11531212
validateFunctionComponentInDev(workInProgress, Component);
@@ -1638,6 +1697,10 @@ function updateContextConsumer(
16381697
return workInProgress.child;
16391698
}
16401699

1700+
export function markWorkInProgressReceivedUpdate() {
1701+
didReceiveUpdate = true;
1702+
}
1703+
16411704
function bailoutOnAlreadyFinishedWork(
16421705
current: Fiber | null,
16431706
workInProgress: Fiber,
@@ -1647,7 +1710,7 @@ function bailoutOnAlreadyFinishedWork(
16471710

16481711
if (current !== null) {
16491712
// Reuse previous context list
1650-
workInProgress.firstContextDependency = current.firstContextDependency;
1713+
workInProgress.contextDependencies = current.contextDependencies;
16511714
}
16521715

16531716
if (enableProfilerTimer) {
@@ -1680,11 +1743,13 @@ function beginWork(
16801743
if (current !== null) {
16811744
const oldProps = current.memoizedProps;
16821745
const newProps = workInProgress.pendingProps;
1683-
if (
1684-
oldProps === newProps &&
1685-
!hasLegacyContextChanged() &&
1686-
updateExpirationTime < renderExpirationTime
1687-
) {
1746+
1747+
if (oldProps !== newProps || hasLegacyContextChanged()) {
1748+
// If props or context changed, mark the fiber as having performed work.
1749+
// This may be unset if the props are determined to be equal later (memo).
1750+
didReceiveUpdate = true;
1751+
} else if (updateExpirationTime < renderExpirationTime) {
1752+
didReceiveUpdate = false;
16881753
// This fiber does not have any pending work. Bailout without entering
16891754
// the begin phase. There's still some bookkeeping we that needs to be done
16901755
// in this optimized path, mostly pushing stuff onto the stack.
@@ -1767,6 +1832,8 @@ function beginWork(
17671832
renderExpirationTime,
17681833
);
17691834
}
1835+
} else {
1836+
didReceiveUpdate = false;
17701837
}
17711838

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

Diff for: 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)