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
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.
  • Loading branch information
acdlite committed Jan 14, 2019
1 parent 3e15b1c commit f13dccf
Show file tree
Hide file tree
Showing 6 changed files with 562 additions and 102 deletions.
10 changes: 5 additions & 5 deletions packages/react-reconciler/src/ReactFiber.js
Original file line number Diff line number Diff line change
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
104 changes: 77 additions & 27 deletions packages/react-reconciler/src/ReactFiberBeginWork.js
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ import {
prepareToReadContext,
calculateChangedBits,
} from './ReactFiberNewContext';
import {prepareToUseHooks, finishHooks, resetHooks} from './ReactFiberHooks';
import {resetHooks, renderWithHooks} from './ReactFiberHooks';
import {stopProfilerTimerIfRunning} from './ReactProfilerTimer';
import {
getMaskedContext,
Expand Down Expand Up @@ -237,19 +237,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 ((workInProgress.effectTag & PerformedWork) === NoEffect) {
return bailoutOnAlreadyFinishedWork(
current,
workInProgress,
renderExpirationTime,
);
}
nextChildren = finishHooks(render, nextProps, nextChildren, ref);

// React DevTools reads this flag.
workInProgress.effectTag |= PerformedWork;
reconcileChildren(
current,
workInProgress,
Expand Down Expand Up @@ -350,8 +368,6 @@ function updateMemoComponent(
);
}
}
// React DevTools reads this flag.
workInProgress.effectTag |= PerformedWork;
let newChild = createWorkInProgress(
currentChild,
nextProps,
Expand Down Expand Up @@ -506,19 +522,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 ((workInProgress.effectTag & PerformedWork) === NoEffect) {
return bailoutOnAlreadyFinishedWork(
current,
workInProgress,
renderExpirationTime,
);
}
nextChildren = finishHooks(Component, nextProps, nextChildren, context);

// React DevTools reads this flag.
workInProgress.effectTag |= PerformedWork;
reconcileChildren(
current,
workInProgress,
Expand Down Expand Up @@ -1063,7 +1097,6 @@ function mountIndeterminateComponent(
const context = getMaskedContext(workInProgress, unmaskedContext);

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

let value;

Expand Down Expand Up @@ -1091,12 +1124,24 @@ 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;

if (
typeof value === 'object' &&
Expand Down Expand Up @@ -1147,7 +1192,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 @@ -1647,9 +1691,11 @@ function bailoutOnAlreadyFinishedWork(

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

workInProgress.effectTag &= ~PerformedWork;

if (enableProfilerTimer) {
// Don't update "base" render times for bailouts.
stopProfilerTimerIfRunning(workInProgress);
Expand Down Expand Up @@ -1680,11 +1726,12 @@ 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).
workInProgress.effectTag |= PerformedWork;
} else if (updateExpirationTime < renderExpirationTime) {
// 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 +1814,9 @@ function beginWork(
renderExpirationTime,
);
}
} else {
// No bailouts on initial mount.
workInProgress.effectTag |= PerformedWork;
}

// Before entering the begin phase, clear the expiration time.
Expand Down
17 changes: 4 additions & 13 deletions packages/react-reconciler/src/ReactFiberCompleteWork.js
Original file line number Diff line number Diff line change
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
Loading

0 comments on commit f13dccf

Please sign in to comment.