Skip to content

Commit

Permalink
Allow useReducer to bail out of rendering by returning previous state (
Browse files Browse the repository at this point in the history
…facebook#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)
  • Loading branch information
acdlite authored and jetoneza committed Jan 23, 2019
1 parent 8d00646 commit 56a323a
Show file tree
Hide file tree
Showing 7 changed files with 673 additions and 101 deletions.
10 changes: 5 additions & 5 deletions packages/react-reconciler/src/ReactFiber.js
Expand Up @@ -14,7 +14,7 @@ import type {TypeOfMode} from './ReactTypeOfMode';
import type {SideEffectTag} from 'shared/ReactSideEffectTags';
import type {ExpirationTime} from './ReactFiberExpirationTime';
import type {UpdateQueue} from './ReactUpdateQueue';
import type {ContextDependency} from './ReactFiberNewContext';
import type {ContextDependencyList} from './ReactFiberNewContext';

import invariant from 'shared/invariant';
import warningWithoutStack from 'shared/warningWithoutStack';
Expand Down Expand Up @@ -141,7 +141,7 @@ export type Fiber = {|
memoizedState: any,

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

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

this.mode = mode;

Expand Down Expand Up @@ -403,7 +403,7 @@ export function createWorkInProgress(
workInProgress.memoizedProps = current.memoizedProps;
workInProgress.memoizedState = current.memoizedState;
workInProgress.updateQueue = current.updateQueue;
workInProgress.firstContextDependency = current.firstContextDependency;
workInProgress.contextDependencies = current.contextDependencies;

// These will be overridden during the parent's reconciliation
workInProgress.sibling = current.sibling;
Expand Down Expand Up @@ -704,7 +704,7 @@ export function assignFiberPropertiesInDEV(
target.memoizedProps = source.memoizedProps;
target.updateQueue = source.updateQueue;
target.memoizedState = source.memoizedState;
target.firstContextDependency = source.firstContextDependency;
target.contextDependencies = source.contextDependencies;
target.mode = source.mode;
target.effectTag = source.effectTag;
target.nextEffect = source.nextEffect;
Expand Down
119 changes: 93 additions & 26 deletions packages/react-reconciler/src/ReactFiberBeginWork.js
Expand Up @@ -90,7 +90,7 @@ import {
prepareToReadContext,
calculateChangedBits,
} from './ReactFiberNewContext';
import {prepareToUseHooks, finishHooks, resetHooks} from './ReactFiberHooks';
import {resetHooks, renderWithHooks, bailoutHooks} from './ReactFiberHooks';
import {stopProfilerTimerIfRunning} from './ReactProfilerTimer';
import {
getMaskedContext,
Expand Down Expand Up @@ -128,6 +128,8 @@ import {

const ReactCurrentOwner = ReactSharedInternals.ReactCurrentOwner;

let didReceiveUpdate: boolean = false;

let didWarnAboutBadClass;
let didWarnAboutContextTypeOnFunctionComponent;
let didWarnAboutGetDerivedStateOnFunctionComponent;
Expand Down Expand Up @@ -237,16 +239,37 @@ function updateForwardRef(
// The rest is a fork of updateFunctionComponent
let nextChildren;
prepareToReadContext(workInProgress, renderExpirationTime);
prepareToUseHooks(current, workInProgress, renderExpirationTime);
if (__DEV__) {
ReactCurrentOwner.current = workInProgress;
setCurrentPhase('render');
nextChildren = render(nextProps, ref);
nextChildren = renderWithHooks(
current,
workInProgress,
render,
nextProps,
ref,
renderExpirationTime,
);
setCurrentPhase(null);
} else {
nextChildren = render(nextProps, ref);
nextChildren = renderWithHooks(
current,
workInProgress,
render,
nextProps,
ref,
renderExpirationTime,
);
}

if (current !== null && !didReceiveUpdate) {
bailoutHooks(current, workInProgress, renderExpirationTime);
return bailoutOnAlreadyFinishedWork(
current,
workInProgress,
renderExpirationTime,
);
}
nextChildren = finishHooks(render, nextProps, nextChildren, ref);

// React DevTools reads this flag.
workInProgress.effectTag |= PerformedWork;
Expand Down Expand Up @@ -395,17 +418,20 @@ function updateSimpleMemoComponent(
// Inner propTypes will be validated in the function component path.
}
}
if (current !== null && updateExpirationTime < renderExpirationTime) {
if (current !== null) {
const prevProps = current.memoizedProps;
if (
shallowEqual(prevProps, nextProps) &&
current.ref === workInProgress.ref
) {
return bailoutOnAlreadyFinishedWork(
current,
workInProgress,
renderExpirationTime,
);
didReceiveUpdate = false;
if (updateExpirationTime < renderExpirationTime) {
return bailoutOnAlreadyFinishedWork(
current,
workInProgress,
renderExpirationTime,
);
}
}
}
return updateFunctionComponent(
Expand Down Expand Up @@ -506,16 +532,37 @@ function updateFunctionComponent(

let nextChildren;
prepareToReadContext(workInProgress, renderExpirationTime);
prepareToUseHooks(current, workInProgress, renderExpirationTime);
if (__DEV__) {
ReactCurrentOwner.current = workInProgress;
setCurrentPhase('render');
nextChildren = Component(nextProps, context);
nextChildren = renderWithHooks(
current,
workInProgress,
Component,
nextProps,
context,
renderExpirationTime,
);
setCurrentPhase(null);
} else {
nextChildren = Component(nextProps, context);
nextChildren = renderWithHooks(
current,
workInProgress,
Component,
nextProps,
context,
renderExpirationTime,
);
}

if (current !== null && !didReceiveUpdate) {
bailoutHooks(current, workInProgress, renderExpirationTime);
return bailoutOnAlreadyFinishedWork(
current,
workInProgress,
renderExpirationTime,
);
}
nextChildren = finishHooks(Component, nextProps, nextChildren, context);

// React DevTools reads this flag.
workInProgress.effectTag |= PerformedWork;
Expand Down Expand Up @@ -850,7 +897,7 @@ function updateHostComponent(current, workInProgress, renderExpirationTime) {
shouldDeprioritizeSubtree(type, nextProps)
) {
// Schedule this fiber to re-render at offscreen priority. Then bailout.
workInProgress.expirationTime = Never;
workInProgress.expirationTime = workInProgress.childExpirationTime = Never;
return null;
}

Expand Down Expand Up @@ -1063,7 +1110,6 @@ function mountIndeterminateComponent(
const context = getMaskedContext(workInProgress, unmaskedContext);

prepareToReadContext(workInProgress, renderExpirationTime);
prepareToUseHooks(null, workInProgress, renderExpirationTime);

let value;

Expand Down Expand Up @@ -1091,9 +1137,23 @@ function mountIndeterminateComponent(
}

ReactCurrentOwner.current = workInProgress;
value = Component(props, context);
value = renderWithHooks(
null,
workInProgress,
Component,
props,
context,
renderExpirationTime,
);
} else {
value = Component(props, context);
value = renderWithHooks(
null,
workInProgress,
Component,
props,
context,
renderExpirationTime,
);
}
// React DevTools reads this flag.
workInProgress.effectTag |= PerformedWork;
Expand Down Expand Up @@ -1147,7 +1207,6 @@ function mountIndeterminateComponent(
} else {
// Proceed under the assumption that this is a function component
workInProgress.tag = FunctionComponent;
value = finishHooks(Component, props, value, context);
reconcileChildren(null, workInProgress, value, renderExpirationTime);
if (__DEV__) {
validateFunctionComponentInDev(workInProgress, Component);
Expand Down Expand Up @@ -1638,6 +1697,10 @@ function updateContextConsumer(
return workInProgress.child;
}

export function markWorkInProgressReceivedUpdate() {
didReceiveUpdate = true;
}

function bailoutOnAlreadyFinishedWork(
current: Fiber | null,
workInProgress: Fiber,
Expand All @@ -1647,7 +1710,7 @@ function bailoutOnAlreadyFinishedWork(

if (current !== null) {
// Reuse previous context list
workInProgress.firstContextDependency = current.firstContextDependency;
workInProgress.contextDependencies = current.contextDependencies;
}

if (enableProfilerTimer) {
Expand Down Expand Up @@ -1680,11 +1743,13 @@ function beginWork(
if (current !== null) {
const oldProps = current.memoizedProps;
const newProps = workInProgress.pendingProps;
if (
oldProps === newProps &&
!hasLegacyContextChanged() &&
updateExpirationTime < renderExpirationTime
) {

if (oldProps !== newProps || hasLegacyContextChanged()) {
// If props or context changed, mark the fiber as having performed work.
// This may be unset if the props are determined to be equal later (memo).
didReceiveUpdate = true;
} else if (updateExpirationTime < renderExpirationTime) {
didReceiveUpdate = false;
// This fiber does not have any pending work. Bailout without entering
// the begin phase. There's still some bookkeeping we that needs to be done
// in this optimized path, mostly pushing stuff onto the stack.
Expand Down Expand Up @@ -1767,6 +1832,8 @@ function beginWork(
renderExpirationTime,
);
}
} else {
didReceiveUpdate = false;
}

// Before entering the begin phase, clear the expiration time.
Expand Down
17 changes: 4 additions & 13 deletions packages/react-reconciler/src/ReactFiberCompleteWork.js
Expand Up @@ -82,7 +82,6 @@ import {
prepareToHydrateHostTextInstance,
popHydrationState,
} from './ReactFiberHydrationContext';
import {ConcurrentMode, NoContext} from './ReactTypeOfMode';

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

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

0 comments on commit 56a323a

Please sign in to comment.