Skip to content

Commit

Permalink
Allow arbitrary types to be wrapped in pure (#13903)
Browse files Browse the repository at this point in the history
* Allow arbitrary types to be wrapped in pure

This creates an outer fiber that container the pure check and an inner
fiber that represents which ever type of component.

* Add optimized fast path for simple pure function components

Special cased when there are no defaultProps and it's a simple function
component instead of class. This doesn't require an extra fiber.

We could make it so that this also works with custom comparer but that
means we have to go through one extra indirection to get to it.
Maybe it's worth it, donno.
  • Loading branch information
sebmarkbage committed Oct 20, 2018
1 parent e770af7 commit 15b11d2
Show file tree
Hide file tree
Showing 10 changed files with 294 additions and 74 deletions.
42 changes: 26 additions & 16 deletions packages/react-reconciler/src/ReactFiber.js
Original file line number Diff line number Diff line change
Expand Up @@ -299,10 +299,15 @@ function shouldConstruct(Component: Function) {
return !!(prototype && prototype.isReactComponent);
}

export function resolveLazyComponentTag(
fiber: Fiber,
Component: Function,
): WorkTag {
export function isSimpleFunctionComponent(type: any) {
return (
typeof type === 'function' &&
!shouldConstruct(type) &&
type.defaultProps === undefined
);
}

export function resolveLazyComponentTag(Component: Function): WorkTag {
if (typeof Component === 'function') {
return shouldConstruct(Component) ? ClassComponent : FunctionComponent;
} else if (Component !== undefined && Component !== null) {
Expand Down Expand Up @@ -406,20 +411,15 @@ export function createHostRootFiber(isConcurrent: boolean): Fiber {
return createFiber(HostRoot, null, null, mode);
}

function createFiberFromElementWithoutDebugInfo(
element: ReactElement,
export function createFiberFromTypeAndProps(
type: any, // React$ElementType
key: null | string,
pendingProps: any,
owner: null | Fiber,
mode: TypeOfMode,
expirationTime: ExpirationTime,
): Fiber {
let owner = null;
if (__DEV__) {
owner = element._owner;
}

let fiber;
const type = element.type;
const key = element.key;
const pendingProps = element.props;

let fiberTag = IndeterminateComponent;
// The resolved type is set if we know what the final type will be. I.e. it's not lazy.
Expand Down Expand Up @@ -522,8 +522,18 @@ export function createFiberFromElement(
mode: TypeOfMode,
expirationTime: ExpirationTime,
): Fiber {
const fiber = createFiberFromElementWithoutDebugInfo(
element,
let owner = null;
if (__DEV__) {
owner = element._owner;
}
const type = element.type;
const key = element.key;
const pendingProps = element.props;
const fiber = createFiberFromTypeAndProps(
type,
key,
pendingProps,
owner,
mode,
expirationTime,
);
Expand Down
124 changes: 91 additions & 33 deletions packages/react-reconciler/src/ReactFiberBeginWork.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import {
Profiler,
SuspenseComponent,
PureComponent,
SimplePureComponent,
LazyComponent,
} from 'shared/ReactWorkTags';
import {
Expand Down Expand Up @@ -103,8 +104,10 @@ import {
import {readLazyComponentType} from './ReactFiberLazyComponent';
import {
resolveLazyComponentTag,
createFiberFromTypeAndProps,
createFiberFromFragment,
createWorkInProgress,
isSimpleFunctionComponent,
} from './ReactFiber';

const ReactCurrentOwner = ReactSharedInternals.ReactCurrentOwner;
Expand Down Expand Up @@ -235,48 +238,99 @@ function updatePureComponent(
nextProps: any,
updateExpirationTime,
renderExpirationTime: ExpirationTime,
) {
const render = Component.render;

): null | Fiber {
if (current === null) {
let type = Component.type;
if (isSimpleFunctionComponent(type) && Component.compare === null) {
// If this is a plain function component without default props,
// and with only the default shallow comparison, we upgrade it
// to a SimplePureComponent to allow fast path updates.
workInProgress.tag = SimplePureComponent;
workInProgress.type = type;
return updateSimplePureComponent(
current,
workInProgress,
type,
nextProps,
updateExpirationTime,
renderExpirationTime,
);
}
let child = createFiberFromTypeAndProps(
Component.type,
null,
nextProps,
null,
workInProgress.mode,
renderExpirationTime,
);
child.ref = workInProgress.ref;
child.return = workInProgress;
workInProgress.child = child;
return child;
}
let currentChild = ((current.child: any): Fiber); // This is always exactly one child
if (
current !== null &&
(updateExpirationTime === NoWork ||
updateExpirationTime > renderExpirationTime)
updateExpirationTime === NoWork ||
updateExpirationTime > renderExpirationTime
) {
const prevProps = current.memoizedProps;
// This will be the props with resolved defaultProps,
// unlike current.memoizedProps which will be the unresolved ones.
const prevProps = currentChild.memoizedProps;
// Default to shallow comparison
let compare = Component.compare;
compare = compare !== null ? compare : shallowEqual;
if (compare(prevProps, nextProps)) {
if (compare(prevProps, nextProps) && current.ref === workInProgress.ref) {
return bailoutOnAlreadyFinishedWork(
current,
workInProgress,
renderExpirationTime,
);
}
}
let newChild = createWorkInProgress(
currentChild,
nextProps,
renderExpirationTime,
);
newChild.ref = workInProgress.ref;
newChild.return = workInProgress;
workInProgress.child = newChild;
return newChild;
}

// The rest is a fork of updateFunctionComponent
let nextChildren;
prepareToReadContext(workInProgress, renderExpirationTime);
if (__DEV__) {
ReactCurrentOwner.current = workInProgress;
ReactCurrentFiber.setCurrentPhase('render');
nextChildren = render(nextProps);
ReactCurrentFiber.setCurrentPhase(null);
} else {
nextChildren = render(nextProps);
function updateSimplePureComponent(
current: Fiber | null,
workInProgress: Fiber,
Component: any,
nextProps: any,
updateExpirationTime,
renderExpirationTime: ExpirationTime,
): null | Fiber {
if (
current !== null &&
(updateExpirationTime === NoWork ||
updateExpirationTime > renderExpirationTime)
) {
const prevProps = current.memoizedProps;
if (
shallowEqual(prevProps, nextProps) &&
current.ref === workInProgress.ref
) {
return bailoutOnAlreadyFinishedWork(
current,
workInProgress,
renderExpirationTime,
);
}
}

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

function updateFragment(
Expand Down Expand Up @@ -725,10 +779,7 @@ function mountLazyComponent(
let Component = readLazyComponentType(elementType);
// Store the unwrapped component in the type.
workInProgress.type = Component;
const resolvedTag = (workInProgress.tag = resolveLazyComponentTag(
workInProgress,
Component,
));
const resolvedTag = (workInProgress.tag = resolveLazyComponentTag(Component));
startWorkTimer(workInProgress);
const resolvedProps = resolveDefaultProps(Component, props);
let child;
Expand Down Expand Up @@ -768,7 +819,7 @@ function mountLazyComponent(
null,
workInProgress,
Component,
resolvedProps,
resolveDefaultProps(Component.type, resolvedProps), // The inner type can have defaults too
updateExpirationTime,
renderExpirationTime,
);
Expand Down Expand Up @@ -1564,10 +1615,7 @@ function beginWork(
case PureComponent: {
const type = workInProgress.type;
const unresolvedProps = workInProgress.pendingProps;
const resolvedProps =
workInProgress.elementType === type
? unresolvedProps
: resolveDefaultProps(type, unresolvedProps);
const resolvedProps = resolveDefaultProps(type.type, unresolvedProps);
return updatePureComponent(
current,
workInProgress,
Expand All @@ -1577,6 +1625,16 @@ function beginWork(
renderExpirationTime,
);
}
case SimplePureComponent: {
return updateSimplePureComponent(
current,
workInProgress,
workInProgress.type,
workInProgress.pendingProps,
updateExpirationTime,
renderExpirationTime,
);
}
default:
invariant(
false,
Expand Down
2 changes: 2 additions & 0 deletions packages/react-reconciler/src/ReactFiberCompleteWork.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import {
Profiler,
SuspenseComponent,
PureComponent,
SimplePureComponent,
LazyComponent,
} from 'shared/ReactWorkTags';
import {Placement, Ref, Update} from 'shared/ReactSideEffectTags';
Expand Down Expand Up @@ -539,6 +540,7 @@ function completeWork(
break;
case LazyComponent:
break;
case SimplePureComponent:
case FunctionComponent:
break;
case ClassComponent: {
Expand Down
44 changes: 36 additions & 8 deletions packages/react-reconciler/src/__tests__/ReactPure-test.internal.js
Original file line number Diff line number Diff line change
Expand Up @@ -183,21 +183,49 @@ describe('pure', () => {
expect(ReactNoop.getChildren()).toEqual([span(1)]);
});

it('warns for class components', () => {
class SomeClass extends React.Component {
it('supports non-pure class components', async () => {
const {unstable_Suspense: Suspense} = React;

class CounterInner extends React.Component {
static defaultProps = {suffix: '!'};
render() {
return null;
return <Text text={this.props.count + '' + this.props.suffix} />;
}
}
expect(() => pure(SomeClass)).toWarnDev(
'pure: The first argument must be a function component.',
{withoutStack: true},
const Counter = pure(CounterInner);

ReactNoop.render(
<Suspense fallback={<Text text="Loading..." />}>
<Counter count={0} />
</Suspense>,
);
expect(ReactNoop.flush()).toEqual(['Loading...']);
await Promise.resolve();
expect(ReactNoop.flush()).toEqual(['0!']);
expect(ReactNoop.getChildren()).toEqual([span('0!')]);

// Should bail out because props have not changed
ReactNoop.render(
<Suspense>
<Counter count={0} />
</Suspense>,
);
expect(ReactNoop.flush()).toEqual([]);
expect(ReactNoop.getChildren()).toEqual([span('0!')]);

// Should update because count prop changed
ReactNoop.render(
<Suspense>
<Counter count={1} />
</Suspense>,
);
expect(ReactNoop.flush()).toEqual(['1!']);
expect(ReactNoop.getChildren()).toEqual([span('1!')]);
});

it('warns if first argument is not a function', () => {
it('warns if first argument is undefined', () => {
expect(() => pure()).toWarnDev(
'pure: The first argument must be a function component. Instead ' +
'pure: The first argument must be a component. Instead ' +
'received: undefined',
{withoutStack: true},
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -415,7 +415,7 @@ exports[`ReactDebugFiberPerf supports pure 1`] = `
⚛ (React Tree Reconciliation: Completed Root)
⚛ Parent [mount]
Pure(Foo) [mount]
⚛ Foo [mount]
⚛ (Committing Changes)
⚛ (Committing Snapshot Effects: 0 Total)
Expand Down
2 changes: 2 additions & 0 deletions packages/react-test-renderer/src/ReactTestRenderer.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import {
ForwardRef,
Profiler,
PureComponent,
SimplePureComponent,
} from 'shared/ReactWorkTags';
import invariant from 'shared/invariant';
import ReactVersion from 'shared/ReactVersion';
Expand Down Expand Up @@ -166,6 +167,7 @@ function toTree(node: ?Fiber) {
rendered: childrenToTree(node.child),
};
case FunctionComponent:
case SimplePureComponent:
return {
nodeType: 'component',
type: node.type,
Expand Down
Loading

0 comments on commit 15b11d2

Please sign in to comment.