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.
Sebastian Markbåge committed
Oct 20, 2018 at 00:32 UTC
15b11d23f960c158a7e99679bf62041ce16aed7e
10 files changed
+294
-74
packages/react-reconciler/src/ReactFiber.js
+26
-16
@@ -299,10 +299,15 @@ function shouldConstruct(Component: Function) {
299
return !!(prototype && prototype.isReactComponent);
300
}
301
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 {
311
if (typeof Component === 'function') {
312
return shouldConstruct(Component) ? ClassComponent : FunctionComponent;
313
} else if (Component !== undefined && Component !== null) {
@@ -406,20 +411,15 @@ export function createHostRootFiber(isConcurrent: boolean): Fiber {
411
return createFiber(HostRoot, null, null, mode);
412
}
413
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,
419
mode: TypeOfMode,
420
expirationTime: ExpirationTime,
421
): Fiber {
414
- let owner = null;
415
- if (__DEV__) {
416
- owner = element._owner;
417
- }
418
-
422
let fiber;
420
- const type = element.type;
421
- const key = element.key;
422
- const pendingProps = element.props;
423
424
let fiberTag = IndeterminateComponent;
425
// 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(
522
mode: TypeOfMode,
523
expirationTime: ExpirationTime,
524
): 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,
537
mode,
538
expirationTime,
539
);
packages/react-reconciler/src/ReactFiberBeginWork.js
+91
-33
@@ -31,6 +31,7 @@ import {
31
Profiler,
32
SuspenseComponent,
33
PureComponent,
34
+ SimplePureComponent,
35
LazyComponent,
36
} from 'shared/ReactWorkTags';
37
import {
@@ -103,8 +104,10 @@ import {
104
import {readLazyComponentType} from './ReactFiberLazyComponent';
105
import {
106
resolveLazyComponentTag,
107
+ createFiberFromTypeAndProps,
108
createFiberFromFragment,
109
createWorkInProgress,
110
+ isSimpleFunctionComponent,
111
} from './ReactFiber';
112
113
const ReactCurrentOwner = ReactSharedInternals.ReactCurrentOwner;
@@ -235,19 +238,49 @@ function updatePureComponent(
238
nextProps: any,
239
updateExpirationTime,
240
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
273
if (
242
- current !== null &&
243
- (updateExpirationTime === NoWork ||
244
- updateExpirationTime > renderExpirationTime)
274
+ updateExpirationTime === NoWork ||
275
+ updateExpirationTime > renderExpirationTime
276
) {
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;
280
// Default to shallow comparison
281
let compare = Component.compare;
282
compare = compare !== null ? compare : shallowEqual;
250
- if (compare(prevProps, nextProps)) {
283
+ if (compare(prevProps, nextProps) && current.ref === workInProgress.ref) {
284
return bailoutOnAlreadyFinishedWork(
285
current,
286
workInProgress,
@@ -255,28 +288,49 @@ function updatePureComponent(
288
);
289
}
290
}
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
+}
301
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
+ }
326
}
270
-
271
- // React DevTools reads this flag.
272
- workInProgress.effectTag |= PerformedWork;
273
- reconcileChildren(
327
+ return updateFunctionComponent(
328
current,
329
workInProgress,
276
- nextChildren,
330
+ Component,
331
+ nextProps,
332
renderExpirationTime,
333
);
279
- return workInProgress.child;
334
}
335
336
function updateFragment(
@@ -725,10 +779,7 @@ function mountLazyComponent(
779
let Component = readLazyComponentType(elementType);
780
// Store the unwrapped component in the type.
781
workInProgress.type = Component;
728
- const resolvedTag = (workInProgress.tag = resolveLazyComponentTag(
729
- workInProgress,
730
- Component,
731
- ));
782
+ const resolvedTag = (workInProgress.tag = resolveLazyComponentTag(Component));
783
startWorkTimer(workInProgress);
784
const resolvedProps = resolveDefaultProps(Component, props);
785
let child;
@@ -768,7 +819,7 @@ function mountLazyComponent(
819
null,
820
workInProgress,
821
Component,
771
- resolvedProps,
822
+ resolveDefaultProps(Component.type, resolvedProps), // The inner type can have defaults too
823
updateExpirationTime,
824
renderExpirationTime,
825
);
@@ -1564,10 +1615,7 @@ function beginWork(
1615
case PureComponent: {
1616
const type = workInProgress.type;
1617
const unresolvedProps = workInProgress.pendingProps;
1567
- const resolvedProps =
1568
- workInProgress.elementType === type
1569
- ? unresolvedProps
1570
- : resolveDefaultProps(type, unresolvedProps);
1618
+ const resolvedProps = resolveDefaultProps(type.type, unresolvedProps);
1619
return updatePureComponent(
1620
current,
1621
workInProgress,
@@ -1577,6 +1625,16 @@ function beginWork(
1625
renderExpirationTime,
1626
);
1627
}
1628
+ case SimplePureComponent: {
1629
+ return updateSimplePureComponent(
1630
+ current,
1631
+ workInProgress,
1632
+ workInProgress.type,
1633
+ workInProgress.pendingProps,
1634
+ updateExpirationTime,
1635
+ renderExpirationTime,
1636
+ );
1637
+ }
1638
default:
1639
invariant(
1640
false,
packages/react-reconciler/src/ReactFiberCompleteWork.js
+2
@@ -35,6 +35,7 @@ import {
35
Profiler,
36
SuspenseComponent,
37
PureComponent,
38
+ SimplePureComponent,
39
LazyComponent,
40
} from 'shared/ReactWorkTags';
41
import {Placement, Ref, Update} from 'shared/ReactSideEffectTags';
@@ -539,6 +540,7 @@ function completeWork(
540
break;
541
case LazyComponent:
542
break;
543
+ case SimplePureComponent:
544
case FunctionComponent:
545
break;
546
case ClassComponent: {
packages/react-reconciler/src/__tests__/ReactPure-test.internal.js
+36
-8
@@ -183,21 +183,49 @@ describe('pure', () => {
183
expect(ReactNoop.getChildren()).toEqual([span(1)]);
184
});
185
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: '!'};
191
render() {
189
- return null;
192
+ return <Text text={this.props.count + '' + this.props.suffix} />;
193
}
194
}
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>,
221
);
222
+ expect(ReactNoop.flush()).toEqual(['1!']);
223
+ expect(ReactNoop.getChildren()).toEqual([span('1!')]);
224
});
225
198
- it('warns if first argument is not a function', () => {
226
+ it('warns if first argument is undefined', () => {
227
expect(() => pure()).toWarnDev(
200
- 'pure: The first argument must be a function component. Instead ' +
228
+ 'pure: The first argument must be a component. Instead ' +
229
'received: undefined',
230
{withoutStack: true},
231
);
packages/react-reconciler/src/__tests__/__snapshots__/ReactIncrementalPerf-test.internal.js.snap
+1
-1
@@ -415,7 +415,7 @@ exports[`ReactDebugFiberPerf supports pure 1`] = `
415
416
⚛ (React Tree Reconciliation: Completed Root)
417
⚛ Parent [mount]
418
- ⚛ Pure(Foo) [mount]
418
+ ⚛ Foo [mount]
419
420
⚛ (Committing Changes)
421
⚛ (Committing Snapshot Effects: 0 Total)
packages/react-test-renderer/src/ReactTestRenderer.js
+2
@@ -28,6 +28,7 @@ import {
28
ForwardRef,
29
Profiler,
30
PureComponent,
31
+ SimplePureComponent,
32
} from 'shared/ReactWorkTags';
33
import invariant from 'shared/invariant';
34
import ReactVersion from 'shared/ReactVersion';
@@ -166,6 +167,7 @@ function toTree(node: ?Fiber) {
167
rendered: childrenToTree(node.child),
168
};
169
case FunctionComponent:
170
+ case SimplePureComponent:
171
return {
172
nodeType: 'component',
173
type: node.type,
packages/react/src/__tests__/forwardRef-test.js
+127
@@ -226,4 +226,131 @@ describe('forwardRef', () => {
226
' in Foo (at **)',
227
);
228
});
229
+
230
+ it('should not bailout if forwardRef is not wrapped in pure', () => {
231
+ const Component = props => <div {...props} />;
232
+
233
+ let renderCount = 0;
234
+
235
+ const RefForwardingComponent = React.forwardRef((props, ref) => {
236
+ renderCount++;
237
+ return <Component {...props} forwardedRef={ref} />;
238
+ });
239
+
240
+ const ref = React.createRef();
241
+
242
+ ReactNoop.render(<RefForwardingComponent ref={ref} optional="foo" />);
243
+ ReactNoop.flush();
244
+ expect(renderCount).toBe(1);
245
+
246
+ ReactNoop.render(<RefForwardingComponent ref={ref} optional="foo" />);
247
+ ReactNoop.flush();
248
+ expect(renderCount).toBe(2);
249
+ });
250
+
251
+ it('should bailout if forwardRef is wrapped in pure', () => {
252
+ const Component = props => <div ref={props.forwardedRef} />;
253
+
254
+ let renderCount = 0;
255
+
256
+ const RefForwardingComponent = React.pure(
257
+ React.forwardRef((props, ref) => {
258
+ renderCount++;
259
+ return <Component {...props} forwardedRef={ref} />;
260
+ }),
261
+ );
262
+
263
+ const ref = React.createRef();
264
+
265
+ ReactNoop.render(<RefForwardingComponent ref={ref} optional="foo" />);
266
+ ReactNoop.flush();
267
+ expect(renderCount).toBe(1);
268
+
269
+ expect(ref.current.type).toBe('div');
270
+
271
+ ReactNoop.render(<RefForwardingComponent ref={ref} optional="foo" />);
272
+ ReactNoop.flush();
273
+ expect(renderCount).toBe(1);
274
+
275
+ const differentRef = React.createRef();
276
+
277
+ ReactNoop.render(
278
+ <RefForwardingComponent ref={differentRef} optional="foo" />,
279
+ );
280
+ ReactNoop.flush();
281
+ expect(renderCount).toBe(2);
282
+
283
+ expect(ref.current).toBe(null);
284
+ expect(differentRef.current.type).toBe('div');
285
+
286
+ ReactNoop.render(<RefForwardingComponent ref={ref} optional="bar" />);
287
+ ReactNoop.flush();
288
+ expect(renderCount).toBe(3);
289
+ });
290
+
291
+ it('should custom pure comparisons to compose', () => {
292
+ const Component = props => <div ref={props.forwardedRef} />;
293
+
294
+ let renderCount = 0;
295
+
296
+ const RefForwardingComponent = React.pure(
297
+ React.forwardRef((props, ref) => {
298
+ renderCount++;
299
+ return <Component {...props} forwardedRef={ref} />;
300
+ }),
301
+ (o, p) => o.a === p.a && o.b === p.b,
302
+ );
303
+
304
+ const ref = React.createRef();
305
+
306
+ ReactNoop.render(<RefForwardingComponent ref={ref} a="0" b="0" c="1" />);
307
+ ReactNoop.flush();
308
+ expect(renderCount).toBe(1);
309
+
310
+ expect(ref.current.type).toBe('div');
311
+
312
+ // Changing either a or b rerenders
313
+ ReactNoop.render(<RefForwardingComponent ref={ref} a="0" b="1" c="1" />);
314
+ ReactNoop.flush();
315
+ expect(renderCount).toBe(2);
316
+
317
+ // Changing c doesn't rerender
318
+ ReactNoop.render(<RefForwardingComponent ref={ref} a="0" b="1" c="2" />);
319
+ ReactNoop.flush();
320
+ expect(renderCount).toBe(2);
321
+
322
+ const ComposedPure = React.pure(
323
+ RefForwardingComponent,
324
+ (o, p) => o.a === p.a && o.c === p.c,
325
+ );
326
+
327
+ ReactNoop.render(<ComposedPure ref={ref} a="0" b="0" c="0" />);
328
+ ReactNoop.flush();
329
+ expect(renderCount).toBe(3);
330
+
331
+ // Changing just b no longer updates
332
+ ReactNoop.render(<ComposedPure ref={ref} a="0" b="1" c="0" />);
333
+ ReactNoop.flush();
334
+ expect(renderCount).toBe(3);
335
+
336
+ // Changing just a and c updates
337
+ ReactNoop.render(<ComposedPure ref={ref} a="2" b="2" c="2" />);
338
+ ReactNoop.flush();
339
+ expect(renderCount).toBe(4);
340
+
341
+ // Changing just c does not update
342
+ ReactNoop.render(<ComposedPure ref={ref} a="2" b="2" c="3" />);
343
+ ReactNoop.flush();
344
+ expect(renderCount).toBe(4);
345
+
346
+ // Changing ref still rerenders
347
+ const differentRef = React.createRef();
348
+
349
+ ReactNoop.render(<ComposedPure ref={differentRef} a="2" b="2" c="3" />);
350
+ ReactNoop.flush();
351
+ expect(renderCount).toBe(5);
352
+
353
+ expect(ref.current).toBe(null);
354
+ expect(differentRef.current.type).toBe('div');
355
+ });
356
});
packages/react/src/pure.js
+6
-14
@@ -7,34 +7,26 @@
7
8
import {REACT_PURE_TYPE} from 'shared/ReactSymbols';
9
10
+import isValidElementType from 'shared/isValidElementType';
11
import warningWithoutStack from 'shared/warningWithoutStack';
12
13
export default function pure<Props>(
13
- render: (props: Props) => React$Node,
14
+ type: React$ElementType,
15
compare?: (oldProps: Props, newProps: Props) => boolean,
16
) {
17
if (__DEV__) {
17
- if (typeof render !== 'function') {
18
+ if (!isValidElementType(type)) {
19
warningWithoutStack(
20
false,
20
- 'pure: The first argument must be a function component. Instead ' +
21
+ 'pure: The first argument must be a component. Instead ' +
22
'received: %s',
22
- render === null ? 'null' : typeof render,
23
+ type === null ? 'null' : typeof type,
24
);
24
- } else {
25
- const prototype = render.prototype;
26
- if (prototype && prototype.isReactComponent) {
27
- warningWithoutStack(
28
- false,
29
- 'pure: The first argument must be a function component. Classes ' +
30
- 'are not supported. Use React.PureComponent instead.',
31
- );
32
- }
25
}
26
}
27
return {
28
$$typeof: REACT_PURE_TYPE,
37
- render,
29
+ type,
30
compare: compare === undefined ? null : compare,
31
};
32
}
packages/shared/ReactWorkTags.js
+2
-1
@@ -43,4 +43,5 @@ export const ForwardRef = 11;
43
export const Profiler = 12;
44
export const SuspenseComponent = 13;
45
export const PureComponent = 14;
46
-export const LazyComponent = 15;
46
+export const SimplePureComponent = 15;
47
+export const LazyComponent = 16;
packages/shared/getComponentName.js
+1
-1
@@ -80,7 +80,7 @@ function getComponentName(type: mixed): string | null {
80
case REACT_FORWARD_REF_TYPE:
81
return getWrappedName(type, type.render, 'ForwardRef');
82
case REACT_PURE_TYPE:
83
- return getWrappedName(type, type.render, 'Pure');
83
+ return getComponentName(type.type);
84
case REACT_LAZY_TYPE: {
85
const thenable: LazyComponent<mixed> = (type: any);
86
const resolvedThenable = refineResolvedLazyComponent(thenable);