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 11, 2019
1 parent f290138 commit f3804aa
Show file tree
Hide file tree
Showing 8 changed files with 578 additions and 98 deletions.
114 changes: 84 additions & 30 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 @@ -125,6 +125,7 @@ import {
createWorkInProgress,
isSimpleFunctionComponent,
} from './ReactFiber';
import maxSigned31BitInt from './maxSigned31BitInt';

const ReactCurrentOwner = ReactSharedInternals.ReactCurrentOwner;

Expand Down Expand Up @@ -237,19 +238,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 +369,6 @@ function updateMemoComponent(
);
}
}
// React DevTools reads this flag.
workInProgress.effectTag |= PerformedWork;
let newChild = createWorkInProgress(
currentChild,
nextProps,
Expand Down Expand Up @@ -506,19 +523,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 +1098,6 @@ function mountIndeterminateComponent(
const context = getMaskedContext(workInProgress, unmaskedContext);

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

let value;

Expand Down Expand Up @@ -1091,12 +1125,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 +1193,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 @@ -1539,17 +1584,17 @@ function updateContextProvider(
}
}

pushProvider(workInProgress, newValue);

let changedBits;
if (oldProps !== null) {
const oldValue = oldProps.value;
const changedBits = calculateChangedBits(context, newValue, oldValue);
changedBits = calculateChangedBits(context, newValue, oldValue);
if (changedBits === 0) {
// No change. Bailout early if children are the same.
if (
oldProps.children === newProps.children &&
!hasLegacyContextChanged()
) {
pushProvider(workInProgress, newValue, 0);
return bailoutOnAlreadyFinishedWork(
current,
workInProgress,
Expand All @@ -1566,7 +1611,10 @@ function updateContextProvider(
renderExpirationTime,
);
}
} else {
changedBits = maxSigned31BitInt;
}
pushProvider(workInProgress, newValue, changedBits);

const newChildren = newProps.children;
reconcileChildren(current, workInProgress, newChildren, renderExpirationTime);
Expand Down Expand Up @@ -1650,6 +1698,8 @@ function bailoutOnAlreadyFinishedWork(
workInProgress.firstContextDependency = current.firstContextDependency;
}

workInProgress.effectTag &= ~PerformedWork;

if (enableProfilerTimer) {
// Don't update "base" render times for bailouts.
stopProfilerTimerIfRunning(workInProgress);
Expand Down Expand Up @@ -1680,11 +1730,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 All @@ -1711,7 +1762,7 @@ function beginWork(
break;
case ContextProvider: {
const newValue = workInProgress.memoizedProps.value;
pushProvider(workInProgress, newValue);
pushProvider(workInProgress, newValue, 0);
break;
}
case Profiler:
Expand Down Expand Up @@ -1767,6 +1818,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 f3804aa

Please sign in to comment.