@samitouri / QOS-React / commits / 15b11d23f9

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);