Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
* pure

A higher-order component version of the `React.PureComponent` class.
During an update, the previous props are compared to the new props. If
they are the same, React will skip rendering the component and
its children.

Unlike userspace implementations, `pure` will not add an additional
fiber to the tree.

The first argument must be a functional component; it does not work
with classes.

`pure` uses shallow comparison by default, like `React.PureComponent`.
A custom comparison can be passed as the second argument.

Co-authored-by: Andrew Clark <acdlite@fb.com>
Co-authored-by: Sophie Alpert <sophiebits@fb.com>

* Warn if first argument is not a functional component
  • Loading branch information
acdlite authored and jetoneza committed Jan 23, 2019
1 parent db0c798 commit e9b525f
Show file tree
Hide file tree
Showing 10 changed files with 433 additions and 70 deletions.
29 changes: 15 additions & 14 deletions packages/react-reconciler/src/ReactFiber.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ import {
FunctionalComponentLazy,
ClassComponentLazy,
ForwardRefLazy,
PureComponent,
PureComponentLazy,
} from 'shared/ReactWorkTags';
import getComponentName from 'shared/getComponentName';

Expand All @@ -57,6 +59,7 @@ import {
REACT_CONTEXT_TYPE,
REACT_CONCURRENT_MODE_TYPE,
REACT_PLACEHOLDER_TYPE,
REACT_PURE_TYPE,
} from 'shared/ReactSymbols';

let hasBadMapPolyfill;
Expand Down Expand Up @@ -300,12 +303,14 @@ export function resolveLazyComponentTag(
return shouldConstruct(Component)
? ClassComponentLazy
: FunctionalComponentLazy;
} else if (
Component !== undefined &&
Component !== null &&
Component.$$typeof
) {
return ForwardRefLazy;
} else if (Component !== undefined && Component !== null) {
const $$typeof = Component.$$typeof;
if ($$typeof === REACT_FORWARD_REF_TYPE) {
return ForwardRefLazy;
}
if ($$typeof === REACT_PURE_TYPE) {
return PureComponentLazy;
}
}
return IndeterminateComponent;
}
Expand Down Expand Up @@ -363,15 +368,8 @@ export function createWorkInProgress(
}
}

// Don't touching the subtree's expiration time, which has not changed.
workInProgress.childExpirationTime = current.childExpirationTime;
if (pendingProps !== current.pendingProps) {
// This fiber has new props.
workInProgress.expirationTime = expirationTime;
} else {
// This fiber's props have not changed.
workInProgress.expirationTime = current.expirationTime;
}
workInProgress.expirationTime = current.expirationTime;

workInProgress.child = current.child;
workInProgress.memoizedProps = current.memoizedProps;
Expand Down Expand Up @@ -460,6 +458,9 @@ export function createFiberFromElement(
case REACT_FORWARD_REF_TYPE:
fiberTag = ForwardRef;
break getTag;
case REACT_PURE_TYPE:
fiberTag = PureComponent;
break getTag;
default: {
if (typeof type.then === 'function') {
fiberTag = IndeterminateComponent;
Expand Down
218 changes: 163 additions & 55 deletions packages/react-reconciler/src/ReactFiberBeginWork.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ import {
ContextConsumer,
Profiler,
PlaceholderComponent,
PureComponent,
PureComponentLazy,
} from 'shared/ReactWorkTags';
import {
NoEffect,
Expand All @@ -52,6 +54,7 @@ import {
enableSchedulerTracing,
} from 'shared/ReactFeatureFlags';
import invariant from 'shared/invariant';
import shallowEqual from 'shared/shallowEqual';
import getComponentName from 'shared/getComponentName';
import ReactStrictModeWarnings from './ReactStrictModeWarnings';
import warning from 'shared/warning';
Expand Down Expand Up @@ -196,6 +199,58 @@ function updateForwardRef(
return workInProgress.child;
}

function updatePureComponent(
current: Fiber | null,
workInProgress: Fiber,
Component: any,
nextProps: any,
updateExpirationTime,
renderExpirationTime: ExpirationTime,
) {
const render = Component.render;

if (
current !== null &&
(updateExpirationTime === NoWork ||
updateExpirationTime > renderExpirationTime)
) {
const prevProps = current.memoizedProps;
// Default to shallow comparison
let compare = Component.compare;
compare = compare !== null ? compare : shallowEqual;
if (compare(prevProps, nextProps)) {
return bailoutOnAlreadyFinishedWork(
current,
workInProgress,
renderExpirationTime,
);
}
}

// The rest is a fork of updateFunctionalComponent
let nextChildren;
prepareToReadContext(workInProgress, renderExpirationTime);
if (__DEV__) {
ReactCurrentOwner.current = workInProgress;
ReactCurrentFiber.setCurrentPhase('render');
nextChildren = render(nextProps);
ReactCurrentFiber.setCurrentPhase(null);
} else {
nextChildren = render(nextProps);
}

// React DevTools reads this flag.
workInProgress.effectTag |= PerformedWork;
reconcileChildren(
current,
workInProgress,
nextChildren,
renderExpirationTime,
);
memoizeProps(workInProgress, nextProps);
return workInProgress.child;
}

function updateFragment(
current: Fiber | null,
workInProgress: Fiber,
Expand Down Expand Up @@ -617,6 +672,7 @@ function mountIndeterminateComponent(
current,
workInProgress,
Component,
updateExpirationTime,
renderExpirationTime,
) {
invariant(
Expand All @@ -637,37 +693,53 @@ function mountIndeterminateComponent(
Component,
));
const resolvedProps = resolveDefaultProps(Component, props);
let child;
switch (resolvedTag) {
case FunctionalComponentLazy: {
return updateFunctionalComponent(
child = updateFunctionalComponent(
current,
workInProgress,
Component,
resolvedProps,
renderExpirationTime,
);
break;
}
case ClassComponentLazy: {
return updateClassComponent(
child = updateClassComponent(
current,
workInProgress,
Component,
resolvedProps,
renderExpirationTime,
);
break;
}
case ForwardRefLazy: {
return updateForwardRef(
child = updateForwardRef(
current,
workInProgress,
Component,
resolvedProps,
renderExpirationTime,
);
break;
}
case PureComponentLazy: {
child = updatePureComponent(
current,
workInProgress,
Component,
resolvedProps,
updateExpirationTime,
renderExpirationTime,
);
break;
}
default: {
// This message intentionally doesn't metion ForwardRef because the
// fact that it's a separate type of work is an implementation detail.
// This message intentionally doesn't metion ForwardRef or PureComponent
// because the fact that it's a separate type of work is an
// implementation detail.
invariant(
false,
'Element type is invalid. Received a promise that resolves to: %s. ' +
Expand All @@ -676,6 +748,8 @@ function mountIndeterminateComponent(
);
}
}
workInProgress.memoizedProps = props;
return child;
}

const unmaskedContext = getUnmaskedContext(workInProgress, Component, false);
Expand Down Expand Up @@ -1106,59 +1180,65 @@ function beginWork(
renderExpirationTime: ExpirationTime,
): Fiber | null {
const updateExpirationTime = workInProgress.expirationTime;
if (
!hasLegacyContextChanged() &&
(updateExpirationTime === NoWork ||
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.
switch (workInProgress.tag) {
case HostRoot:
pushHostRootContext(workInProgress);
resetHydrationState();
break;
case HostComponent:
pushHostContext(workInProgress);
break;
case ClassComponent: {
const Component = workInProgress.type;
if (isLegacyContextProvider(Component)) {
pushLegacyContextProvider(workInProgress);

if (current !== null) {
const oldProps = current.memoizedProps;
const newProps = workInProgress.pendingProps;
if (
oldProps === newProps &&
!hasLegacyContextChanged() &&
(updateExpirationTime === NoWork ||
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.
switch (workInProgress.tag) {
case HostRoot:
pushHostRootContext(workInProgress);
resetHydrationState();
break;
case HostComponent:
pushHostContext(workInProgress);
break;
case ClassComponent: {
const Component = workInProgress.type;
if (isLegacyContextProvider(Component)) {
pushLegacyContextProvider(workInProgress);
}
break;
}
break;
}
case ClassComponentLazy: {
const thenable = workInProgress.type;
const Component = getResultFromResolvedThenable(thenable);
if (isLegacyContextProvider(Component)) {
pushLegacyContextProvider(workInProgress);
case ClassComponentLazy: {
const thenable = workInProgress.type;
const Component = getResultFromResolvedThenable(thenable);
if (isLegacyContextProvider(Component)) {
pushLegacyContextProvider(workInProgress);
}
break;
}
break;
}
case HostPortal:
pushHostContainer(
workInProgress,
workInProgress.stateNode.containerInfo,
);
break;
case ContextProvider: {
const newValue = workInProgress.memoizedProps.value;
pushProvider(workInProgress, newValue);
break;
}
case Profiler:
if (enableProfilerTimer) {
workInProgress.effectTag |= Update;
case HostPortal:
pushHostContainer(
workInProgress,
workInProgress.stateNode.containerInfo,
);
break;
case ContextProvider: {
const newValue = workInProgress.memoizedProps.value;
pushProvider(workInProgress, newValue);
break;
}
break;
case Profiler:
if (enableProfilerTimer) {
workInProgress.effectTag |= Update;
}
break;
}
return bailoutOnAlreadyFinishedWork(
current,
workInProgress,
renderExpirationTime,
);
}
return bailoutOnAlreadyFinishedWork(
current,
workInProgress,
renderExpirationTime,
);
}

// Before entering the begin phase, clear the expiration time.
Expand All @@ -1171,6 +1251,7 @@ function beginWork(
current,
workInProgress,
Component,
updateExpirationTime,
renderExpirationTime,
);
}
Expand Down Expand Up @@ -1252,7 +1333,7 @@ function beginWork(
renderExpirationTime,
);
}
case ForwardRefLazy:
case ForwardRefLazy: {
const thenable = workInProgress.type;
const Component = getResultFromResolvedThenable(thenable);
const unresolvedProps = workInProgress.pendingProps;
Expand All @@ -1265,6 +1346,7 @@ function beginWork(
);
workInProgress.memoizedProps = unresolvedProps;
return child;
}
case Fragment:
return updateFragment(current, workInProgress, renderExpirationTime);
case Mode:
Expand All @@ -1283,6 +1365,32 @@ function beginWork(
workInProgress,
renderExpirationTime,
);
case PureComponent: {
const type = workInProgress.type;
return updatePureComponent(
current,
workInProgress,
type,
workInProgress.pendingProps,
updateExpirationTime,
renderExpirationTime,
);
}
case PureComponentLazy: {
const thenable = workInProgress.type;
const Component = getResultFromResolvedThenable(thenable);
const unresolvedProps = workInProgress.pendingProps;
const child = updatePureComponent(
current,
workInProgress,
Component,
resolveDefaultProps(Component, unresolvedProps),
updateExpirationTime,
renderExpirationTime,
);
workInProgress.memoizedProps = unresolvedProps;
return child;
}
default:
invariant(
false,
Expand Down

0 comments on commit e9b525f

Please sign in to comment.