Skip to content

Commit 15b11d2

Browse files
authored
Allow arbitrary types to be wrapped in pure (#13903)
* 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.
1 parent e770af7 commit 15b11d2

File tree

10 files changed

+294
-74
lines changed

10 files changed

+294
-74
lines changed

packages/react-reconciler/src/ReactFiber.js

Lines changed: 26 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -299,10 +299,15 @@ function shouldConstruct(Component: Function) {
299299
return !!(prototype && prototype.isReactComponent);
300300
}
301301

302-
export function resolveLazyComponentTag(
303-
fiber: Fiber,
304-
Component: Function,
305-
): WorkTag {
302+
export function isSimpleFunctionComponent(type: any) {
303+
return (
304+
typeof type === 'function' &&
305+
!shouldConstruct(type) &&
306+
type.defaultProps === undefined
307+
);
308+
}
309+
310+
export function resolveLazyComponentTag(Component: Function): WorkTag {
306311
if (typeof Component === 'function') {
307312
return shouldConstruct(Component) ? ClassComponent : FunctionComponent;
308313
} else if (Component !== undefined && Component !== null) {
@@ -406,20 +411,15 @@ export function createHostRootFiber(isConcurrent: boolean): Fiber {
406411
return createFiber(HostRoot, null, null, mode);
407412
}
408413

409-
function createFiberFromElementWithoutDebugInfo(
410-
element: ReactElement,
414+
export function createFiberFromTypeAndProps(
415+
type: any, // React$ElementType
416+
key: null | string,
417+
pendingProps: any,
418+
owner: null | Fiber,
411419
mode: TypeOfMode,
412420
expirationTime: ExpirationTime,
413421
): Fiber {
414-
let owner = null;
415-
if (__DEV__) {
416-
owner = element._owner;
417-
}
418-
419422
let fiber;
420-
const type = element.type;
421-
const key = element.key;
422-
const pendingProps = element.props;
423423

424424
let fiberTag = IndeterminateComponent;
425425
// The resolved type is set if we know what the final type will be. I.e. it's not lazy.
@@ -522,8 +522,18 @@ export function createFiberFromElement(
522522
mode: TypeOfMode,
523523
expirationTime: ExpirationTime,
524524
): Fiber {
525-
const fiber = createFiberFromElementWithoutDebugInfo(
526-
element,
525+
let owner = null;
526+
if (__DEV__) {
527+
owner = element._owner;
528+
}
529+
const type = element.type;
530+
const key = element.key;
531+
const pendingProps = element.props;
532+
const fiber = createFiberFromTypeAndProps(
533+
type,
534+
key,
535+
pendingProps,
536+
owner,
527537
mode,
528538
expirationTime,
529539
);

packages/react-reconciler/src/ReactFiberBeginWork.js

Lines changed: 91 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import {
3131
Profiler,
3232
SuspenseComponent,
3333
PureComponent,
34+
SimplePureComponent,
3435
LazyComponent,
3536
} from 'shared/ReactWorkTags';
3637
import {
@@ -103,8 +104,10 @@ import {
103104
import {readLazyComponentType} from './ReactFiberLazyComponent';
104105
import {
105106
resolveLazyComponentTag,
107+
createFiberFromTypeAndProps,
106108
createFiberFromFragment,
107109
createWorkInProgress,
110+
isSimpleFunctionComponent,
108111
} from './ReactFiber';
109112

110113
const ReactCurrentOwner = ReactSharedInternals.ReactCurrentOwner;
@@ -235,48 +238,99 @@ function updatePureComponent(
235238
nextProps: any,
236239
updateExpirationTime,
237240
renderExpirationTime: ExpirationTime,
238-
) {
239-
const render = Component.render;
240-
241+
): null | Fiber {
242+
if (current === null) {
243+
let type = Component.type;
244+
if (isSimpleFunctionComponent(type) && Component.compare === null) {
245+
// If this is a plain function component without default props,
246+
// and with only the default shallow comparison, we upgrade it
247+
// to a SimplePureComponent to allow fast path updates.
248+
workInProgress.tag = SimplePureComponent;
249+
workInProgress.type = type;
250+
return updateSimplePureComponent(
251+
current,
252+
workInProgress,
253+
type,
254+
nextProps,
255+
updateExpirationTime,
256+
renderExpirationTime,
257+
);
258+
}
259+
let child = createFiberFromTypeAndProps(
260+
Component.type,
261+
null,
262+
nextProps,
263+
null,
264+
workInProgress.mode,
265+
renderExpirationTime,
266+
);
267+
child.ref = workInProgress.ref;
268+
child.return = workInProgress;
269+
workInProgress.child = child;
270+
return child;
271+
}
272+
let currentChild = ((current.child: any): Fiber); // This is always exactly one child
241273
if (
242-
current !== null &&
243-
(updateExpirationTime === NoWork ||
244-
updateExpirationTime > renderExpirationTime)
274+
updateExpirationTime === NoWork ||
275+
updateExpirationTime > renderExpirationTime
245276
) {
246-
const prevProps = current.memoizedProps;
277+
// This will be the props with resolved defaultProps,
278+
// unlike current.memoizedProps which will be the unresolved ones.
279+
const prevProps = currentChild.memoizedProps;
247280
// Default to shallow comparison
248281
let compare = Component.compare;
249282
compare = compare !== null ? compare : shallowEqual;
250-
if (compare(prevProps, nextProps)) {
283+
if (compare(prevProps, nextProps) && current.ref === workInProgress.ref) {
251284
return bailoutOnAlreadyFinishedWork(
252285
current,
253286
workInProgress,
254287
renderExpirationTime,
255288
);
256289
}
257290
}
291+
let newChild = createWorkInProgress(
292+
currentChild,
293+
nextProps,
294+
renderExpirationTime,
295+
);
296+
newChild.ref = workInProgress.ref;
297+
newChild.return = workInProgress;
298+
workInProgress.child = newChild;
299+
return newChild;
300+
}
258301

259-
// The rest is a fork of updateFunctionComponent
260-
let nextChildren;
261-
prepareToReadContext(workInProgress, renderExpirationTime);
262-
if (__DEV__) {
263-
ReactCurrentOwner.current = workInProgress;
264-
ReactCurrentFiber.setCurrentPhase('render');
265-
nextChildren = render(nextProps);
266-
ReactCurrentFiber.setCurrentPhase(null);
267-
} else {
268-
nextChildren = render(nextProps);
302+
function updateSimplePureComponent(
303+
current: Fiber | null,
304+
workInProgress: Fiber,
305+
Component: any,
306+
nextProps: any,
307+
updateExpirationTime,
308+
renderExpirationTime: ExpirationTime,
309+
): null | Fiber {
310+
if (
311+
current !== null &&
312+
(updateExpirationTime === NoWork ||
313+
updateExpirationTime > renderExpirationTime)
314+
) {
315+
const prevProps = current.memoizedProps;
316+
if (
317+
shallowEqual(prevProps, nextProps) &&
318+
current.ref === workInProgress.ref
319+
) {
320+
return bailoutOnAlreadyFinishedWork(
321+
current,
322+
workInProgress,
323+
renderExpirationTime,
324+
);
325+
}
269326
}
270-
271-
// React DevTools reads this flag.
272-
workInProgress.effectTag |= PerformedWork;
273-
reconcileChildren(
327+
return updateFunctionComponent(
274328
current,
275329
workInProgress,
276-
nextChildren,
330+
Component,
331+
nextProps,
277332
renderExpirationTime,
278333
);
279-
return workInProgress.child;
280334
}
281335

282336
function updateFragment(
@@ -725,10 +779,7 @@ function mountLazyComponent(
725779
let Component = readLazyComponentType(elementType);
726780
// Store the unwrapped component in the type.
727781
workInProgress.type = Component;
728-
const resolvedTag = (workInProgress.tag = resolveLazyComponentTag(
729-
workInProgress,
730-
Component,
731-
));
782+
const resolvedTag = (workInProgress.tag = resolveLazyComponentTag(Component));
732783
startWorkTimer(workInProgress);
733784
const resolvedProps = resolveDefaultProps(Component, props);
734785
let child;
@@ -768,7 +819,7 @@ function mountLazyComponent(
768819
null,
769820
workInProgress,
770821
Component,
771-
resolvedProps,
822+
resolveDefaultProps(Component.type, resolvedProps), // The inner type can have defaults too
772823
updateExpirationTime,
773824
renderExpirationTime,
774825
);
@@ -1564,10 +1615,7 @@ function beginWork(
15641615
case PureComponent: {
15651616
const type = workInProgress.type;
15661617
const unresolvedProps = workInProgress.pendingProps;
1567-
const resolvedProps =
1568-
workInProgress.elementType === type
1569-
? unresolvedProps
1570-
: resolveDefaultProps(type, unresolvedProps);
1618+
const resolvedProps = resolveDefaultProps(type.type, unresolvedProps);
15711619
return updatePureComponent(
15721620
current,
15731621
workInProgress,
@@ -1577,6 +1625,16 @@ function beginWork(
15771625
renderExpirationTime,
15781626
);
15791627
}
1628+
case SimplePureComponent: {
1629+
return updateSimplePureComponent(
1630+
current,
1631+
workInProgress,
1632+
workInProgress.type,
1633+
workInProgress.pendingProps,
1634+
updateExpirationTime,
1635+
renderExpirationTime,
1636+
);
1637+
}
15801638
default:
15811639
invariant(
15821640
false,

packages/react-reconciler/src/ReactFiberCompleteWork.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import {
3535
Profiler,
3636
SuspenseComponent,
3737
PureComponent,
38+
SimplePureComponent,
3839
LazyComponent,
3940
} from 'shared/ReactWorkTags';
4041
import {Placement, Ref, Update} from 'shared/ReactSideEffectTags';
@@ -539,6 +540,7 @@ function completeWork(
539540
break;
540541
case LazyComponent:
541542
break;
543+
case SimplePureComponent:
542544
case FunctionComponent:
543545
break;
544546
case ClassComponent: {

packages/react-reconciler/src/__tests__/ReactPure-test.internal.js

Lines changed: 36 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -183,21 +183,49 @@ describe('pure', () => {
183183
expect(ReactNoop.getChildren()).toEqual([span(1)]);
184184
});
185185

186-
it('warns for class components', () => {
187-
class SomeClass extends React.Component {
186+
it('supports non-pure class components', async () => {
187+
const {unstable_Suspense: Suspense} = React;
188+
189+
class CounterInner extends React.Component {
190+
static defaultProps = {suffix: '!'};
188191
render() {
189-
return null;
192+
return <Text text={this.props.count + '' + this.props.suffix} />;
190193
}
191194
}
192-
expect(() => pure(SomeClass)).toWarnDev(
193-
'pure: The first argument must be a function component.',
194-
{withoutStack: true},
195+
const Counter = pure(CounterInner);
196+
197+
ReactNoop.render(
198+
<Suspense fallback={<Text text="Loading..." />}>
199+
<Counter count={0} />
200+
</Suspense>,
201+
);
202+
expect(ReactNoop.flush()).toEqual(['Loading...']);
203+
await Promise.resolve();
204+
expect(ReactNoop.flush()).toEqual(['0!']);
205+
expect(ReactNoop.getChildren()).toEqual([span('0!')]);
206+
207+
// Should bail out because props have not changed
208+
ReactNoop.render(
209+
<Suspense>
210+
<Counter count={0} />
211+
</Suspense>,
212+
);
213+
expect(ReactNoop.flush()).toEqual([]);
214+
expect(ReactNoop.getChildren()).toEqual([span('0!')]);
215+
216+
// Should update because count prop changed
217+
ReactNoop.render(
218+
<Suspense>
219+
<Counter count={1} />
220+
</Suspense>,
195221
);
222+
expect(ReactNoop.flush()).toEqual(['1!']);
223+
expect(ReactNoop.getChildren()).toEqual([span('1!')]);
196224
});
197225

198-
it('warns if first argument is not a function', () => {
226+
it('warns if first argument is undefined', () => {
199227
expect(() => pure()).toWarnDev(
200-
'pure: The first argument must be a function component. Instead ' +
228+
'pure: The first argument must be a component. Instead ' +
201229
'received: undefined',
202230
{withoutStack: true},
203231
);

packages/react-reconciler/src/__tests__/__snapshots__/ReactIncrementalPerf-test.internal.js.snap

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -415,7 +415,7 @@ exports[`ReactDebugFiberPerf supports pure 1`] = `
415415
416416
⚛ (React Tree Reconciliation: Completed Root)
417417
⚛ Parent [mount]
418-
Pure(Foo) [mount]
418+
⚛ Foo [mount]
419419
420420
⚛ (Committing Changes)
421421
⚛ (Committing Snapshot Effects: 0 Total)

packages/react-test-renderer/src/ReactTestRenderer.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import {
2828
ForwardRef,
2929
Profiler,
3030
PureComponent,
31+
SimplePureComponent,
3132
} from 'shared/ReactWorkTags';
3233
import invariant from 'shared/invariant';
3334
import ReactVersion from 'shared/ReactVersion';
@@ -166,6 +167,7 @@ function toTree(node: ?Fiber) {
166167
rendered: childrenToTree(node.child),
167168
};
168169
case FunctionComponent:
170+
case SimplePureComponent:
169171
return {
170172
nodeType: 'component',
171173
type: node.type,

0 commit comments

Comments
 (0)