diff --git a/packages/react-reconciler/src/ReactFiber.js b/packages/react-reconciler/src/ReactFiber.js
index 019436c42fc4c..6038faad3ad0c 100644
--- a/packages/react-reconciler/src/ReactFiber.js
+++ b/packages/react-reconciler/src/ReactFiber.js
@@ -37,6 +37,8 @@ import {
FunctionalComponentLazy,
ClassComponentLazy,
ForwardRefLazy,
+ PureComponent,
+ PureComponentLazy,
} from 'shared/ReactWorkTags';
import getComponentName from 'shared/getComponentName';
@@ -57,6 +59,7 @@ import {
REACT_CONTEXT_TYPE,
REACT_CONCURRENT_MODE_TYPE,
REACT_PLACEHOLDER_TYPE,
+ REACT_PURE_TYPE,
} from 'shared/ReactSymbols';
let hasBadMapPolyfill;
@@ -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;
}
@@ -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;
@@ -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;
diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.js b/packages/react-reconciler/src/ReactFiberBeginWork.js
index 48f680cad3a3e..d66132574649c 100644
--- a/packages/react-reconciler/src/ReactFiberBeginWork.js
+++ b/packages/react-reconciler/src/ReactFiberBeginWork.js
@@ -31,6 +31,8 @@ import {
ContextConsumer,
Profiler,
PlaceholderComponent,
+ PureComponent,
+ PureComponentLazy,
} from 'shared/ReactWorkTags';
import {
NoEffect,
@@ -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';
@@ -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,
@@ -617,6 +672,7 @@ function mountIndeterminateComponent(
current,
workInProgress,
Component,
+ updateExpirationTime,
renderExpirationTime,
) {
invariant(
@@ -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. ' +
@@ -676,6 +748,8 @@ function mountIndeterminateComponent(
);
}
}
+ workInProgress.memoizedProps = props;
+ return child;
}
const unmaskedContext = getUnmaskedContext(workInProgress, Component, false);
@@ -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.
@@ -1171,6 +1251,7 @@ function beginWork(
current,
workInProgress,
Component,
+ updateExpirationTime,
renderExpirationTime,
);
}
@@ -1252,7 +1333,7 @@ function beginWork(
renderExpirationTime,
);
}
- case ForwardRefLazy:
+ case ForwardRefLazy: {
const thenable = workInProgress.type;
const Component = getResultFromResolvedThenable(thenable);
const unresolvedProps = workInProgress.pendingProps;
@@ -1265,6 +1346,7 @@ function beginWork(
);
workInProgress.memoizedProps = unresolvedProps;
return child;
+ }
case Fragment:
return updateFragment(current, workInProgress, renderExpirationTime);
case Mode:
@@ -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,
diff --git a/packages/react-reconciler/src/ReactFiberCompleteWork.js b/packages/react-reconciler/src/ReactFiberCompleteWork.js
index cdcf541db39fb..4be43bf09656e 100644
--- a/packages/react-reconciler/src/ReactFiberCompleteWork.js
+++ b/packages/react-reconciler/src/ReactFiberCompleteWork.js
@@ -36,6 +36,8 @@ import {
Profiler,
PlaceholderComponent,
ForwardRefLazy,
+ PureComponent,
+ PureComponentLazy,
} from 'shared/ReactWorkTags';
import {Placement, Ref, Update} from 'shared/ReactSideEffectTags';
import invariant from 'shared/invariant';
@@ -524,6 +526,9 @@ function completeWork(
break;
case ContextConsumer:
break;
+ case PureComponent:
+ case PureComponentLazy:
+ break;
// Error cases
case IndeterminateComponent:
invariant(
diff --git a/packages/react-reconciler/src/__tests__/ReactPure-test.internal.js b/packages/react-reconciler/src/__tests__/ReactPure-test.internal.js
new file mode 100644
index 0000000000000..2813e33f9d95c
--- /dev/null
+++ b/packages/react-reconciler/src/__tests__/ReactPure-test.internal.js
@@ -0,0 +1,194 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ *
+ * @emails react-core
+ * @jest-environment node
+ */
+
+/* eslint-disable no-func-assign */
+
+'use strict';
+
+let React;
+let ReactFeatureFlags;
+let ReactNoop;
+
+describe('pure', () => {
+ beforeEach(() => {
+ jest.resetModules();
+ ReactFeatureFlags = require('shared/ReactFeatureFlags');
+ ReactFeatureFlags.debugRenderPhaseSideEffectsForStrictMode = false;
+ ReactFeatureFlags.enableSuspense = true;
+ React = require('react');
+ ReactNoop = require('react-noop-renderer');
+ });
+
+ function span(prop) {
+ return {type: 'span', children: [], prop};
+ }
+
+ function Text(props) {
+ ReactNoop.yield(props.text);
+ return ;
+ }
+
+ // Tests should run against both the lazy and non-lazy versions of `pure`.
+ // To make the tests work for both versions, we wrap the non-lazy verion in
+ // a lazy function component.
+ sharedTests('normal', (...args) => {
+ const Pure = React.pure(...args);
+ function Indirection(props) {
+ return ;
+ }
+ return Promise.resolve(Indirection);
+ });
+ sharedTests('lazy', (...args) => Promise.resolve(React.pure(...args)));
+
+ function sharedTests(label, pure) {
+ describe(`${label}`, () => {
+ it('bails out on props equality', async () => {
+ const {Placeholder} = React;
+
+ function Counter({count}) {
+ return ;
+ }
+ Counter = pure(Counter);
+
+ ReactNoop.render(
+
+
+ ,
+ );
+ expect(ReactNoop.flush()).toEqual([]);
+ await Promise.resolve();
+ expect(ReactNoop.flush()).toEqual([0]);
+ expect(ReactNoop.getChildren()).toEqual([span(0)]);
+
+ // Should bail out because props have not changed
+ ReactNoop.render(
+
+
+ ,
+ );
+ expect(ReactNoop.flush()).toEqual([]);
+ expect(ReactNoop.getChildren()).toEqual([span(0)]);
+
+ // Should update because count prop changed
+ ReactNoop.render(
+
+
+ ,
+ );
+ expect(ReactNoop.flush()).toEqual([1]);
+ expect(ReactNoop.getChildren()).toEqual([span(1)]);
+ });
+ });
+
+ it("does not bail out if there's a context change", async () => {
+ const {Placeholder} = React;
+
+ const CountContext = React.createContext(0);
+
+ function Counter(props) {
+ const count = CountContext.unstable_read();
+ return ;
+ }
+ Counter = pure(Counter);
+
+ class Parent extends React.Component {
+ state = {count: 0};
+ render() {
+ return (
+
+
+
+
+
+ );
+ }
+ }
+
+ const parent = React.createRef(null);
+ ReactNoop.render();
+ expect(ReactNoop.flush()).toEqual([]);
+ await Promise.resolve();
+ expect(ReactNoop.flush()).toEqual(['Count: 0']);
+ expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]);
+
+ // Should bail out because props have not changed
+ ReactNoop.render();
+ expect(ReactNoop.flush()).toEqual([]);
+ expect(ReactNoop.getChildren()).toEqual([span('Count: 0')]);
+
+ // Should update because there was a context change
+ parent.current.setState({count: 1});
+ expect(ReactNoop.flush()).toEqual(['Count: 1']);
+ expect(ReactNoop.getChildren()).toEqual([span('Count: 1')]);
+ });
+
+ it('accepts custom comparison function', async () => {
+ const {Placeholder} = React;
+
+ function Counter({count}) {
+ return ;
+ }
+ Counter = pure(Counter, (oldProps, newProps) => {
+ ReactNoop.yield(
+ `Old count: ${oldProps.count}, New count: ${newProps.count}`,
+ );
+ return oldProps.count === newProps.count;
+ });
+
+ ReactNoop.render(
+
+
+ ,
+ );
+ expect(ReactNoop.flush()).toEqual([]);
+ await Promise.resolve();
+ expect(ReactNoop.flush()).toEqual([0]);
+ expect(ReactNoop.getChildren()).toEqual([span(0)]);
+
+ // Should bail out because props have not changed
+ ReactNoop.render(
+
+
+ ,
+ );
+ expect(ReactNoop.flush()).toEqual(['Old count: 0, New count: 0']);
+ expect(ReactNoop.getChildren()).toEqual([span(0)]);
+
+ // Should update because count prop changed
+ ReactNoop.render(
+
+
+ ,
+ );
+ expect(ReactNoop.flush()).toEqual(['Old count: 0, New count: 1', 1]);
+ expect(ReactNoop.getChildren()).toEqual([span(1)]);
+ });
+
+ it('warns for class components', () => {
+ class SomeClass extends React.Component {
+ render() {
+ return null;
+ }
+ }
+ expect(() => pure(SomeClass)).toWarnDev(
+ 'pure: The first argument must be a functional component.',
+ {withoutStack: true},
+ );
+ });
+
+ it('warns if first argument is not a function', () => {
+ expect(() => pure()).toWarnDev(
+ 'pure: The first argument must be a functional component. Instead ' +
+ 'received: undefined',
+ {withoutStack: true},
+ );
+ });
+ }
+});
diff --git a/packages/react-test-renderer/src/ReactTestRenderer.js b/packages/react-test-renderer/src/ReactTestRenderer.js
index 10b664651a017..69782f10c51ed 100644
--- a/packages/react-test-renderer/src/ReactTestRenderer.js
+++ b/packages/react-test-renderer/src/ReactTestRenderer.js
@@ -30,6 +30,8 @@ import {
ForwardRef,
Profiler,
ForwardRefLazy,
+ PureComponent,
+ PureComponentLazy,
} from 'shared/ReactWorkTags';
import invariant from 'shared/invariant';
import ReactVersion from 'shared/ReactVersion';
@@ -199,6 +201,8 @@ function toTree(node: ?Fiber) {
case Profiler:
case ForwardRef:
case ForwardRefLazy:
+ case PureComponent:
+ case PureComponentLazy:
return childrenToTree(node.child);
default:
invariant(
@@ -217,6 +221,8 @@ const validWrapperTypes = new Set([
HostComponent,
ForwardRef,
ForwardRefLazy,
+ PureComponent,
+ PureComponentLazy,
// Normally skipped, but used when there's more than one root child.
HostRoot,
]);
diff --git a/packages/react/src/React.js b/packages/react/src/React.js
index 27a5638a0845a..c85cb29cc8170 100644
--- a/packages/react/src/React.js
+++ b/packages/react/src/React.js
@@ -27,6 +27,7 @@ import {
import {createContext} from './ReactContext';
import {lazy} from './ReactLazy';
import forwardRef from './forwardRef';
+import pure from './pure';
import {
createElementWithValidation,
createFactoryWithValidation,
@@ -49,6 +50,7 @@ const React = {
createContext,
forwardRef,
+ pure,
Fragment: REACT_FRAGMENT_TYPE,
StrictMode: REACT_STRICT_MODE_TYPE,
diff --git a/packages/react/src/pure.js b/packages/react/src/pure.js
new file mode 100644
index 0000000000000..1d2aa6952c2e6
--- /dev/null
+++ b/packages/react/src/pure.js
@@ -0,0 +1,40 @@
+/**
+ * Copyright (c) Facebook, Inc. and its affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
+import {REACT_PURE_TYPE} from 'shared/ReactSymbols';
+
+import warningWithoutStack from 'shared/warningWithoutStack';
+
+export default function pure(
+ render: (props: Props) => React$Node,
+ compare?: (oldProps: Props, newProps: Props) => boolean,
+) {
+ if (__DEV__) {
+ if (typeof render !== 'function') {
+ warningWithoutStack(
+ false,
+ 'pure: The first argument must be a functional component. Instead ' +
+ 'received: %s',
+ render === null ? 'null' : typeof render,
+ );
+ } else {
+ const prototype = render.prototype;
+ if (prototype && prototype.isReactComponent) {
+ warningWithoutStack(
+ false,
+ 'pure: The first argument must be a functional component. Classes ' +
+ 'are not supported. Use React.PureComponent instead.',
+ );
+ }
+ }
+ }
+ return {
+ $$typeof: REACT_PURE_TYPE,
+ render,
+ compare: compare === undefined ? null : compare,
+ };
+}
diff --git a/packages/shared/ReactSymbols.js b/packages/shared/ReactSymbols.js
index a13a5fc792eb8..68f7e10a52de5 100644
--- a/packages/shared/ReactSymbols.js
+++ b/packages/shared/ReactSymbols.js
@@ -41,6 +41,7 @@ export const REACT_FORWARD_REF_TYPE = hasSymbol
export const REACT_PLACEHOLDER_TYPE = hasSymbol
? Symbol.for('react.placeholder')
: 0xead1;
+export const REACT_PURE_TYPE = hasSymbol ? Symbol.for('react.pure') : 0xead3;
const MAYBE_ITERATOR_SYMBOL = typeof Symbol === 'function' && Symbol.iterator;
const FAUX_ITERATOR_SYMBOL = '@@iterator';
diff --git a/packages/shared/ReactWorkTags.js b/packages/shared/ReactWorkTags.js
index 1a639d78338ec..78c22567c95ff 100644
--- a/packages/shared/ReactWorkTags.js
+++ b/packages/shared/ReactWorkTags.js
@@ -24,7 +24,9 @@ export type WorkTag =
| 13
| 14
| 15
- | 16;
+ | 16
+ | 17
+ | 18;
export const FunctionalComponent = 0;
export const FunctionalComponentLazy = 1;
@@ -43,3 +45,5 @@ export const ForwardRef = 13;
export const ForwardRefLazy = 14;
export const Profiler = 15;
export const PlaceholderComponent = 16;
+export const PureComponent = 17;
+export const PureComponentLazy = 18;
diff --git a/packages/shared/isValidElementType.js b/packages/shared/isValidElementType.js
index 22fab9d39aae3..6997c275f41f0 100644
--- a/packages/shared/isValidElementType.js
+++ b/packages/shared/isValidElementType.js
@@ -16,6 +16,7 @@ import {
REACT_PROVIDER_TYPE,
REACT_STRICT_MODE_TYPE,
REACT_PLACEHOLDER_TYPE,
+ REACT_PURE_TYPE,
} from 'shared/ReactSymbols';
export default function isValidElementType(type: mixed) {
@@ -31,6 +32,7 @@ export default function isValidElementType(type: mixed) {
(typeof type === 'object' &&
type !== null &&
(typeof type.then === 'function' ||
+ type.$$typeof === REACT_PURE_TYPE ||
type.$$typeof === REACT_PROVIDER_TYPE ||
type.$$typeof === REACT_CONTEXT_TYPE ||
type.$$typeof === REACT_FORWARD_REF_TYPE))