From e576180720f740e37c7508389c0398ea47e554ea Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Tue, 14 Aug 2018 13:16:51 -0700 Subject: [PATCH 01/14] Accept promise as element type On the initial render, the element will suspend as if a promise were thrown from inside the body of the unresolved component. Siblings should continue rendering and if the parent is a Placeholder, the promise should be captured by that Placeholder. When the promise resolves, rendering resumes. If the resolved value has a `default` property, it is assumed to be the default export of an ES module, and we use that as the component type. If it does not have a `default` property, we use the resolved value itself. The resolved value is stored as an expando on the promise/thenable. --- .../react-reconciler/src/ReactChildFiber.js | 22 ++- packages/react-reconciler/src/ReactFiber.js | 55 +++++++- .../__tests__/ReactSuspense-test.internal.js | 127 ++++++++++++++++++ packages/shared/isValidElementType.js | 3 +- 4 files changed, 199 insertions(+), 8 deletions(-) diff --git a/packages/react-reconciler/src/ReactChildFiber.js b/packages/react-reconciler/src/ReactChildFiber.js index 1f377fee3c6..90195e8729b 100644 --- a/packages/react-reconciler/src/ReactChildFiber.js +++ b/packages/react-reconciler/src/ReactChildFiber.js @@ -377,7 +377,14 @@ function ChildReconciler(shouldTrackSideEffects) { element: ReactElement, expirationTime: ExpirationTime, ): Fiber { - if (current !== null && current.type === element.type) { + const elementType = element.type; + if ( + current !== null && + (current.type === elementType || + (elementType !== null && + elementType !== undefined && + current.type === elementType._reactResult)) + ) { // Move based on index const existing = useFiber(current, element.props, expirationTime); existing.ref = coerceRef(returnFiber, current, element); @@ -1111,21 +1118,26 @@ function ChildReconciler(shouldTrackSideEffects) { element: ReactElement, expirationTime: ExpirationTime, ): Fiber { + const elementType = element.type; const key = element.key; let child = currentFirstChild; while (child !== null) { // TODO: If key === null and child.key === null, then this only applies to // the first item in the list. if (child.key === key) { + const childType = child.type; if ( child.tag === Fragment - ? element.type === REACT_FRAGMENT_TYPE - : child.type === element.type + ? elementType === REACT_FRAGMENT_TYPE + : childType === elementType || + (elementType !== null && + elementType !== undefined && + childType === elementType._reactResult) ) { deleteRemainingChildren(returnFiber, child.sibling); const existing = useFiber( child, - element.type === REACT_FRAGMENT_TYPE + elementType === REACT_FRAGMENT_TYPE ? element.props.children : element.props, expirationTime, @@ -1147,7 +1159,7 @@ function ChildReconciler(shouldTrackSideEffects) { child = child.sibling; } - if (element.type === REACT_FRAGMENT_TYPE) { + if (elementType === REACT_FRAGMENT_TYPE) { const created = createFiberFromFragment( element.props.children, returnFiber.mode, diff --git a/packages/react-reconciler/src/ReactFiber.js b/packages/react-reconciler/src/ReactFiber.js index 01aa5a29c2b..fcfe933081f 100644 --- a/packages/react-reconciler/src/ReactFiber.js +++ b/packages/react-reconciler/src/ReactFiber.js @@ -388,9 +388,13 @@ export function createFiberFromElement( } let fiber; - const type = element.type; + let type = element.type; const key = element.key; - let pendingProps = element.props; + const pendingProps = element.props; + + if (type !== null && type !== undefined && typeof type.then === 'function') { + type = resolveThenableType(type); + } let fiberTag; if (typeof type === 'function') { @@ -437,6 +441,53 @@ export function createFiberFromElement( return fiber; } +export const Pending = 0; +const Resolved = 1; +const Rejected = 2; + +function UnresolvedComponent(thenable) { + thenable.then( + resolvedValue => { + if (thenable._reactStatus === Pending) { + thenable._reactStatus = Resolved; + // If the default value is not empty, assume it's the result of + // an async import() and use that. Otherwise, use resolved value. + const defaultExport = resolvedValue.default; + thenable._reactResult = + defaultExport !== null && defaultExport !== undefined + ? defaultExport + : resolvedValue; + } + }, + error => { + if (thenable._reactStatus === Pending) { + thenable._reactStatus = Rejected; + thenable._reactResult = error; + } + }, + ); + throw thenable; +} + +function RejectedComponent(error) { + throw error; +} + +function resolveThenableType(type) { + const result = type._reactResult; + switch (type._reactStatus) { + case Pending: + return UnresolvedComponent.bind(null, type); + case Resolved: + return result; + case Rejected: + return RejectedComponent.bind(null, result); + default: + type._reactStatus = Pending; + return UnresolvedComponent.bind(null, type); + } +} + function getFiberTagFromObjectType(type, owner): TypeOfWork { const $$typeof = typeof type === 'object' && type !== null ? type.$$typeof : null; diff --git a/packages/react-reconciler/src/__tests__/ReactSuspense-test.internal.js b/packages/react-reconciler/src/__tests__/ReactSuspense-test.internal.js index c95e050cfe0..1294070f639 100644 --- a/packages/react-reconciler/src/__tests__/ReactSuspense-test.internal.js +++ b/packages/react-reconciler/src/__tests__/ReactSuspense-test.internal.js @@ -1347,6 +1347,133 @@ describe('ReactSuspense', () => { }); }); + describe('Promise as element type', () => { + it('accepts a promise as an element type', async () => { + const LazyText = Promise.resolve(Text); + + ReactNoop.render( + }> + + , + ); + expect(ReactNoop.flush()).toEqual(['Loading...']); + expect(ReactNoop.getChildren()).toEqual([]); + + await LazyText; + + expect(ReactNoop.flush()).toEqual(['Hi']); + expect(ReactNoop.getChildren()).toEqual([span('Hi')]); + + // Should not suspend on update + ReactNoop.render( + }> + + , + ); + expect(ReactNoop.flush()).toEqual(['Hi again']); + expect(ReactNoop.getChildren()).toEqual([span('Hi again')]); + }); + + it('throws if promise rejects', async () => { + const LazyText = Promise.reject(new Error('Bad network')); + + ReactNoop.render( + }> + + , + ); + expect(ReactNoop.flush()).toEqual(['Loading...']); + + await LazyText.catch(() => {}); + + expect(() => ReactNoop.flush()).toThrow('Bad network'); + }); + + it('mount and reorder', async () => { + class Child extends React.Component { + componentDidMount() { + ReactNoop.yield('Did mount: ' + this.props.label); + } + componentDidUpdate() { + ReactNoop.yield('Did update: ' + this.props.label); + } + render() { + return ; + } + } + + const LazyChildA = Promise.resolve(Child); + const LazyChildB = Promise.resolve(Child); + + function Parent({swap}) { + return ( + }> + {swap + ? [ + , + , + ] + : [ + , + , + ]} + + ); + } + + ReactNoop.render(); + expect(ReactNoop.flush()).toEqual(['Loading...']); + expect(ReactNoop.getChildren()).toEqual([]); + + await LazyChildA; + await LazyChildB; + + expect(ReactNoop.flush()).toEqual([ + 'A', + 'B', + 'Did mount: A', + 'Did mount: B', + ]); + expect(ReactNoop.getChildren()).toEqual([span('A'), span('B')]); + + // Swap the position of A and B + ReactNoop.render(); + expect(ReactNoop.flush()).toEqual([ + 'B', + 'A', + 'Did update: B', + 'Did update: A', + ]); + expect(ReactNoop.getChildren()).toEqual([span('B'), span('A')]); + }); + + it('uses `default` property, if it exists', async () => { + const LazyText = Promise.resolve({default: Text}); + + ReactNoop.render( + }> + + , + ); + expect(ReactNoop.flush()).toEqual(['Loading...']); + expect(ReactNoop.getChildren()).toEqual([]); + + await LazyText; + + expect(ReactNoop.flush()).toEqual(['Hi']); + expect(ReactNoop.getChildren()).toEqual([span('Hi')]); + + // Should not suspend on update + ReactNoop.render( + }> + + , + ); + expect(ReactNoop.flush()).toEqual(['Hi again']); + expect(ReactNoop.getChildren()).toEqual([span('Hi again')]); + }); + }); + it('does not call lifecycles of a suspended component', async () => { class TextWithLifecycle extends React.Component { componentDidMount() { diff --git a/packages/shared/isValidElementType.js b/packages/shared/isValidElementType.js index c52354495ff..7f7d73926c3 100644 --- a/packages/shared/isValidElementType.js +++ b/packages/shared/isValidElementType.js @@ -30,7 +30,8 @@ export default function isValidElementType(type: mixed) { type === REACT_PLACEHOLDER_TYPE || (typeof type === 'object' && type !== null && - (type.$$typeof === REACT_PROVIDER_TYPE || + (typeof type.then === 'function' || + type.$$typeof === REACT_PROVIDER_TYPE || type.$$typeof === REACT_CONTEXT_TYPE || type.$$typeof === REACT_FORWARD_REF_TYPE)) ); From 260bf066b77a23c299560f679c6e0236f0bf6c50 Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Tue, 14 Aug 2018 18:00:52 -0700 Subject: [PATCH 02/14] Use special types of work for lazy components Because reconciliation is a hot path, this adds ClassComponentLazy, FunctionalComponentLazy, and ForwardRefLazy as special types of work. The other types are not supported, but wouldn't be placed into a separate module regardless. --- .../src/test-utils/ReactTestUtils.js | 6 +- .../react-reconciler/src/ReactChildFiber.js | 31 ++-- .../react-reconciler/src/ReactCurrentFiber.js | 4 + packages/react-reconciler/src/ReactFiber.js | 60 +------ .../src/ReactFiberBeginWork.js | 154 +++++++++++++++--- .../src/ReactFiberClassComponent.js | 58 ++++--- .../src/ReactFiberCommitWork.js | 17 +- .../src/ReactFiberCompleteWork.js | 21 ++- .../react-reconciler/src/ReactFiberContext.js | 52 ++++-- .../src/ReactFiberLazyComponent.js | 61 +++++++ .../src/ReactFiberNewContext.js | 11 +- .../src/ReactFiberReconciler.js | 29 +++- .../src/ReactFiberScheduler.js | 27 ++- .../src/ReactFiberTreeReflection.js | 6 +- .../src/ReactFiberUnwindWork.js | 41 ++++- .../react-reconciler/src/ReactUpdateQueue.js | 4 +- ...actIncrementalSideEffects-test.internal.js | 2 +- .../__tests__/ReactSuspense-test.internal.js | 4 +- .../src/ReactTestRenderer.js | 30 ++++ .../ReactStrictMode-test.internal.js | 4 +- packages/shared/ReactTypeOfWork.js | 26 +-- packages/shared/getComponentName.js | 7 + 22 files changed, 469 insertions(+), 186 deletions(-) create mode 100644 packages/react-reconciler/src/ReactFiberLazyComponent.js diff --git a/packages/react-dom/src/test-utils/ReactTestUtils.js b/packages/react-dom/src/test-utils/ReactTestUtils.js index 3c01e3f4f9c..2d31136eb4d 100644 --- a/packages/react-dom/src/test-utils/ReactTestUtils.js +++ b/packages/react-dom/src/test-utils/ReactTestUtils.js @@ -11,7 +11,9 @@ import {findCurrentFiberUsingSlowPath} from 'react-reconciler/reflection'; import * as ReactInstanceMap from 'shared/ReactInstanceMap'; import { ClassComponent, + ClassComponentLazy, FunctionalComponent, + FunctionalComponentLazy, HostComponent, HostText, } from 'shared/ReactTypeOfWork'; @@ -81,7 +83,9 @@ function findAllInRenderedFiberTreeInternal(fiber, test) { node.tag === HostComponent || node.tag === HostText || node.tag === ClassComponent || - node.tag === FunctionalComponent + node.tag === ClassComponentLazy || + node.tag === FunctionalComponent || + node.tag === FunctionalComponentLazy ) { const publicInst = node.stateNode; if (test(publicInst)) { diff --git a/packages/react-reconciler/src/ReactChildFiber.js b/packages/react-reconciler/src/ReactChildFiber.js index 90195e8729b..7f7c610a715 100644 --- a/packages/react-reconciler/src/ReactChildFiber.js +++ b/packages/react-reconciler/src/ReactChildFiber.js @@ -23,6 +23,7 @@ import { import { FunctionalComponent, ClassComponent, + ClassComponentLazy, HostText, HostPortal, Fragment, @@ -117,7 +118,7 @@ function coerceRef( if (!didWarnAboutStringRefInStrictMode[componentName]) { warningWithoutStack( false, - 'A string ref, "%s", has been found within a strict mode tree. ' + + 'A string ref, "%s", has been found within a strict mode tree. ' + 'String refs are a source of potential bugs and should be avoided. ' + 'We recommend using createRef() instead.' + '\n%s' + @@ -137,7 +138,8 @@ function coerceRef( if (owner) { const ownerFiber = ((owner: any): Fiber); invariant( - ownerFiber.tag === ClassComponent, + ownerFiber.tag === ClassComponent || + ownerFiber.tag === ClassComponentLazy, 'Stateless function components cannot have refs.', ); inst = ownerFiber.stateNode; @@ -377,14 +379,7 @@ function ChildReconciler(shouldTrackSideEffects) { element: ReactElement, expirationTime: ExpirationTime, ): Fiber { - const elementType = element.type; - if ( - current !== null && - (current.type === elementType || - (elementType !== null && - elementType !== undefined && - current.type === elementType._reactResult)) - ) { + if (current !== null && current.type === element.type) { // Move based on index const existing = useFiber(current, element.props, expirationTime); existing.ref = coerceRef(returnFiber, current, element); @@ -1118,26 +1113,21 @@ function ChildReconciler(shouldTrackSideEffects) { element: ReactElement, expirationTime: ExpirationTime, ): Fiber { - const elementType = element.type; const key = element.key; let child = currentFirstChild; while (child !== null) { // TODO: If key === null and child.key === null, then this only applies to // the first item in the list. if (child.key === key) { - const childType = child.type; if ( child.tag === Fragment - ? elementType === REACT_FRAGMENT_TYPE - : childType === elementType || - (elementType !== null && - elementType !== undefined && - childType === elementType._reactResult) + ? element.type === REACT_FRAGMENT_TYPE + : child.type === element.type ) { deleteRemainingChildren(returnFiber, child.sibling); const existing = useFiber( child, - elementType === REACT_FRAGMENT_TYPE + element.type === REACT_FRAGMENT_TYPE ? element.props.children : element.props, expirationTime, @@ -1159,7 +1149,7 @@ function ChildReconciler(shouldTrackSideEffects) { child = child.sibling; } - if (elementType === REACT_FRAGMENT_TYPE) { + if (element.type === REACT_FRAGMENT_TYPE) { const created = createFiberFromFragment( element.props.children, returnFiber.mode, @@ -1319,7 +1309,8 @@ function ChildReconciler(shouldTrackSideEffects) { // component, throw an error. If Fiber return types are disabled, // we already threw above. switch (returnFiber.tag) { - case ClassComponent: { + case ClassComponent: + case ClassComponentLazy: { if (__DEV__) { const instance = returnFiber.stateNode; if (instance.render._isMockFunction) { diff --git a/packages/react-reconciler/src/ReactCurrentFiber.js b/packages/react-reconciler/src/ReactCurrentFiber.js index cd72ffc5209..11bd2b4f51d 100644 --- a/packages/react-reconciler/src/ReactCurrentFiber.js +++ b/packages/react-reconciler/src/ReactCurrentFiber.js @@ -11,7 +11,9 @@ import ReactSharedInternals from 'shared/ReactSharedInternals'; import { IndeterminateComponent, FunctionalComponent, + FunctionalComponentLazy, ClassComponent, + ClassComponentLazy, HostComponent, Mode, } from 'shared/ReactTypeOfWork'; @@ -28,7 +30,9 @@ function describeFiber(fiber: Fiber): string { switch (fiber.tag) { case IndeterminateComponent: case FunctionalComponent: + case FunctionalComponentLazy: case ClassComponent: + case ClassComponentLazy: case HostComponent: case Mode: const owner = fiber._debugOwner; diff --git a/packages/react-reconciler/src/ReactFiber.js b/packages/react-reconciler/src/ReactFiber.js index fcfe933081f..32d9cd3e9e0 100644 --- a/packages/react-reconciler/src/ReactFiber.js +++ b/packages/react-reconciler/src/ReactFiber.js @@ -278,7 +278,7 @@ const createFiber = function( return new FiberNode(tag, pendingProps, key, mode); }; -function shouldConstruct(Component) { +export function shouldConstruct(Component: Function) { return !!(Component.prototype && Component.prototype.isReactComponent); } @@ -392,10 +392,6 @@ export function createFiberFromElement( const key = element.key; const pendingProps = element.props; - if (type !== null && type !== undefined && typeof type.then === 'function') { - type = resolveThenableType(type); - } - let fiberTag; if (typeof type === 'function') { fiberTag = shouldConstruct(type) ? ClassComponent : IndeterminateComponent; @@ -441,53 +437,6 @@ export function createFiberFromElement( return fiber; } -export const Pending = 0; -const Resolved = 1; -const Rejected = 2; - -function UnresolvedComponent(thenable) { - thenable.then( - resolvedValue => { - if (thenable._reactStatus === Pending) { - thenable._reactStatus = Resolved; - // If the default value is not empty, assume it's the result of - // an async import() and use that. Otherwise, use resolved value. - const defaultExport = resolvedValue.default; - thenable._reactResult = - defaultExport !== null && defaultExport !== undefined - ? defaultExport - : resolvedValue; - } - }, - error => { - if (thenable._reactStatus === Pending) { - thenable._reactStatus = Rejected; - thenable._reactResult = error; - } - }, - ); - throw thenable; -} - -function RejectedComponent(error) { - throw error; -} - -function resolveThenableType(type) { - const result = type._reactResult; - switch (type._reactStatus) { - case Pending: - return UnresolvedComponent.bind(null, type); - case Resolved: - return result; - case Rejected: - return RejectedComponent.bind(null, result); - default: - type._reactStatus = Pending; - return UnresolvedComponent.bind(null, type); - } -} - function getFiberTagFromObjectType(type, owner): TypeOfWork { const $$typeof = typeof type === 'object' && type !== null ? type.$$typeof : null; @@ -501,6 +450,13 @@ function getFiberTagFromObjectType(type, owner): TypeOfWork { case REACT_FORWARD_REF_TYPE: return ForwardRef; default: { + if ( + type !== undefined && + type !== null && + typeof type.then === 'function' + ) { + return IndeterminateComponent; + } let info = ''; if (__DEV__) { if ( diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.js b/packages/react-reconciler/src/ReactFiberBeginWork.js index 84a835a9e00..61308bc1548 100644 --- a/packages/react-reconciler/src/ReactFiberBeginWork.js +++ b/packages/react-reconciler/src/ReactFiberBeginWork.js @@ -16,12 +16,15 @@ import checkPropTypes from 'prop-types/checkPropTypes'; import { IndeterminateComponent, FunctionalComponent, + FunctionalComponentLazy, ClassComponent, + ClassComponentLazy, HostRoot, HostComponent, HostText, HostPortal, ForwardRef, + ForwardRefLazy, Fragment, Mode, ContextProvider, @@ -96,6 +99,8 @@ import { resumeMountClassInstance, updateClassInstance, } from './ReactFiberClassComponent'; +import {readLazyComponentType} from './ReactFiberLazyComponent'; +import {shouldConstruct} from './ReactFiber'; const ReactCurrentOwner = ReactSharedInternals.ReactCurrentOwner; @@ -145,9 +150,10 @@ export function reconcileChildren( function updateForwardRef( current: Fiber | null, workInProgress: Fiber, + type: any, renderExpirationTime: ExpirationTime, ) { - const render = workInProgress.type.render; + const render = type.render; const nextProps = workInProgress.pendingProps; const ref = workInProgress.ref; if (hasLegacyContextChanged()) { @@ -250,9 +256,9 @@ function markRef(current: Fiber | null, workInProgress: Fiber) { function updateFunctionalComponent( current, workInProgress, + Component, renderExpirationTime, ) { - const fn = workInProgress.type; const nextProps = workInProgress.pendingProps; const unmaskedContext = getUnmaskedContext(workInProgress); const context = getMaskedContext(workInProgress, unmaskedContext); @@ -262,10 +268,10 @@ function updateFunctionalComponent( if (__DEV__) { ReactCurrentOwner.current = workInProgress; ReactCurrentFiber.setCurrentPhase('render'); - nextChildren = fn(nextProps, context); + nextChildren = Component(nextProps, context); ReactCurrentFiber.setCurrentPhase(null); } else { - nextChildren = fn(nextProps, context); + nextChildren = Component(nextProps, context); } // React DevTools reads this flag. @@ -283,6 +289,7 @@ function updateFunctionalComponent( function updateClassComponent( current: Fiber | null, workInProgress: Fiber, + Component: any, renderExpirationTime: ExpirationTime, ) { // Push context providers early to prevent context stack mismatches. @@ -297,16 +304,18 @@ function updateClassComponent( // In the initial pass we might need to construct the instance. constructClassInstance( workInProgress, + Component, workInProgress.pendingProps, renderExpirationTime, ); - mountClassInstance(workInProgress, renderExpirationTime); + mountClassInstance(workInProgress, Component, renderExpirationTime); shouldUpdate = true; } else { // In a resume, we'll already have an instance we can reuse. shouldUpdate = resumeMountClassInstance( workInProgress, + Component, renderExpirationTime, ); } @@ -314,12 +323,14 @@ function updateClassComponent( shouldUpdate = updateClassInstance( current, workInProgress, + Component, renderExpirationTime, ); } return finishClassComponent( current, workInProgress, + Component, shouldUpdate, hasContext, renderExpirationTime, @@ -329,6 +340,7 @@ function updateClassComponent( function finishClassComponent( current: Fiber | null, workInProgress: Fiber, + Component: any, shouldUpdate: boolean, hasContext: boolean, renderExpirationTime: ExpirationTime, @@ -341,7 +353,7 @@ function finishClassComponent( if (!shouldUpdate && !didCaptureError) { // Context providers should defer to sCU for rendering if (hasContext) { - invalidateContextProvider(workInProgress, false); + invalidateContextProvider(workInProgress, Component, false); } return bailoutOnAlreadyFinishedWork( @@ -351,7 +363,6 @@ function finishClassComponent( ); } - const ctor = workInProgress.type; const instance = workInProgress.stateNode; // Rerender @@ -360,7 +371,7 @@ function finishClassComponent( if ( didCaptureError && (!enableGetDerivedStateFromCatch || - typeof ctor.getDerivedStateFromCatch !== 'function') + typeof Component.getDerivedStateFromCatch !== 'function') ) { // If we captured an error, but getDerivedStateFrom catch is not defined, // unmount all the children. componentDidCatch will schedule an update to @@ -413,7 +424,7 @@ function finishClassComponent( // The context might have changed so we need to recalculate it. if (hasContext) { - invalidateContextProvider(workInProgress, true); + invalidateContextProvider(workInProgress, Component, true); } return workInProgress.child; @@ -571,6 +582,7 @@ function updateHostText(current, workInProgress) { function mountIndeterminateComponent( current, workInProgress, + Component, renderExpirationTime, ) { invariant( @@ -578,7 +590,50 @@ function mountIndeterminateComponent( 'An indeterminate component should never have mounted. This error is ' + 'likely caused by a bug in React. Please file an issue.', ); - const fn = workInProgress.type; + + if ( + Component !== null && + Component !== undefined && + typeof Component.then === 'function' + ) { + Component = readLazyComponentType(Component); + if (typeof Component === 'function') { + if (shouldConstruct(Component)) { + workInProgress.tag = ClassComponentLazy; + return updateClassComponent( + current, + workInProgress, + Component, + renderExpirationTime, + ); + } else { + const child = mountIndeterminateComponent( + current, + workInProgress, + Component, + renderExpirationTime, + ); + workInProgress.tag = + workInProgress.tag === FunctionalComponent + ? FunctionalComponentLazy + : ClassComponentLazy; + return child; + } + } else if ( + Component !== undefined && + Component !== null && + Component.$$typeof + ) { + workInProgress.tag = ForwardRefLazy; + return updateForwardRef( + current, + workInProgress, + Component, + renderExpirationTime, + ); + } + } + const props = workInProgress.pendingProps; const unmaskedContext = getUnmaskedContext(workInProgress); const context = getMaskedContext(workInProgress, unmaskedContext); @@ -588,8 +643,11 @@ function mountIndeterminateComponent( let value; if (__DEV__) { - if (fn.prototype && typeof fn.prototype.render === 'function') { - const componentName = getComponentName(fn) || 'Unknown'; + if ( + Component.prototype && + typeof Component.prototype.render === 'function' + ) { + const componentName = getComponentName(Component) || 'Unknown'; if (!didWarnAboutBadClass[componentName]) { warningWithoutStack( @@ -608,9 +666,9 @@ function mountIndeterminateComponent( } ReactCurrentOwner.current = workInProgress; - value = fn(props, context); + value = Component(props, context); } else { - value = fn(props, context); + value = Component(props, context); } // React DevTools reads this flag. workInProgress.effectTag |= PerformedWork; @@ -621,8 +679,6 @@ function mountIndeterminateComponent( typeof value.render === 'function' && value.$$typeof === undefined ) { - const Component = workInProgress.type; - // Proceed under the assumption that this is a class instance workInProgress.tag = ClassComponent; @@ -638,16 +694,18 @@ function mountIndeterminateComponent( if (typeof getDerivedStateFromProps === 'function') { applyDerivedStateFromProps( workInProgress, + Component, getDerivedStateFromProps, props, ); } adoptClassInstance(workInProgress, value); - mountClassInstance(workInProgress, renderExpirationTime); + mountClassInstance(workInProgress, Component, renderExpirationTime); return finishClassComponent( current, workInProgress, + Component, true, hasContext, renderExpirationTime, @@ -656,8 +714,6 @@ function mountIndeterminateComponent( // Proceed under the assumption that this is a functional component workInProgress.tag = FunctionalComponent; if (__DEV__) { - const Component = workInProgress.type; - if (Component) { warningWithoutStack( !Component.childContextTypes, @@ -688,8 +744,8 @@ function mountIndeterminateComponent( } } - if (typeof fn.getDerivedStateFromProps === 'function') { - const componentName = getComponentName(fn) || 'Unknown'; + if (typeof Component.getDerivedStateFromProps === 'function') { + const componentName = getComponentName(Component) || 'Unknown'; if (!didWarnAboutGetDerivedStateOnFunctionalComponent[componentName]) { warningWithoutStack( @@ -995,6 +1051,7 @@ function beginWork( pushHostContext(workInProgress); break; case ClassComponent: + case ClassComponentLazy: pushLegacyContextProvider(workInProgress); break; case HostPortal: @@ -1025,24 +1082,53 @@ function beginWork( workInProgress.expirationTime = NoWork; switch (workInProgress.tag) { - case IndeterminateComponent: + case IndeterminateComponent: { + const Component = workInProgress.type; return mountIndeterminateComponent( current, workInProgress, + Component, + renderExpirationTime, + ); + } + case FunctionalComponent: { + const Component = workInProgress.type; + return updateFunctionalComponent( + current, + workInProgress, + Component, renderExpirationTime, ); - case FunctionalComponent: + } + case FunctionalComponentLazy: { + const thenable = workInProgress.type; + const Component = (thenable._reactResult: any); return updateFunctionalComponent( current, workInProgress, + Component, renderExpirationTime, ); - case ClassComponent: + } + case ClassComponent: { + const Component = workInProgress.type; return updateClassComponent( current, workInProgress, + Component, renderExpirationTime, ); + } + case ClassComponentLazy: { + const thenable = workInProgress.type; + const Component = (thenable._reactResult: any); + return updateClassComponent( + current, + workInProgress, + Component, + renderExpirationTime, + ); + } case HostRoot: return updateHostRoot(current, workInProgress, renderExpirationTime); case HostComponent: @@ -1061,8 +1147,24 @@ function beginWork( workInProgress, renderExpirationTime, ); - case ForwardRef: - return updateForwardRef(current, workInProgress, renderExpirationTime); + case ForwardRef: { + const type = workInProgress.type; + return updateForwardRef( + current, + workInProgress, + type, + renderExpirationTime, + ); + } + case ForwardRefLazy: + const thenable = workInProgress.type; + const Component = (thenable._reactResult: any); + return updateForwardRef( + current, + workInProgress, + Component, + renderExpirationTime, + ); case Fragment: return updateFragment(current, workInProgress, renderExpirationTime); case Mode: diff --git a/packages/react-reconciler/src/ReactFiberClassComponent.js b/packages/react-reconciler/src/ReactFiberClassComponent.js index ba7b9d2e0ed..11e42bb91f3 100644 --- a/packages/react-reconciler/src/ReactFiberClassComponent.js +++ b/packages/react-reconciler/src/ReactFiberClassComponent.js @@ -41,7 +41,6 @@ import { cacheContext, getMaskedContext, getUnmaskedContext, - isContextConsumer, hasContextChanged, emptyContextObject, } from './ReactFiberContext'; @@ -131,6 +130,7 @@ if (__DEV__) { export function applyDerivedStateFromProps( workInProgress: Fiber, + ctor: any, getDerivedStateFromProps: (props: any, state: any) => any, nextProps: any, ) { @@ -150,7 +150,7 @@ export function applyDerivedStateFromProps( const partialState = getDerivedStateFromProps(nextProps, prevState); if (__DEV__) { - warnOnUndefinedDerivedState(workInProgress.type, partialState); + warnOnUndefinedDerivedState(ctor, partialState); } // Merge the partial state and the previous state. const memoizedState = @@ -227,6 +227,7 @@ const classComponentUpdater = { function checkShouldComponentUpdate( workInProgress, + ctor, oldProps, newProps, oldState, @@ -234,7 +235,6 @@ function checkShouldComponentUpdate( nextLegacyContext, ) { const instance = workInProgress.stateNode; - const ctor = workInProgress.type; if (typeof instance.shouldComponentUpdate === 'function') { startPhaseTimer(workInProgress, 'shouldComponentUpdate'); const shouldUpdate = instance.shouldComponentUpdate( @@ -265,15 +265,14 @@ function checkShouldComponentUpdate( return true; } -function checkClassInstance(workInProgress: Fiber) { +function checkClassInstance(workInProgress: Fiber, ctor: any) { const instance = workInProgress.stateNode; - const type = workInProgress.type; if (__DEV__) { - const name = getComponentName(type) || 'Component'; + const name = getComponentName(ctor) || 'Component'; const renderPresent = instance.render; if (!renderPresent) { - if (type.prototype && typeof type.prototype.render === 'function') { + if (ctor.prototype && typeof ctor.prototype.render === 'function') { warningWithoutStack( false, '%s(...): No `render` method found on the returned component ' + @@ -336,8 +335,8 @@ function checkClassInstance(workInProgress: Fiber) { name, ); if ( - type.prototype && - type.prototype.isPureReactComponent && + ctor.prototype && + ctor.prototype.isPureReactComponent && typeof instance.shouldComponentUpdate !== 'undefined' ) { warningWithoutStack( @@ -345,7 +344,7 @@ function checkClassInstance(workInProgress: Fiber) { '%s has a method called shouldComponentUpdate(). ' + 'shouldComponentUpdate should not be used when extending React.PureComponent. ' + 'Please extend React.Component if shouldComponentUpdate is used.', - getComponentName(type) || 'A pure component', + getComponentName(ctor) || 'A pure component', ); } const noComponentDidUnmount = @@ -404,14 +403,14 @@ function checkClassInstance(workInProgress: Fiber) { if ( typeof instance.getSnapshotBeforeUpdate === 'function' && typeof instance.componentDidUpdate !== 'function' && - !didWarnAboutGetSnapshotBeforeUpdateWithoutDidUpdate.has(type) + !didWarnAboutGetSnapshotBeforeUpdateWithoutDidUpdate.has(ctor) ) { - didWarnAboutGetSnapshotBeforeUpdateWithoutDidUpdate.add(type); + didWarnAboutGetSnapshotBeforeUpdateWithoutDidUpdate.add(ctor); warningWithoutStack( false, '%s: getSnapshotBeforeUpdate() should be used with componentDidUpdate(). ' + 'This component defines getSnapshotBeforeUpdate() only.', - getComponentName(type), + getComponentName(ctor), ); } @@ -432,7 +431,7 @@ function checkClassInstance(workInProgress: Fiber) { name, ); const noStaticGetSnapshotBeforeUpdate = - typeof type.getSnapshotBeforeUpdate !== 'function'; + typeof ctor.getSnapshotBeforeUpdate !== 'function'; warningWithoutStack( noStaticGetSnapshotBeforeUpdate, '%s: getSnapshotBeforeUpdate() is defined as a static method ' + @@ -449,7 +448,7 @@ function checkClassInstance(workInProgress: Fiber) { } if (typeof instance.getChildContext === 'function') { warningWithoutStack( - typeof type.childContextTypes === 'object', + typeof ctor.childContextTypes === 'object', '%s.getChildContext(): childContextTypes must be defined in order to ' + 'use getChildContext().', name, @@ -470,13 +469,14 @@ function adoptClassInstance(workInProgress: Fiber, instance: any): void { function constructClassInstance( workInProgress: Fiber, + ctor: any, props: any, renderExpirationTime: ExpirationTime, ): any { - const ctor = workInProgress.type; const unmaskedContext = getUnmaskedContext(workInProgress); - const needsContext = isContextConsumer(workInProgress); - const context = needsContext + const contextTypes = ctor.contextTypes; + const isContextConsumer = contextTypes !== null && contextTypes !== undefined; + const context = isContextConsumer ? getMaskedContext(workInProgress, unmaskedContext) : emptyContextObject; @@ -585,7 +585,7 @@ function constructClassInstance( // Cache unmasked context so we can avoid recreating masked context unless necessary. // ReactFiberContext usually updates this cache but can't for newly-created instances. - if (needsContext) { + if (isContextConsumer) { cacheContext(workInProgress, unmaskedContext, context); } @@ -657,12 +657,11 @@ function callComponentWillReceiveProps( // Invokes the mount life-cycles on a previously never rendered instance. function mountClassInstance( workInProgress: Fiber, + ctor: any, renderExpirationTime: ExpirationTime, ): void { - const ctor = workInProgress.type; - if (__DEV__) { - checkClassInstance(workInProgress); + checkClassInstance(workInProgress, ctor); } const instance = workInProgress.stateNode; @@ -709,7 +708,12 @@ function mountClassInstance( const getDerivedStateFromProps = ctor.getDerivedStateFromProps; if (typeof getDerivedStateFromProps === 'function') { - applyDerivedStateFromProps(workInProgress, getDerivedStateFromProps, props); + applyDerivedStateFromProps( + workInProgress, + ctor, + getDerivedStateFromProps, + props, + ); instance.state = workInProgress.memoizedState; } @@ -744,9 +748,9 @@ function mountClassInstance( function resumeMountClassInstance( workInProgress: Fiber, + ctor: any, renderExpirationTime: ExpirationTime, ): boolean { - const ctor = workInProgress.type; const instance = workInProgress.stateNode; const oldProps = workInProgress.memoizedProps; @@ -818,6 +822,7 @@ function resumeMountClassInstance( if (typeof getDerivedStateFromProps === 'function') { applyDerivedStateFromProps( workInProgress, + ctor, getDerivedStateFromProps, newProps, ); @@ -828,6 +833,7 @@ function resumeMountClassInstance( checkHasForceUpdateAfterProcessing() || checkShouldComponentUpdate( workInProgress, + ctor, oldProps, newProps, oldState, @@ -881,9 +887,9 @@ function resumeMountClassInstance( function updateClassInstance( current: Fiber, workInProgress: Fiber, + ctor: any, renderExpirationTime: ExpirationTime, ): boolean { - const ctor = workInProgress.type; const instance = workInProgress.stateNode; const oldProps = workInProgress.memoizedProps; @@ -969,6 +975,7 @@ function updateClassInstance( if (typeof getDerivedStateFromProps === 'function') { applyDerivedStateFromProps( workInProgress, + ctor, getDerivedStateFromProps, newProps, ); @@ -979,6 +986,7 @@ function updateClassInstance( checkHasForceUpdateAfterProcessing() || checkShouldComponentUpdate( workInProgress, + ctor, oldProps, newProps, oldState, diff --git a/packages/react-reconciler/src/ReactFiberCommitWork.js b/packages/react-reconciler/src/ReactFiberCommitWork.js index 3ccd0c451cc..c5b2a6b4306 100644 --- a/packages/react-reconciler/src/ReactFiberCommitWork.js +++ b/packages/react-reconciler/src/ReactFiberCommitWork.js @@ -22,6 +22,7 @@ import type {CapturedValue, CapturedError} from './ReactCapturedValue'; import {enableProfilerTimer, enableSuspense} from 'shared/ReactFeatureFlags'; import { ClassComponent, + ClassComponentLazy, HostRoot, HostComponent, HostText, @@ -179,7 +180,8 @@ function commitBeforeMutationLifeCycles( finishedWork: Fiber, ): void { switch (finishedWork.tag) { - case ClassComponent: { + case ClassComponent: + case ClassComponentLazy: { if (finishedWork.effectTag & Snapshot) { if (current !== null) { const prevProps = current.memoizedProps; @@ -235,7 +237,8 @@ function commitLifeCycles( committedExpirationTime: ExpirationTime, ): void { switch (finishedWork.tag) { - case ClassComponent: { + case ClassComponent: + case ClassComponentLazy: { const instance = finishedWork.stateNode; if (finishedWork.effectTag & Update) { if (current === null) { @@ -281,6 +284,7 @@ function commitLifeCycles( instance = getPublicInstance(finishedWork.child.stateNode); break; case ClassComponent: + case ClassComponentLazy: instance = finishedWork.child.stateNode; break; } @@ -400,7 +404,8 @@ function commitUnmount(current: Fiber): void { onCommitUnmount(current); switch (current.tag) { - case ClassComponent: { + case ClassComponent: + case ClassComponentLazy: { safelyDetachRef(current); const instance = current.stateNode; if (typeof instance.componentWillUnmount === 'function') { @@ -493,7 +498,8 @@ function commitContainer(finishedWork: Fiber) { } switch (finishedWork.tag) { - case ClassComponent: { + case ClassComponent: + case ClassComponentLazy: { return; } case HostComponent: { @@ -778,7 +784,8 @@ function commitWork(current: Fiber | null, finishedWork: Fiber): void { } switch (finishedWork.tag) { - case ClassComponent: { + case ClassComponent: + case ClassComponentLazy: { return; } case HostComponent: { diff --git a/packages/react-reconciler/src/ReactFiberCompleteWork.js b/packages/react-reconciler/src/ReactFiberCompleteWork.js index fae0ac77d4a..8466e95881f 100644 --- a/packages/react-reconciler/src/ReactFiberCompleteWork.js +++ b/packages/react-reconciler/src/ReactFiberCompleteWork.js @@ -23,7 +23,9 @@ import type { import { IndeterminateComponent, FunctionalComponent, + FunctionalComponentLazy, ClassComponent, + ClassComponentLazy, HostRoot, HostComponent, HostText, @@ -35,6 +37,7 @@ import { Mode, Profiler, PlaceholderComponent, + ForwardRefLazy, } from 'shared/ReactTypeOfWork'; import {Placement, Ref, Update} from 'shared/ReactTypeOfSideEffect'; import invariant from 'shared/invariant'; @@ -59,7 +62,7 @@ import { popHostContainer, } from './ReactFiberHostContext'; import { - popContextProvider as popLegacyContextProvider, + popContext as popLegacyContext, popTopLevelContextObject as popTopLevelLegacyContextObject, } from './ReactFiberContext'; import {popProvider} from './ReactFiberNewContext'; @@ -313,10 +316,23 @@ function completeWork( switch (workInProgress.tag) { case FunctionalComponent: + case FunctionalComponentLazy: break; case ClassComponent: { // We are leaving this subtree, so pop context if any. - popLegacyContextProvider(workInProgress); + const childContextTypes = workInProgress.type.childContextTypes; + if (childContextTypes !== null && childContextTypes !== undefined) { + popLegacyContext(workInProgress); + } + break; + } + case ClassComponentLazy: { + // We are leaving this subtree, so pop context if any. + const childContextTypes = + workInProgress.type._reactResult.childContextTypes; + if (childContextTypes !== null && childContextTypes !== undefined) { + popLegacyContext(workInProgress); + } break; } case HostRoot: { @@ -479,6 +495,7 @@ function completeWork( break; } case ForwardRef: + case ForwardRefLazy: break; case PlaceholderComponent: break; diff --git a/packages/react-reconciler/src/ReactFiberContext.js b/packages/react-reconciler/src/ReactFiberContext.js index 27c36b0d9d7..e2ef3421ca2 100644 --- a/packages/react-reconciler/src/ReactFiberContext.js +++ b/packages/react-reconciler/src/ReactFiberContext.js @@ -11,7 +11,11 @@ import type {Fiber} from './ReactFiber'; import type {StackCursor} from './ReactFiberStack'; import {isFiberMounted} from 'react-reconciler/reflection'; -import {ClassComponent, HostRoot} from 'shared/ReactTypeOfWork'; +import { + ClassComponent, + HostRoot, + ClassComponentLazy, +} from 'shared/ReactTypeOfWork'; import getComponentName from 'shared/getComponentName'; import invariant from 'shared/invariant'; import warningWithoutStack from 'shared/warningWithoutStack'; @@ -20,6 +24,7 @@ import checkPropTypes from 'prop-types/checkPropTypes'; import * as ReactCurrentFiber from './ReactCurrentFiber'; import {startPhaseTimer, stopPhaseTimer} from './ReactDebugFiberPerf'; import {createCursor, push, pop} from './ReactFiberStack'; +import {getLazyComponentTypeIfResolved} from 'react-reconciler/src/ReactFiberLazyComponent'; let warnedAboutMissingGetChildContext; @@ -113,19 +118,23 @@ function hasContextChanged(): boolean { return didPerformWorkStackCursor.current; } -function isContextConsumer(fiber: Fiber): boolean { - return fiber.tag === ClassComponent && fiber.type.contextTypes != null; -} - function isContextProvider(fiber: Fiber): boolean { - return fiber.tag === ClassComponent && fiber.type.childContextTypes != null; -} - -function popContextProvider(fiber: Fiber): void { - if (!isContextProvider(fiber)) { - return; + let type; + if (fiber.tag === ClassComponent) { + type = fiber.type; + } else if (fiber.tag === ClassComponentLazy) { + type = getLazyComponentTypeIfResolved(fiber.type); + if (type === null) { + return false; + } + } else { + return false; } + const childContextTypes = type.childContextTypes; + return childContextTypes !== null && childContextTypes !== undefined; +} +function popContext(fiber: Fiber): void { pop(didPerformWorkStackCursor, fiber); pop(contextStackCursor, fiber); } @@ -150,9 +159,12 @@ function pushTopLevelContextObject( push(didPerformWorkStackCursor, didChange, fiber); } -function processChildContext(fiber: Fiber, parentContext: Object): Object { +function processChildContext( + fiber: Fiber, + type: any, + parentContext: Object, +): Object { const instance = fiber.stateNode; - const type = fiber.type; const childContextTypes = type.childContextTypes; // TODO (bvaughn) Replace this behavior with an invariant() in the future. @@ -241,6 +253,7 @@ function pushContextProvider(workInProgress: Fiber): boolean { function invalidateContextProvider( workInProgress: Fiber, + type: any, didChange: boolean, ): void { const instance = workInProgress.stateNode; @@ -254,7 +267,11 @@ function invalidateContextProvider( // Merge parent and own context. // Skip this if we're not updating due to sCU. // This avoids unnecessarily recomputing memoized values. - const mergedContext = processChildContext(workInProgress, previousContext); + const mergedContext = processChildContext( + workInProgress, + type, + previousContext, + ); instance.__reactInternalMemoizedMergedChildContext = mergedContext; // Replace the old (or empty) context with the new one. @@ -274,7 +291,8 @@ function findCurrentUnmaskedContext(fiber: Fiber): Object { // Currently this is only used with renderSubtreeIntoContainer; not sure if it // makes sense elsewhere invariant( - isFiberMounted(fiber) && fiber.tag === ClassComponent, + isFiberMounted(fiber) && + (fiber.tag === ClassComponent || fiber.tag === ClassComponentLazy), 'Expected subtree parent to be a mounted class component. ' + 'This error is likely caused by a bug in React. Please file an issue.', ); @@ -300,9 +318,7 @@ export { cacheContext, getMaskedContext, hasContextChanged, - isContextConsumer, - isContextProvider, - popContextProvider, + popContext, popTopLevelContextObject, pushTopLevelContextObject, processChildContext, diff --git a/packages/react-reconciler/src/ReactFiberLazyComponent.js b/packages/react-reconciler/src/ReactFiberLazyComponent.js new file mode 100644 index 00000000000..16b70e7ee79 --- /dev/null +++ b/packages/react-reconciler/src/ReactFiberLazyComponent.js @@ -0,0 +1,61 @@ +/** + * 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. + * + * @flow + */ + +type Thenable = { + then(resolve: (T) => mixed, reject: (mixed) => mixed): mixed, + _reactStatus?: 0 | 1 | 2, + _reactResult: any, +}; + +export const Pending = 0; +export const Resolved = 1; +export const Rejected = 2; + +export function getLazyComponentTypeIfResolved( + thenable: Thenable, +): T | null { + return thenable._reactStatus === Resolved ? thenable._reactResult : null; +} + +export function readLazyComponentType(thenable: Thenable): T { + const status = thenable._reactStatus; + switch (status) { + case Resolved: + const Component: T = thenable._reactResult; + return Component; + case Rejected: + throw thenable._reactResult; + case Pending: + throw thenable; + default: { + thenable._reactStatus = Pending; + thenable.then( + resolvedValue => { + if (thenable._reactStatus === Pending) { + thenable._reactStatus = Resolved; + // If the default value is not empty, assume it's the result of + // an async import() and use that. Otherwise, use resolved value. + const defaultExport = (resolvedValue: any).default; + thenable._reactResult = + defaultExport !== null && defaultExport !== undefined + ? defaultExport + : resolvedValue; + } + }, + error => { + if (thenable._reactStatus === Pending) { + thenable._reactStatus = Rejected; + thenable._reactResult = error; + } + }, + ); + throw thenable; + } + } +} diff --git a/packages/react-reconciler/src/ReactFiberNewContext.js b/packages/react-reconciler/src/ReactFiberNewContext.js index 39c190431f8..06583cdd80b 100644 --- a/packages/react-reconciler/src/ReactFiberNewContext.js +++ b/packages/react-reconciler/src/ReactFiberNewContext.js @@ -23,7 +23,11 @@ import {isPrimaryRenderer} from './ReactFiberHostConfig'; import {createCursor, push, pop} from './ReactFiberStack'; import maxSigned31BitInt from './maxSigned31BitInt'; import {NoWork} from './ReactFiberExpirationTime'; -import {ContextProvider, ClassComponent} from 'shared/ReactTypeOfWork'; +import { + ContextProvider, + ClassComponent, + ClassComponentLazy, +} from 'shared/ReactTypeOfWork'; import invariant from 'shared/invariant'; import warning from 'shared/warning'; @@ -159,7 +163,10 @@ export function propagateContextChange( ) { // Match! Schedule an update on this fiber. - if (fiber.tag === ClassComponent) { + if ( + fiber.tag === ClassComponent || + fiber.tag === ClassComponentLazy + ) { // Schedule a force update on the work-in-progress. const update = createUpdate(renderExpirationTime); update.tag = ForceUpdate; diff --git a/packages/react-reconciler/src/ReactFiberReconciler.js b/packages/react-reconciler/src/ReactFiberReconciler.js index 140b75e3aa7..6e262b0a869 100644 --- a/packages/react-reconciler/src/ReactFiberReconciler.js +++ b/packages/react-reconciler/src/ReactFiberReconciler.js @@ -23,7 +23,11 @@ import { findCurrentHostFiberWithNoPortals, } from 'react-reconciler/reflection'; import * as ReactInstanceMap from 'shared/ReactInstanceMap'; -import {HostComponent} from 'shared/ReactTypeOfWork'; +import { + HostComponent, + ClassComponent, + ClassComponentLazy, +} from 'shared/ReactTypeOfWork'; import getComponentName from 'shared/getComponentName'; import invariant from 'shared/invariant'; import warningWithoutStack from 'shared/warningWithoutStack'; @@ -31,7 +35,6 @@ import warningWithoutStack from 'shared/warningWithoutStack'; import {getPublicInstance} from './ReactFiberHostConfig'; import { findCurrentUnmaskedContext, - isContextProvider, processChildContext, emptyContextObject, } from './ReactFiberContext'; @@ -56,6 +59,7 @@ import { import {createUpdate, enqueueUpdate} from './ReactUpdateQueue'; import ReactFiberInstrumentation from './ReactFiberInstrumentation'; import * as ReactCurrentFiber from './ReactCurrentFiber'; +import {getLazyComponentTypeIfResolved} from 'react-reconciler/src/ReactFiberLazyComponent'; type OpaqueRoot = FiberRoot; @@ -91,9 +95,24 @@ function getContextForSubtree( const fiber = ReactInstanceMap.get(parentComponent); const parentContext = findCurrentUnmaskedContext(fiber); - return isContextProvider(fiber) - ? processChildContext(fiber, parentContext) - : parentContext; + + if (fiber.tag === ClassComponent) { + const type = fiber.type; + const childContextTypes = type.childContextTypes; + if (childContextTypes !== undefined && childContextTypes !== null) { + return processChildContext(fiber, type, parentContext); + } + } else if (fiber.tag === ClassComponentLazy) { + const type = getLazyComponentTypeIfResolved(fiber.type); + if (type !== null) { + const childContextTypes = type.childContextTypes; + if (childContextTypes !== undefined && childContextTypes !== null) { + return processChildContext(fiber, type, parentContext); + } + } + } + + return parentContext; } function scheduleRootUpdate( diff --git a/packages/react-reconciler/src/ReactFiberScheduler.js b/packages/react-reconciler/src/ReactFiberScheduler.js index b8bad42320a..eba79ed685b 100644 --- a/packages/react-reconciler/src/ReactFiberScheduler.js +++ b/packages/react-reconciler/src/ReactFiberScheduler.js @@ -36,6 +36,7 @@ import { import { HostRoot, ClassComponent, + ClassComponentLazy, HostComponent, ContextProvider, HostPortal, @@ -111,7 +112,7 @@ import {enqueueUpdate, resetCurrentlyProcessingQueue} from './ReactUpdateQueue'; import {createCapturedValue} from './ReactCapturedValue'; import { popTopLevelContextObject as popTopLevelLegacyContextObject, - popContextProvider as popLegacyContextProvider, + popContext as popLegacyContext, } from './ReactFiberContext'; import {popProvider, resetContextDependences} from './ReactFiberNewContext'; import {popHostContext, popHostContainer} from './ReactFiberHostContext'; @@ -287,9 +288,21 @@ if (__DEV__ && replayFailedUnitOfWorkWithInvokeGuardedCallback) { case HostComponent: popHostContext(failedUnitOfWork); break; - case ClassComponent: - popLegacyContextProvider(failedUnitOfWork); + case ClassComponent: { + const childContextTypes = failedUnitOfWork.type.childContextTypes; + if (childContextTypes !== null && childContextTypes !== undefined) { + popLegacyContext(failedUnitOfWork); + } + break; + } + case ClassComponentLazy: { + const childContextTypes = + failedUnitOfWork.type._reactResult.childContextTypes; + if (childContextTypes !== null && childContextTypes !== undefined) { + popLegacyContext(failedUnitOfWork); + } break; + } case HostPortal: popHostContainer(failedUnitOfWork); break; @@ -1292,6 +1305,7 @@ function dispatch( while (fiber !== null) { switch (fiber.tag) { case ClassComponent: + case ClassComponentLazy: const ctor = fiber.type; const instance = fiber.stateNode; if ( @@ -1499,7 +1513,7 @@ function scheduleWork(fiber: Fiber, expirationTime: ExpirationTime) { recordScheduleUpdate(); if (__DEV__) { - if (fiber.tag === ClassComponent) { + if (fiber.tag === ClassComponent || fiber.tag === ClassComponentLazy) { const instance = fiber.stateNode; warnAboutInvalidUpdates(instance); } @@ -1507,7 +1521,10 @@ function scheduleWork(fiber: Fiber, expirationTime: ExpirationTime) { const root = scheduleWorkToRoot(fiber, expirationTime); if (root === null) { - if (__DEV__ && fiber.tag === ClassComponent) { + if ( + __DEV__ && + (fiber.tag === ClassComponent || fiber.tag === ClassComponentLazy) + ) { warnAboutUpdateOnUnmounted(fiber); } return; diff --git a/packages/react-reconciler/src/ReactFiberTreeReflection.js b/packages/react-reconciler/src/ReactFiberTreeReflection.js index e3429c55a86..f41fd35604f 100644 --- a/packages/react-reconciler/src/ReactFiberTreeReflection.js +++ b/packages/react-reconciler/src/ReactFiberTreeReflection.js @@ -17,6 +17,7 @@ import ReactSharedInternals from 'shared/ReactSharedInternals'; import getComponentName from 'shared/getComponentName'; import { ClassComponent, + ClassComponentLazy, HostComponent, HostRoot, HostPortal, @@ -66,7 +67,10 @@ export function isFiberMounted(fiber: Fiber): boolean { export function isMounted(component: React$Component): boolean { if (__DEV__) { const owner = (ReactCurrentOwner.current: any); - if (owner !== null && owner.tag === ClassComponent) { + if ( + owner !== null && + (owner.tag === ClassComponent || owner.tag === ClassComponentLazy) + ) { const ownerFiber: Fiber = owner; const instance = ownerFiber.stateNode; warningWithoutStack( diff --git a/packages/react-reconciler/src/ReactFiberUnwindWork.js b/packages/react-reconciler/src/ReactFiberUnwindWork.js index 1fd1d4fe0e8..3f3d8ed99fd 100644 --- a/packages/react-reconciler/src/ReactFiberUnwindWork.js +++ b/packages/react-reconciler/src/ReactFiberUnwindWork.js @@ -18,6 +18,7 @@ import { IndeterminateComponent, FunctionalComponent, ClassComponent, + ClassComponentLazy, HostRoot, HostComponent, HostPortal, @@ -47,7 +48,7 @@ import { import {logError} from './ReactFiberCommitWork'; import {popHostContainer, popHostContext} from './ReactFiberHostContext'; import { - popContextProvider as popLegacyContextProvider, + popContext as popLegacyContext, popTopLevelContextObject as popTopLevelLegacyContextObject, } from './ReactFiberContext'; import {popProvider} from './ReactFiberNewContext'; @@ -249,7 +250,10 @@ function throwException( sourceFiber.tag = FunctionalComponent; } - if (sourceFiber.tag === ClassComponent) { + if ( + sourceFiber.tag === ClassComponent || + sourceFiber.tag === ClassComponentLazy + ) { // We're going to commit this fiber even though it didn't // complete. But we shouldn't call any lifecycle methods or // callbacks. Remove all lifecycle effect tags. @@ -343,6 +347,7 @@ function throwException( return; } case ClassComponent: + case ClassComponentLazy: // Capture and retry const errorInfo = value; const ctor = workInProgress.type; @@ -380,7 +385,24 @@ function unwindWork( ) { switch (workInProgress.tag) { case ClassComponent: { - popLegacyContextProvider(workInProgress); + const childContextTypes = workInProgress.type.childContextTypes; + if (childContextTypes !== null && childContextTypes !== undefined) { + popLegacyContext(workInProgress); + } + const effectTag = workInProgress.effectTag; + if (effectTag & ShouldCapture) { + workInProgress.effectTag = (effectTag & ~ShouldCapture) | DidCapture; + return workInProgress; + } + return null; + } + case ClassComponentLazy: { + const childContextTypes = + workInProgress.type._reactResult.childContextTypes; + if (childContextTypes !== null && childContextTypes !== undefined) { + popLegacyContext(workInProgress); + } + popLegacyContext(workInProgress); const effectTag = workInProgress.effectTag; if (effectTag & ShouldCapture) { workInProgress.effectTag = (effectTag & ~ShouldCapture) | DidCapture; @@ -426,7 +448,18 @@ function unwindWork( function unwindInterruptedWork(interruptedWork: Fiber) { switch (interruptedWork.tag) { case ClassComponent: { - popLegacyContextProvider(interruptedWork); + const childContextTypes = interruptedWork.type.childContextTypes; + if (childContextTypes !== null && childContextTypes !== undefined) { + popLegacyContext(interruptedWork); + } + break; + } + case ClassComponentLazy: { + const childContextTypes = + interruptedWork.type._reactResult.childContextTypes; + if (childContextTypes !== null && childContextTypes !== undefined) { + popLegacyContext(interruptedWork); + } break; } case HostRoot: { diff --git a/packages/react-reconciler/src/ReactUpdateQueue.js b/packages/react-reconciler/src/ReactUpdateQueue.js index f4d2354f2d1..b636b6c05ca 100644 --- a/packages/react-reconciler/src/ReactUpdateQueue.js +++ b/packages/react-reconciler/src/ReactUpdateQueue.js @@ -93,7 +93,7 @@ import { ShouldCapture, DidCapture, } from 'shared/ReactTypeOfSideEffect'; -import {ClassComponent} from 'shared/ReactTypeOfWork'; +import {ClassComponent, ClassComponentLazy} from 'shared/ReactTypeOfWork'; import { debugRenderPhaseSideEffects, @@ -275,7 +275,7 @@ export function enqueueUpdate(fiber: Fiber, update: Update) { if (__DEV__) { if ( - fiber.tag === ClassComponent && + (fiber.tag === ClassComponent || fiber.tag === ClassComponentLazy) && (currentlyProcessingQueue === queue1 || (queue2 !== null && currentlyProcessingQueue === queue2)) && !didWarnUpdateInsideUpdate diff --git a/packages/react-reconciler/src/__tests__/ReactIncrementalSideEffects-test.internal.js b/packages/react-reconciler/src/__tests__/ReactIncrementalSideEffects-test.internal.js index 4b814eeb7cc..d93be6fcd98 100644 --- a/packages/react-reconciler/src/__tests__/ReactIncrementalSideEffects-test.internal.js +++ b/packages/react-reconciler/src/__tests__/ReactIncrementalSideEffects-test.internal.js @@ -1204,7 +1204,7 @@ describe('ReactIncrementalSideEffects', () => { ReactNoop.render(); expect(ReactNoop.flush).toWarnDev( - 'Warning: A string ref, "bar", has been found within a strict mode tree.', + 'Warning: A string ref, "bar", has been found within a strict mode tree.', ); expect(fooInstance.refs.bar.test).toEqual('test'); diff --git a/packages/react-reconciler/src/__tests__/ReactSuspense-test.internal.js b/packages/react-reconciler/src/__tests__/ReactSuspense-test.internal.js index 1294070f639..7731dc4fb9f 100644 --- a/packages/react-reconciler/src/__tests__/ReactSuspense-test.internal.js +++ b/packages/react-reconciler/src/__tests__/ReactSuspense-test.internal.js @@ -1414,8 +1414,8 @@ describe('ReactSuspense', () => { , ] : [ - , - , + , + , ]} ); diff --git a/packages/react-test-renderer/src/ReactTestRenderer.js b/packages/react-test-renderer/src/ReactTestRenderer.js index 9102963b6af..fcea4c76e82 100644 --- a/packages/react-test-renderer/src/ReactTestRenderer.js +++ b/packages/react-test-renderer/src/ReactTestRenderer.js @@ -14,10 +14,13 @@ import type {Instance, TextInstance} from './ReactTestHostConfig'; import * as TestRenderer from 'react-reconciler/inline.test'; import {batchedUpdates} from 'events/ReactGenericBatching'; import {findCurrentFiberUsingSlowPath} from 'react-reconciler/reflection'; +import {getLazyComponentTypeIfResolved} from 'react-reconciler/src/ReactFiberLazyComponent'; import { Fragment, FunctionalComponent, + FunctionalComponentLazy, ClassComponent, + ClassComponentLazy, HostComponent, HostPortal, HostText, @@ -27,6 +30,7 @@ import { Mode, ForwardRef, Profiler, + ForwardRefLazy, } from 'shared/ReactTypeOfWork'; import invariant from 'shared/invariant'; import ReactVersion from 'shared/ReactVersion'; @@ -148,6 +152,17 @@ function toTree(node: ?Fiber) { instance: node.stateNode, rendered: childrenToTree(node.child), }; + case ClassComponentLazy: { + const thenable = node.type; + const type = getLazyComponentTypeIfResolved(thenable); + return { + nodeType: 'component', + type, + props: {...node.memoizedProps}, + instance: node.stateNode, + rendered: childrenToTree(node.child), + }; + } case FunctionalComponent: return { nodeType: 'component', @@ -156,6 +171,17 @@ function toTree(node: ?Fiber) { instance: null, rendered: childrenToTree(node.child), }; + case FunctionalComponentLazy: { + const thenable = node.type; + const type = getLazyComponentTypeIfResolved(thenable); + return { + nodeType: 'component', + type: type, + props: {...node.memoizedProps}, + instance: node.stateNode, + rendered: childrenToTree(node.child), + }; + } case HostComponent: { return { nodeType: 'host', @@ -173,6 +199,7 @@ function toTree(node: ?Fiber) { case Mode: case Profiler: case ForwardRef: + case ForwardRefLazy: return childrenToTree(node.child); default: invariant( @@ -198,9 +225,12 @@ function wrapFiber(fiber: Fiber): ReactTestInstance { const validWrapperTypes = new Set([ FunctionalComponent, + FunctionalComponentLazy, ClassComponent, + ClassComponentLazy, HostComponent, ForwardRef, + ForwardRefLazy, // Normally skipped, but used when there's more than one root child. HostRoot, ]); diff --git a/packages/react/src/__tests__/ReactStrictMode-test.internal.js b/packages/react/src/__tests__/ReactStrictMode-test.internal.js index fffb538571f..91600ba28be 100644 --- a/packages/react/src/__tests__/ReactStrictMode-test.internal.js +++ b/packages/react/src/__tests__/ReactStrictMode-test.internal.js @@ -759,7 +759,7 @@ describe('ReactStrictMode', () => { expect(() => { renderer = ReactTestRenderer.create(); }).toWarnDev( - 'Warning: A string ref, "somestring", has been found within a strict mode tree. ' + + 'Warning: A string ref, "somestring", has been found within a strict mode tree. ' + 'String refs are a source of potential bugs and should be avoided. ' + 'We recommend using createRef() instead.\n\n' + ' in StrictMode (at **)\n' + @@ -801,7 +801,7 @@ describe('ReactStrictMode', () => { expect(() => { renderer = ReactTestRenderer.create(); }).toWarnDev( - 'Warning: A string ref, "somestring", has been found within a strict mode tree. ' + + 'Warning: A string ref, "somestring", has been found within a strict mode tree. ' + 'String refs are a source of potential bugs and should be avoided. ' + 'We recommend using createRef() instead.\n\n' + ' in InnerComponent (at **)\n' + diff --git a/packages/shared/ReactTypeOfWork.js b/packages/shared/ReactTypeOfWork.js index 9cd5ecb0952..4b3cfe80e16 100644 --- a/packages/shared/ReactTypeOfWork.js +++ b/packages/shared/ReactTypeOfWork.js @@ -28,18 +28,18 @@ export type TypeOfWork = export const IndeterminateComponent = 0; // Before we know whether it is functional or class export const FunctionalComponent = 1; -export const ClassComponent = 2; -export const HostRoot = 3; // Root of a host tree. Could be nested inside another node. -export const HostPortal = 4; // A subtree. Could be an entry point to a different renderer. -export const HostComponent = 5; -export const HostText = 6; -export const CallComponent_UNUSED = 7; -export const CallHandlerPhase_UNUSED = 8; -export const ReturnComponent_UNUSED = 9; -export const Fragment = 10; -export const Mode = 11; -export const ContextConsumer = 12; -export const ContextProvider = 13; -export const ForwardRef = 14; +export const FunctionalComponentLazy = 2; +export const ClassComponent = 3; +export const ClassComponentLazy = 4; +export const HostRoot = 5; // Root of a host tree. Could be nested inside another node. +export const HostPortal = 6; // A subtree. Could be an entry point to a different renderer. +export const HostComponent = 7; +export const HostText = 8; +export const Fragment = 9; +export const Mode = 10; +export const ContextConsumer = 11; +export const ContextProvider = 12; +export const ForwardRef = 13; +export const ForwardRefLazy = 14; export const Profiler = 15; export const PlaceholderComponent = 16; diff --git a/packages/shared/getComponentName.js b/packages/shared/getComponentName.js index 28664e24122..39d1f86b296 100644 --- a/packages/shared/getComponentName.js +++ b/packages/shared/getComponentName.js @@ -19,6 +19,7 @@ import { REACT_STRICT_MODE_TYPE, REACT_PLACEHOLDER_TYPE, } from 'shared/ReactSymbols'; +import {Resolved, Rejected} from 'react-reconciler/src/ReactFiberLazyComponent'; function getComponentName(type: mixed): string | null { if (type == null) { @@ -67,6 +68,12 @@ function getComponentName(type: mixed): string | null { ? `ForwardRef(${functionName})` : 'ForwardRef'; } + if (typeof type.then === 'function') { + const status = type._reactStatus; + if (status === Resolved || status === Rejected) { + return getComponentName(type._reactResult); + } + } } return null; } From cd277747a929bbef7c1c9aa9ddfd558d2b8a2eef Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Tue, 14 Aug 2018 18:38:35 -0700 Subject: [PATCH 03/14] Resolve defaultProps for lazy types --- .../src/ReactFiberBeginWork.js | 119 ++++++++++++++++-- .../__tests__/ReactSuspense-test.internal.js | 71 +++++++++++ 2 files changed, 178 insertions(+), 12 deletions(-) diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.js b/packages/react-reconciler/src/ReactFiberBeginWork.js index 61308bc1548..5671691bff4 100644 --- a/packages/react-reconciler/src/ReactFiberBeginWork.js +++ b/packages/react-reconciler/src/ReactFiberBeginWork.js @@ -190,6 +190,34 @@ function updateForwardRef( return workInProgress.child; } +function updateForwardRefLazy( + current: Fiber | null, + workInProgress: Fiber, + Component: any, + renderExpirationTime: ExpirationTime, +) { + const props = workInProgress.pendingProps; + const resolvedProps = resolveDefaultProps(Component, props); + if (resolvedProps !== null) { + workInProgress.pendingProps = resolvedProps; + const child = updateForwardRef( + current, + workInProgress, + Component, + renderExpirationTime, + ); + workInProgress.pendingProps = workInProgress.memoizedProps = props; + return child; + } else { + return updateForwardRef( + current, + workInProgress, + Component, + renderExpirationTime, + ); + } +} + function updateFragment( current: Fiber | null, workInProgress: Fiber, @@ -286,6 +314,34 @@ function updateFunctionalComponent( return workInProgress.child; } +function updateFunctionalComponentLazy( + current, + workInProgress, + Component, + renderExpirationTime, +) { + const props = workInProgress.pendingProps; + const resolvedProps = resolveDefaultProps(Component, props); + if (resolvedProps !== null) { + workInProgress.pendingProps = resolvedProps; + const child = updateFunctionalComponent( + current, + workInProgress, + Component, + renderExpirationTime, + ); + workInProgress.pendingProps = workInProgress.memoizedProps = props; + return child; + } else { + return updateFunctionalComponent( + current, + workInProgress, + Component, + renderExpirationTime, + ); + } +} + function updateClassComponent( current: Fiber | null, workInProgress: Fiber, @@ -430,6 +486,34 @@ function finishClassComponent( return workInProgress.child; } +function updateClassComponentLazy( + current, + workInProgress, + Component, + renderExpirationTime, +) { + const props = workInProgress.pendingProps; + const resolvedProps = resolveDefaultProps(Component, props); + if (resolvedProps !== null) { + workInProgress.pendingProps = resolvedProps; + const child = updateClassComponent( + current, + workInProgress, + Component, + renderExpirationTime, + ); + workInProgress.pendingProps = workInProgress.memoizedProps = props; + return child; + } else { + return updateClassComponent( + current, + workInProgress, + Component, + renderExpirationTime, + ); + } +} + function pushHostRootContext(workInProgress) { const root = (workInProgress.stateNode: FiberRoot); if (root.pendingContext) { @@ -579,6 +663,21 @@ function updateHostText(current, workInProgress) { return null; } +function resolveDefaultProps(Component, baseProps) { + if (Component && Component.defaultProps) { + // Resolve default props. Taken from ReactElement + const props = Object.assign({}, baseProps); + const defaultProps = Component.defaultProps; + for (let propName in defaultProps) { + if (props[propName] === undefined) { + props[propName] = defaultProps[propName]; + } + } + return props; + } + return null; +} + function mountIndeterminateComponent( current, workInProgress, @@ -591,6 +690,7 @@ function mountIndeterminateComponent( 'likely caused by a bug in React. Please file an issue.', ); + const props = workInProgress.pendingProps; if ( Component !== null && Component !== undefined && @@ -600,24 +700,20 @@ function mountIndeterminateComponent( if (typeof Component === 'function') { if (shouldConstruct(Component)) { workInProgress.tag = ClassComponentLazy; - return updateClassComponent( + return updateClassComponentLazy( current, workInProgress, Component, renderExpirationTime, ); } else { - const child = mountIndeterminateComponent( + workInProgress.tag = FunctionalComponentLazy; + return updateFunctionalComponentLazy( current, workInProgress, Component, renderExpirationTime, ); - workInProgress.tag = - workInProgress.tag === FunctionalComponent - ? FunctionalComponentLazy - : ClassComponentLazy; - return child; } } else if ( Component !== undefined && @@ -625,7 +721,7 @@ function mountIndeterminateComponent( Component.$$typeof ) { workInProgress.tag = ForwardRefLazy; - return updateForwardRef( + return updateForwardRefLazy( current, workInProgress, Component, @@ -634,7 +730,6 @@ function mountIndeterminateComponent( } } - const props = workInProgress.pendingProps; const unmaskedContext = getUnmaskedContext(workInProgress); const context = getMaskedContext(workInProgress, unmaskedContext); @@ -1103,7 +1198,7 @@ function beginWork( case FunctionalComponentLazy: { const thenable = workInProgress.type; const Component = (thenable._reactResult: any); - return updateFunctionalComponent( + return updateFunctionalComponentLazy( current, workInProgress, Component, @@ -1122,7 +1217,7 @@ function beginWork( case ClassComponentLazy: { const thenable = workInProgress.type; const Component = (thenable._reactResult: any); - return updateClassComponent( + return updateClassComponentLazy( current, workInProgress, Component, @@ -1159,7 +1254,7 @@ function beginWork( case ForwardRefLazy: const thenable = workInProgress.type; const Component = (thenable._reactResult: any); - return updateForwardRef( + return updateForwardRefLazy( current, workInProgress, Component, diff --git a/packages/react-reconciler/src/__tests__/ReactSuspense-test.internal.js b/packages/react-reconciler/src/__tests__/ReactSuspense-test.internal.js index 7731dc4fb9f..ff641844bcd 100644 --- a/packages/react-reconciler/src/__tests__/ReactSuspense-test.internal.js +++ b/packages/react-reconciler/src/__tests__/ReactSuspense-test.internal.js @@ -1472,6 +1472,77 @@ describe('ReactSuspense', () => { expect(ReactNoop.flush()).toEqual(['Hi again']); expect(ReactNoop.getChildren()).toEqual([span('Hi again')]); }); + + it('resolves defaultProps, on mount and update', async () => { + function T(props) { + return ; + } + T.defaultProps = {text: 'Hi'}; + const LazyText = Promise.resolve(T); + + ReactNoop.render( + }> + + , + ); + expect(ReactNoop.flush()).toEqual(['Loading...']); + expect(ReactNoop.getChildren()).toEqual([]); + + await LazyText; + + expect(ReactNoop.flush()).toEqual(['Hi']); + expect(ReactNoop.getChildren()).toEqual([span('Hi')]); + + T.defaultProps = {text: 'Hi again'}; + + ReactNoop.render( + }> + + , + ); + expect(ReactNoop.flush()).toEqual(['Hi again']); + expect(ReactNoop.getChildren()).toEqual([span('Hi again')]); + }); + + it('resolves defaultProps without breaking memoization', async () => { + function LazyImpl(props) { + ReactNoop.yield('Lazy'); + return ( + + + {props.children} + + ); + } + LazyImpl.defaultProps = {siblingText: 'Sibling'}; + const Lazy = Promise.resolve(LazyImpl); + + class Stateful extends React.Component { + state = {text: 'A'}; + render() { + return ; + } + } + + const stateful = React.createRef(null); + ReactNoop.render( + }> + + + + , + ); + expect(ReactNoop.flush()).toEqual(['Loading...']); + expect(ReactNoop.getChildren()).toEqual([]); + await Lazy; + expect(ReactNoop.flush()).toEqual(['Lazy', 'Sibling', 'A']); + expect(ReactNoop.getChildren()).toEqual([span('Sibling'), span('A')]); + + // Lazy should not have re-rendered + stateful.current.setState({text: 'B'}); + expect(ReactNoop.flush()).toEqual(['B']); + expect(ReactNoop.getChildren()).toEqual([span('Sibling'), span('B')]); + }); }); it('does not call lifecycles of a suspended component', async () => { From 179df3ae1aa1e97f946dc1fbe3073d18dd45533a Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Wed, 15 Aug 2018 15:19:44 -0700 Subject: [PATCH 04/14] Remove some calls to isContextProvider isContextProvider checks the fiber tag, but it's typically called after we've already refined the type of work. We should get rid of it. I removed some of them in the previous commit, and deleted a few more in this one. I left a few behind because the remaining ones would require additional refactoring that feels outside the scope of this PR. --- .../src/ReactFiberBeginWork.js | 37 ++++++++++++++++--- 1 file changed, 32 insertions(+), 5 deletions(-) diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.js b/packages/react-reconciler/src/ReactFiberBeginWork.js index 5671691bff4..c17a8251182 100644 --- a/packages/react-reconciler/src/ReactFiberBeginWork.js +++ b/packages/react-reconciler/src/ReactFiberBeginWork.js @@ -351,7 +351,14 @@ function updateClassComponent( // Push context providers early to prevent context stack mismatches. // During mounting we don't know the child context yet as the instance doesn't exist. // We will invalidate the child context in finishClassComponent() right after rendering. - const hasContext = pushLegacyContextProvider(workInProgress); + let hasContext; + const childContextTypes = Component.childContextTypes; + if (childContextTypes !== null && childContextTypes !== undefined) { + hasContext = true; + pushLegacyContextProvider(workInProgress); + } else { + hasContext = false; + } prepareToReadContext(workInProgress, renderExpirationTime); let shouldUpdate; @@ -780,7 +787,14 @@ function mountIndeterminateComponent( // Push context providers early to prevent context stack mismatches. // During mounting we don't know the child context yet as the instance doesn't exist. // We will invalidate the child context in finishClassComponent() right after rendering. - const hasContext = pushLegacyContextProvider(workInProgress); + let hasContext = false; + const childContextTypes = Component.childContextTypes; + if (childContextTypes !== null && childContextTypes !== undefined) { + hasContext = true; + pushLegacyContextProvider(workInProgress); + } else { + hasContext = false; + } workInProgress.memoizedState = value.state !== null && value.state !== undefined ? value.state : null; @@ -1145,10 +1159,23 @@ function beginWork( case HostComponent: pushHostContext(workInProgress); break; - case ClassComponent: - case ClassComponentLazy: - pushLegacyContextProvider(workInProgress); + case ClassComponent: { + const Component = workInProgress.type; + const childContextTypes = Component.childContextTypes; + if (childContextTypes !== null && childContextTypes !== undefined) { + pushLegacyContextProvider(workInProgress); + } break; + } + case ClassComponentLazy: { + const thenable = workInProgress.type; + const Component = (thenable._reactResult: any); + const childContextTypes = Component.childContextTypes; + if (childContextTypes !== null && childContextTypes !== undefined) { + pushLegacyContextProvider(workInProgress); + } + break; + } case HostPortal: pushHostContainer( workInProgress, From 4f09f491946258645bb021197dff0d60169f0a4d Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Wed, 15 Aug 2018 15:23:42 -0700 Subject: [PATCH 05/14] Remove getLazyComponentTypeIfResolved --- packages/react-reconciler/src/ReactFiberContext.js | 3 +-- packages/react-reconciler/src/ReactFiberLazyComponent.js | 6 ------ packages/react-reconciler/src/ReactFiberReconciler.js | 5 ++--- packages/react-test-renderer/src/ReactTestRenderer.js | 5 ++--- 4 files changed, 5 insertions(+), 14 deletions(-) diff --git a/packages/react-reconciler/src/ReactFiberContext.js b/packages/react-reconciler/src/ReactFiberContext.js index e2ef3421ca2..679638e3bc1 100644 --- a/packages/react-reconciler/src/ReactFiberContext.js +++ b/packages/react-reconciler/src/ReactFiberContext.js @@ -24,7 +24,6 @@ import checkPropTypes from 'prop-types/checkPropTypes'; import * as ReactCurrentFiber from './ReactCurrentFiber'; import {startPhaseTimer, stopPhaseTimer} from './ReactDebugFiberPerf'; import {createCursor, push, pop} from './ReactFiberStack'; -import {getLazyComponentTypeIfResolved} from 'react-reconciler/src/ReactFiberLazyComponent'; let warnedAboutMissingGetChildContext; @@ -123,7 +122,7 @@ function isContextProvider(fiber: Fiber): boolean { if (fiber.tag === ClassComponent) { type = fiber.type; } else if (fiber.tag === ClassComponentLazy) { - type = getLazyComponentTypeIfResolved(fiber.type); + type = fiber.type._reactResult; if (type === null) { return false; } diff --git a/packages/react-reconciler/src/ReactFiberLazyComponent.js b/packages/react-reconciler/src/ReactFiberLazyComponent.js index 16b70e7ee79..b978d76092d 100644 --- a/packages/react-reconciler/src/ReactFiberLazyComponent.js +++ b/packages/react-reconciler/src/ReactFiberLazyComponent.js @@ -17,12 +17,6 @@ export const Pending = 0; export const Resolved = 1; export const Rejected = 2; -export function getLazyComponentTypeIfResolved( - thenable: Thenable, -): T | null { - return thenable._reactStatus === Resolved ? thenable._reactResult : null; -} - export function readLazyComponentType(thenable: Thenable): T { const status = thenable._reactStatus; switch (status) { diff --git a/packages/react-reconciler/src/ReactFiberReconciler.js b/packages/react-reconciler/src/ReactFiberReconciler.js index 6e262b0a869..1bab73c4704 100644 --- a/packages/react-reconciler/src/ReactFiberReconciler.js +++ b/packages/react-reconciler/src/ReactFiberReconciler.js @@ -59,7 +59,6 @@ import { import {createUpdate, enqueueUpdate} from './ReactUpdateQueue'; import ReactFiberInstrumentation from './ReactFiberInstrumentation'; import * as ReactCurrentFiber from './ReactCurrentFiber'; -import {getLazyComponentTypeIfResolved} from 'react-reconciler/src/ReactFiberLazyComponent'; type OpaqueRoot = FiberRoot; @@ -103,8 +102,8 @@ function getContextForSubtree( return processChildContext(fiber, type, parentContext); } } else if (fiber.tag === ClassComponentLazy) { - const type = getLazyComponentTypeIfResolved(fiber.type); - if (type !== null) { + const type = fiber.type._reactResult; + if (type !== undefined) { const childContextTypes = type.childContextTypes; if (childContextTypes !== undefined && childContextTypes !== null) { return processChildContext(fiber, type, parentContext); diff --git a/packages/react-test-renderer/src/ReactTestRenderer.js b/packages/react-test-renderer/src/ReactTestRenderer.js index fcea4c76e82..52bccda4c01 100644 --- a/packages/react-test-renderer/src/ReactTestRenderer.js +++ b/packages/react-test-renderer/src/ReactTestRenderer.js @@ -14,7 +14,6 @@ import type {Instance, TextInstance} from './ReactTestHostConfig'; import * as TestRenderer from 'react-reconciler/inline.test'; import {batchedUpdates} from 'events/ReactGenericBatching'; import {findCurrentFiberUsingSlowPath} from 'react-reconciler/reflection'; -import {getLazyComponentTypeIfResolved} from 'react-reconciler/src/ReactFiberLazyComponent'; import { Fragment, FunctionalComponent, @@ -154,7 +153,7 @@ function toTree(node: ?Fiber) { }; case ClassComponentLazy: { const thenable = node.type; - const type = getLazyComponentTypeIfResolved(thenable); + const type = thenable._reactResult; return { nodeType: 'component', type, @@ -173,7 +172,7 @@ function toTree(node: ?Fiber) { }; case FunctionalComponentLazy: { const thenable = node.type; - const type = getLazyComponentTypeIfResolved(thenable); + const type = thenable._reactResult; return { nodeType: 'component', type: type, From d96e01994b3bf8bda8d4f025f8a38fde079f8442 Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Wed, 15 Aug 2018 15:31:24 -0700 Subject: [PATCH 06/14] Return baseProps instead of null The caller compares the result to baseProps to see if anything changed. --- packages/react-reconciler/src/ReactFiberBeginWork.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.js b/packages/react-reconciler/src/ReactFiberBeginWork.js index c17a8251182..ca7e8453bed 100644 --- a/packages/react-reconciler/src/ReactFiberBeginWork.js +++ b/packages/react-reconciler/src/ReactFiberBeginWork.js @@ -198,7 +198,7 @@ function updateForwardRefLazy( ) { const props = workInProgress.pendingProps; const resolvedProps = resolveDefaultProps(Component, props); - if (resolvedProps !== null) { + if (resolvedProps !== props) { workInProgress.pendingProps = resolvedProps; const child = updateForwardRef( current, @@ -322,7 +322,7 @@ function updateFunctionalComponentLazy( ) { const props = workInProgress.pendingProps; const resolvedProps = resolveDefaultProps(Component, props); - if (resolvedProps !== null) { + if (resolvedProps !== props) { workInProgress.pendingProps = resolvedProps; const child = updateFunctionalComponent( current, @@ -501,7 +501,7 @@ function updateClassComponentLazy( ) { const props = workInProgress.pendingProps; const resolvedProps = resolveDefaultProps(Component, props); - if (resolvedProps !== null) { + if (resolvedProps !== props) { workInProgress.pendingProps = resolvedProps; const child = updateClassComponent( current, @@ -682,7 +682,7 @@ function resolveDefaultProps(Component, baseProps) { } return props; } - return null; + return baseProps; } function mountIndeterminateComponent( From b1e3dbf4c447fa1920af14b972b6b1a0ce18d337 Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Wed, 15 Aug 2018 15:43:32 -0700 Subject: [PATCH 07/14] Avoid redundant checks by inlining getFiberTagFromObjectType --- packages/react-reconciler/src/ReactFiber.js | 103 ++++++++++---------- 1 file changed, 49 insertions(+), 54 deletions(-) diff --git a/packages/react-reconciler/src/ReactFiber.js b/packages/react-reconciler/src/ReactFiber.js index 32d9cd3e9e0..0381715baee 100644 --- a/packages/react-reconciler/src/ReactFiber.js +++ b/packages/react-reconciler/src/ReactFiber.js @@ -398,7 +398,7 @@ export function createFiberFromElement( } else if (typeof type === 'string') { fiberTag = HostComponent; } else { - switch (type) { + getTag: switch (type) { case REACT_FRAGMENT_TYPE: return createFiberFromFragment( pendingProps.children, @@ -419,9 +419,54 @@ export function createFiberFromElement( case REACT_PLACEHOLDER_TYPE: fiberTag = PlaceholderComponent; break; - default: - fiberTag = getFiberTagFromObjectType(type, owner); - break; + default: { + if (typeof type === 'object' && type !== null) { + switch (type.$$typeof) { + case REACT_PROVIDER_TYPE: + fiberTag = ContextProvider; + break getTag; + case REACT_CONTEXT_TYPE: + // This is a consumer + fiberTag = ContextConsumer; + break getTag; + case REACT_FORWARD_REF_TYPE: + fiberTag = ForwardRef; + break getTag; + default: { + if (typeof type.then === 'function') { + fiberTag = IndeterminateComponent; + break getTag; + } + } + } + } + let info = ''; + if (__DEV__) { + if ( + type === undefined || + (typeof type === 'object' && + type !== null && + Object.keys(type).length === 0) + ) { + info += + ' You likely forgot to export your component from the file ' + + "it's defined in, or you might have mixed up default and " + + 'named imports.'; + } + const ownerName = owner ? getComponentName(owner.type) : null; + if (ownerName) { + info += '\n\nCheck the render method of `' + ownerName + '`.'; + } + } + invariant( + false, + 'Element type is invalid: expected a string (for built-in ' + + 'components) or a class/function (for composite components) ' + + 'but got: %s.%s', + type == null ? type : typeof type, + info, + ); + } } } @@ -437,56 +482,6 @@ export function createFiberFromElement( return fiber; } -function getFiberTagFromObjectType(type, owner): TypeOfWork { - const $$typeof = - typeof type === 'object' && type !== null ? type.$$typeof : null; - - switch ($$typeof) { - case REACT_PROVIDER_TYPE: - return ContextProvider; - case REACT_CONTEXT_TYPE: - // This is a consumer - return ContextConsumer; - case REACT_FORWARD_REF_TYPE: - return ForwardRef; - default: { - if ( - type !== undefined && - type !== null && - typeof type.then === 'function' - ) { - return IndeterminateComponent; - } - let info = ''; - if (__DEV__) { - if ( - type === undefined || - (typeof type === 'object' && - type !== null && - Object.keys(type).length === 0) - ) { - info += - ' You likely forgot to export your component from the file ' + - "it's defined in, or you might have mixed up default and " + - 'named imports.'; - } - const ownerName = owner ? getComponentName(owner.type) : null; - if (ownerName) { - info += '\n\nCheck the render method of `' + ownerName + '`.'; - } - } - invariant( - false, - 'Element type is invalid: expected a string (for built-in ' + - 'components) or a class/function (for composite components) ' + - 'but got: %s.%s', - type == null ? type : typeof type, - info, - ); - } - } -} - export function createFiberFromFragment( elements: ReactFragment, mode: TypeOfMode, From 9c511ed47f2cd5f61395b5095db5adb372e78a7a Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Wed, 15 Aug 2018 16:15:02 -0700 Subject: [PATCH 08/14] Move tag resolution to ReactFiber module --- packages/react-reconciler/src/ReactFiber.js | 32 +++++++++++-- .../src/ReactFiberBeginWork.js | 45 ++++++++++--------- 2 files changed, 53 insertions(+), 24 deletions(-) diff --git a/packages/react-reconciler/src/ReactFiber.js b/packages/react-reconciler/src/ReactFiber.js index 0381715baee..70ff482a0ef 100644 --- a/packages/react-reconciler/src/ReactFiber.js +++ b/packages/react-reconciler/src/ReactFiber.js @@ -33,6 +33,9 @@ import { ContextConsumer, Profiler, PlaceholderComponent, + FunctionalComponentLazy, + ClassComponentLazy, + ForwardRefLazy, } from 'shared/ReactTypeOfWork'; import getComponentName from 'shared/getComponentName'; @@ -278,8 +281,31 @@ const createFiber = function( return new FiberNode(tag, pendingProps, key, mode); }; -export function shouldConstruct(Component: Function) { - return !!(Component.prototype && Component.prototype.isReactComponent); +function shouldConstruct(Component: Function) { + const prototype = Component.prototype; + return ( + typeof prototype === 'object' && + prototype !== null && + typeof prototype.isReactComponent === 'object' && + prototype.isReactComponent !== null + ); +} + +export function reassignLazyComponentTag( + fiber: Fiber, + Component: Function, +): void { + if (typeof Component === 'function') { + fiber.tag = shouldConstruct(Component) + ? ClassComponentLazy + : FunctionalComponentLazy; + } else if ( + Component !== undefined && + Component !== null && + Component.$$typeof + ) { + fiber.tag = ForwardRefLazy; + } } // This is used to create an alternate fiber to do work on. @@ -388,7 +414,7 @@ export function createFiberFromElement( } let fiber; - let type = element.type; + const type = element.type; const key = element.key; const pendingProps = element.props; diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.js b/packages/react-reconciler/src/ReactFiberBeginWork.js index ca7e8453bed..dfd4691d076 100644 --- a/packages/react-reconciler/src/ReactFiberBeginWork.js +++ b/packages/react-reconciler/src/ReactFiberBeginWork.js @@ -100,7 +100,7 @@ import { updateClassInstance, } from './ReactFiberClassComponent'; import {readLazyComponentType} from './ReactFiberLazyComponent'; -import {shouldConstruct} from './ReactFiber'; +import {reassignLazyComponentTag} from './ReactFiber'; const ReactCurrentOwner = ReactSharedInternals.ReactCurrentOwner; @@ -699,41 +699,44 @@ function mountIndeterminateComponent( const props = workInProgress.pendingProps; if ( + typeof Component === 'object' && Component !== null && - Component !== undefined && typeof Component.then === 'function' ) { Component = readLazyComponentType(Component); - if (typeof Component === 'function') { - if (shouldConstruct(Component)) { - workInProgress.tag = ClassComponentLazy; + reassignLazyComponentTag(workInProgress, Component); + switch (workInProgress.tag) { + case FunctionalComponentLazy: + return updateFunctionalComponentLazy( + current, + workInProgress, + Component, + renderExpirationTime, + ); + + case ClassComponentLazy: return updateClassComponentLazy( current, workInProgress, Component, renderExpirationTime, ); - } else { - workInProgress.tag = FunctionalComponentLazy; - return updateFunctionalComponentLazy( + case ForwardRefLazy: + return updateForwardRefLazy( current, workInProgress, Component, renderExpirationTime, ); - } - } else if ( - Component !== undefined && - Component !== null && - Component.$$typeof - ) { - workInProgress.tag = ForwardRefLazy; - return updateForwardRefLazy( - current, - workInProgress, - Component, - renderExpirationTime, - ); + default: + // This message intentionally doesn't metion ForwardRef 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. ' + + 'Promise elements must resolve to a class or function.', + Component, + ); } } From 02a7998845b6ad420b6d9616b9de21602ff58c69 Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Wed, 15 Aug 2018 17:09:28 -0700 Subject: [PATCH 09/14] Pass next props to update* functions We should do this with all types of work in the future. --- .../src/ReactFiberBeginWork.js | 153 ++++++------------ .../src/ReactFiberClassComponent.js | 20 +-- 2 files changed, 62 insertions(+), 111 deletions(-) diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.js b/packages/react-reconciler/src/ReactFiberBeginWork.js index dfd4691d076..d6e9ea8abfc 100644 --- a/packages/react-reconciler/src/ReactFiberBeginWork.js +++ b/packages/react-reconciler/src/ReactFiberBeginWork.js @@ -151,10 +151,10 @@ function updateForwardRef( current: Fiber | null, workInProgress: Fiber, type: any, + nextProps: any, renderExpirationTime: ExpirationTime, ) { const render = type.render; - const nextProps = workInProgress.pendingProps; const ref = workInProgress.ref; if (hasLegacyContextChanged()) { // Normally we can bail out on props equality but if context has changed @@ -190,34 +190,6 @@ function updateForwardRef( return workInProgress.child; } -function updateForwardRefLazy( - current: Fiber | null, - workInProgress: Fiber, - Component: any, - renderExpirationTime: ExpirationTime, -) { - const props = workInProgress.pendingProps; - const resolvedProps = resolveDefaultProps(Component, props); - if (resolvedProps !== props) { - workInProgress.pendingProps = resolvedProps; - const child = updateForwardRef( - current, - workInProgress, - Component, - renderExpirationTime, - ); - workInProgress.pendingProps = workInProgress.memoizedProps = props; - return child; - } else { - return updateForwardRef( - current, - workInProgress, - Component, - renderExpirationTime, - ); - } -} - function updateFragment( current: Fiber | null, workInProgress: Fiber, @@ -285,9 +257,9 @@ function updateFunctionalComponent( current, workInProgress, Component, + nextProps: any, renderExpirationTime, ) { - const nextProps = workInProgress.pendingProps; const unmaskedContext = getUnmaskedContext(workInProgress); const context = getMaskedContext(workInProgress, unmaskedContext); @@ -314,38 +286,11 @@ function updateFunctionalComponent( return workInProgress.child; } -function updateFunctionalComponentLazy( - current, - workInProgress, - Component, - renderExpirationTime, -) { - const props = workInProgress.pendingProps; - const resolvedProps = resolveDefaultProps(Component, props); - if (resolvedProps !== props) { - workInProgress.pendingProps = resolvedProps; - const child = updateFunctionalComponent( - current, - workInProgress, - Component, - renderExpirationTime, - ); - workInProgress.pendingProps = workInProgress.memoizedProps = props; - return child; - } else { - return updateFunctionalComponent( - current, - workInProgress, - Component, - renderExpirationTime, - ); - } -} - function updateClassComponent( current: Fiber | null, workInProgress: Fiber, Component: any, + nextProps, renderExpirationTime: ExpirationTime, ) { // Push context providers early to prevent context stack mismatches. @@ -368,17 +313,22 @@ function updateClassComponent( constructClassInstance( workInProgress, Component, - workInProgress.pendingProps, + nextProps, + renderExpirationTime, + ); + mountClassInstance( + workInProgress, + Component, + nextProps, renderExpirationTime, ); - mountClassInstance(workInProgress, Component, renderExpirationTime); - shouldUpdate = true; } else { // In a resume, we'll already have an instance we can reuse. shouldUpdate = resumeMountClassInstance( workInProgress, Component, + nextProps, renderExpirationTime, ); } @@ -387,6 +337,7 @@ function updateClassComponent( current, workInProgress, Component, + nextProps, renderExpirationTime, ); } @@ -493,34 +444,6 @@ function finishClassComponent( return workInProgress.child; } -function updateClassComponentLazy( - current, - workInProgress, - Component, - renderExpirationTime, -) { - const props = workInProgress.pendingProps; - const resolvedProps = resolveDefaultProps(Component, props); - if (resolvedProps !== props) { - workInProgress.pendingProps = resolvedProps; - const child = updateClassComponent( - current, - workInProgress, - Component, - renderExpirationTime, - ); - workInProgress.pendingProps = workInProgress.memoizedProps = props; - return child; - } else { - return updateClassComponent( - current, - workInProgress, - Component, - renderExpirationTime, - ); - } -} - function pushHostRootContext(workInProgress) { const root = (workInProgress.stateNode: FiberRoot); if (root.pendingContext) { @@ -705,30 +628,40 @@ function mountIndeterminateComponent( ) { Component = readLazyComponentType(Component); reassignLazyComponentTag(workInProgress, Component); + const resolvedProps = resolveDefaultProps(Component, props); + let child; switch (workInProgress.tag) { - case FunctionalComponentLazy: - return updateFunctionalComponentLazy( + case FunctionalComponentLazy: { + child = updateFunctionalComponent( current, workInProgress, Component, + resolvedProps, renderExpirationTime, ); - - case ClassComponentLazy: - return updateClassComponentLazy( + break; + } + case ClassComponentLazy: { + child = updateClassComponent( current, workInProgress, Component, + resolvedProps, renderExpirationTime, ); - case ForwardRefLazy: - return updateForwardRefLazy( + break; + } + case ForwardRefLazy: { + child = updateForwardRef( current, workInProgress, Component, + resolvedProps, renderExpirationTime, ); - default: + break; + } + default: { // This message intentionally doesn't metion ForwardRef because the // fact that it's a separate type of work is an implementation detail. invariant( @@ -737,7 +670,10 @@ function mountIndeterminateComponent( 'Promise elements must resolve to a class or function.', Component, ); + } } + workInProgress.pendingProps = props; + return child; } const unmaskedContext = getUnmaskedContext(workInProgress); @@ -813,7 +749,7 @@ function mountIndeterminateComponent( } adoptClassInstance(workInProgress, value); - mountClassInstance(workInProgress, Component, renderExpirationTime); + mountClassInstance(workInProgress, Component, props, renderExpirationTime); return finishClassComponent( current, workInProgress, @@ -1222,18 +1158,23 @@ function beginWork( current, workInProgress, Component, + workInProgress.pendingProps, renderExpirationTime, ); } case FunctionalComponentLazy: { const thenable = workInProgress.type; const Component = (thenable._reactResult: any); - return updateFunctionalComponentLazy( + const unresolvedProps = workInProgress.pendingProps; + const child = updateFunctionalComponent( current, workInProgress, Component, + resolveDefaultProps(Component, unresolvedProps), renderExpirationTime, ); + workInProgress.memoizedProps = unresolvedProps; + return child; } case ClassComponent: { const Component = workInProgress.type; @@ -1241,18 +1182,23 @@ function beginWork( current, workInProgress, Component, + workInProgress.pendingProps, renderExpirationTime, ); } case ClassComponentLazy: { const thenable = workInProgress.type; const Component = (thenable._reactResult: any); - return updateClassComponentLazy( + const unresolvedProps = workInProgress.pendingProps; + const child = updateClassComponent( current, workInProgress, Component, + resolveDefaultProps(Component, unresolvedProps), renderExpirationTime, ); + workInProgress.memoizedProps = unresolvedProps; + return child; } case HostRoot: return updateHostRoot(current, workInProgress, renderExpirationTime); @@ -1278,18 +1224,23 @@ function beginWork( current, workInProgress, type, + workInProgress.pendingProps, renderExpirationTime, ); } case ForwardRefLazy: const thenable = workInProgress.type; const Component = (thenable._reactResult: any); - return updateForwardRefLazy( + const unresolvedProps = workInProgress.pendingProps; + const child = updateForwardRef( current, workInProgress, Component, + resolveDefaultProps(Component, unresolvedProps), renderExpirationTime, ); + workInProgress.memoizedProps = unresolvedProps; + return child; case Fragment: return updateFragment(current, workInProgress, renderExpirationTime); case Mode: diff --git a/packages/react-reconciler/src/ReactFiberClassComponent.js b/packages/react-reconciler/src/ReactFiberClassComponent.js index 11e42bb91f3..4f593175d69 100644 --- a/packages/react-reconciler/src/ReactFiberClassComponent.js +++ b/packages/react-reconciler/src/ReactFiberClassComponent.js @@ -265,7 +265,7 @@ function checkShouldComponentUpdate( return true; } -function checkClassInstance(workInProgress: Fiber, ctor: any) { +function checkClassInstance(workInProgress: Fiber, ctor: any, newProps: any) { const instance = workInProgress.stateNode; if (__DEV__) { const name = getComponentName(ctor) || 'Component'; @@ -383,7 +383,7 @@ function checkClassInstance(workInProgress: Fiber, ctor: any) { 'UNSAFE_componentWillRecieveProps(). Did you mean UNSAFE_componentWillReceiveProps()?', name, ); - const hasMutatedProps = instance.props !== workInProgress.pendingProps; + const hasMutatedProps = instance.props !== newProps; warningWithoutStack( instance.props === undefined || !hasMutatedProps, '%s(...): When calling super() in `%s`, make sure to pass ' + @@ -658,17 +658,17 @@ function callComponentWillReceiveProps( function mountClassInstance( workInProgress: Fiber, ctor: any, + newProps: any, renderExpirationTime: ExpirationTime, ): void { if (__DEV__) { - checkClassInstance(workInProgress, ctor); + checkClassInstance(workInProgress, ctor, newProps); } const instance = workInProgress.stateNode; - const props = workInProgress.pendingProps; const unmaskedContext = getUnmaskedContext(workInProgress); - instance.props = props; + instance.props = newProps; instance.state = workInProgress.memoizedState; instance.refs = emptyRefsObject; instance.context = getMaskedContext(workInProgress, unmaskedContext); @@ -699,7 +699,7 @@ function mountClassInstance( processUpdateQueue( workInProgress, updateQueue, - props, + newProps, instance, renderExpirationTime, ); @@ -712,7 +712,7 @@ function mountClassInstance( workInProgress, ctor, getDerivedStateFromProps, - props, + newProps, ); instance.state = workInProgress.memoizedState; } @@ -733,7 +733,7 @@ function mountClassInstance( processUpdateQueue( workInProgress, updateQueue, - props, + newProps, instance, renderExpirationTime, ); @@ -749,12 +749,12 @@ function mountClassInstance( function resumeMountClassInstance( workInProgress: Fiber, ctor: any, + newProps: any, renderExpirationTime: ExpirationTime, ): boolean { const instance = workInProgress.stateNode; const oldProps = workInProgress.memoizedProps; - const newProps = workInProgress.pendingProps; instance.props = oldProps; const oldContext = instance.context; @@ -888,12 +888,12 @@ function updateClassInstance( current: Fiber, workInProgress: Fiber, ctor: any, + newProps: any, renderExpirationTime: ExpirationTime, ): boolean { const instance = workInProgress.stateNode; const oldProps = workInProgress.memoizedProps; - const newProps = workInProgress.pendingProps; instance.props = oldProps; const oldContext = instance.context; From 93c376ed73f630e6b7b9f859c475b817a6cb3e8c Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Wed, 15 Aug 2018 17:45:41 -0700 Subject: [PATCH 10/14] Refine component type before pushing/popping context Removes unnecessary checks. --- .../src/ReactFiberBeginWork.js | 29 +++------ .../src/ReactFiberClassComponent.js | 8 +-- .../src/ReactFiberCompleteWork.js | 12 ++-- .../react-reconciler/src/ReactFiberContext.js | 64 ++++++++++--------- .../src/ReactFiberReconciler.js | 17 ++--- .../src/ReactFiberScheduler.js | 10 +-- .../src/ReactFiberUnwindWork.js | 11 ++-- 7 files changed, 69 insertions(+), 82 deletions(-) diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.js b/packages/react-reconciler/src/ReactFiberBeginWork.js index d6e9ea8abfc..fdfc31a8f1e 100644 --- a/packages/react-reconciler/src/ReactFiberBeginWork.js +++ b/packages/react-reconciler/src/ReactFiberBeginWork.js @@ -84,6 +84,7 @@ import { getUnmaskedContext, hasContextChanged as hasLegacyContextChanged, pushContextProvider as pushLegacyContextProvider, + isContextProvider as isLegacyContextProvider, pushTopLevelContextObject, invalidateContextProvider, } from './ReactFiberContext'; @@ -260,7 +261,7 @@ function updateFunctionalComponent( nextProps: any, renderExpirationTime, ) { - const unmaskedContext = getUnmaskedContext(workInProgress); + const unmaskedContext = getUnmaskedContext(workInProgress, Component); const context = getMaskedContext(workInProgress, unmaskedContext); let nextChildren; @@ -297,8 +298,7 @@ function updateClassComponent( // During mounting we don't know the child context yet as the instance doesn't exist. // We will invalidate the child context in finishClassComponent() right after rendering. let hasContext; - const childContextTypes = Component.childContextTypes; - if (childContextTypes !== null && childContextTypes !== undefined) { + if (isLegacyContextProvider(Component)) { hasContext = true; pushLegacyContextProvider(workInProgress); } else { @@ -629,37 +629,33 @@ function mountIndeterminateComponent( Component = readLazyComponentType(Component); reassignLazyComponentTag(workInProgress, Component); const resolvedProps = resolveDefaultProps(Component, props); - let child; switch (workInProgress.tag) { case FunctionalComponentLazy: { - child = updateFunctionalComponent( + return updateFunctionalComponent( current, workInProgress, Component, resolvedProps, renderExpirationTime, ); - break; } case ClassComponentLazy: { - child = updateClassComponent( + return updateClassComponent( current, workInProgress, Component, resolvedProps, renderExpirationTime, ); - break; } case ForwardRefLazy: { - child = updateForwardRef( + return updateForwardRef( current, workInProgress, Component, resolvedProps, renderExpirationTime, ); - break; } default: { // This message intentionally doesn't metion ForwardRef because the @@ -672,11 +668,9 @@ function mountIndeterminateComponent( ); } } - workInProgress.pendingProps = props; - return child; } - const unmaskedContext = getUnmaskedContext(workInProgress); + const unmaskedContext = getUnmaskedContext(workInProgress, Component); const context = getMaskedContext(workInProgress, unmaskedContext); prepareToReadContext(workInProgress, renderExpirationTime); @@ -727,8 +721,7 @@ function mountIndeterminateComponent( // During mounting we don't know the child context yet as the instance doesn't exist. // We will invalidate the child context in finishClassComponent() right after rendering. let hasContext = false; - const childContextTypes = Component.childContextTypes; - if (childContextTypes !== null && childContextTypes !== undefined) { + if (isLegacyContextProvider(Component)) { hasContext = true; pushLegacyContextProvider(workInProgress); } else { @@ -1100,8 +1093,7 @@ function beginWork( break; case ClassComponent: { const Component = workInProgress.type; - const childContextTypes = Component.childContextTypes; - if (childContextTypes !== null && childContextTypes !== undefined) { + if (isLegacyContextProvider(Component)) { pushLegacyContextProvider(workInProgress); } break; @@ -1109,8 +1101,7 @@ function beginWork( case ClassComponentLazy: { const thenable = workInProgress.type; const Component = (thenable._reactResult: any); - const childContextTypes = Component.childContextTypes; - if (childContextTypes !== null && childContextTypes !== undefined) { + if (isLegacyContextProvider(Component)) { pushLegacyContextProvider(workInProgress); } break; diff --git a/packages/react-reconciler/src/ReactFiberClassComponent.js b/packages/react-reconciler/src/ReactFiberClassComponent.js index 4f593175d69..a4b3692a5af 100644 --- a/packages/react-reconciler/src/ReactFiberClassComponent.js +++ b/packages/react-reconciler/src/ReactFiberClassComponent.js @@ -473,7 +473,7 @@ function constructClassInstance( props: any, renderExpirationTime: ExpirationTime, ): any { - const unmaskedContext = getUnmaskedContext(workInProgress); + const unmaskedContext = getUnmaskedContext(workInProgress, ctor); const contextTypes = ctor.contextTypes; const isContextConsumer = contextTypes !== null && contextTypes !== undefined; const context = isContextConsumer @@ -666,7 +666,7 @@ function mountClassInstance( } const instance = workInProgress.stateNode; - const unmaskedContext = getUnmaskedContext(workInProgress); + const unmaskedContext = getUnmaskedContext(workInProgress, ctor); instance.props = newProps; instance.state = workInProgress.memoizedState; @@ -758,7 +758,7 @@ function resumeMountClassInstance( instance.props = oldProps; const oldContext = instance.context; - const nextLegacyUnmaskedContext = getUnmaskedContext(workInProgress); + const nextLegacyUnmaskedContext = getUnmaskedContext(workInProgress, ctor); const nextLegacyContext = getMaskedContext( workInProgress, nextLegacyUnmaskedContext, @@ -897,7 +897,7 @@ function updateClassInstance( instance.props = oldProps; const oldContext = instance.context; - const nextLegacyUnmaskedContext = getUnmaskedContext(workInProgress); + const nextLegacyUnmaskedContext = getUnmaskedContext(workInProgress, ctor); const nextLegacyContext = getMaskedContext( workInProgress, nextLegacyUnmaskedContext, diff --git a/packages/react-reconciler/src/ReactFiberCompleteWork.js b/packages/react-reconciler/src/ReactFiberCompleteWork.js index 8466e95881f..2b63b7f9ab7 100644 --- a/packages/react-reconciler/src/ReactFiberCompleteWork.js +++ b/packages/react-reconciler/src/ReactFiberCompleteWork.js @@ -62,6 +62,7 @@ import { popHostContainer, } from './ReactFiberHostContext'; import { + isContextProvider as isLegacyContextProvider, popContext as popLegacyContext, popTopLevelContextObject as popTopLevelLegacyContextObject, } from './ReactFiberContext'; @@ -319,18 +320,15 @@ function completeWork( case FunctionalComponentLazy: break; case ClassComponent: { - // We are leaving this subtree, so pop context if any. - const childContextTypes = workInProgress.type.childContextTypes; - if (childContextTypes !== null && childContextTypes !== undefined) { + const Component = workInProgress.type; + if (isLegacyContextProvider(Component)) { popLegacyContext(workInProgress); } break; } case ClassComponentLazy: { - // We are leaving this subtree, so pop context if any. - const childContextTypes = - workInProgress.type._reactResult.childContextTypes; - if (childContextTypes !== null && childContextTypes !== undefined) { + const Component = workInProgress.type._reactResult; + if (isLegacyContextProvider(Component)) { popLegacyContext(workInProgress); } break; diff --git a/packages/react-reconciler/src/ReactFiberContext.js b/packages/react-reconciler/src/ReactFiberContext.js index 679638e3bc1..b5d5519ee8d 100644 --- a/packages/react-reconciler/src/ReactFiberContext.js +++ b/packages/react-reconciler/src/ReactFiberContext.js @@ -45,8 +45,11 @@ let didPerformWorkStackCursor: StackCursor = createCursor(false); // pushed the next context provider, and now need to merge their contexts. let previousContext: Object = emptyContextObject; -function getUnmaskedContext(workInProgress: Fiber): Object { - const hasOwnContext = isContextProvider(workInProgress); +function getUnmaskedContext( + workInProgress: Fiber, + Component: Function, +): Object { + const hasOwnContext = isContextProvider(Component); if (hasOwnContext) { // If the fiber is a context provider itself, when we read its context // we have already pushed its own child context on the stack. A context @@ -117,18 +120,7 @@ function hasContextChanged(): boolean { return didPerformWorkStackCursor.current; } -function isContextProvider(fiber: Fiber): boolean { - let type; - if (fiber.tag === ClassComponent) { - type = fiber.type; - } else if (fiber.tag === ClassComponentLazy) { - type = fiber.type._reactResult; - if (type === null) { - return false; - } - } else { - return false; - } +function isContextProvider(type: Function): boolean { const childContextTypes = type.childContextTypes; return childContextTypes !== null && childContextTypes !== undefined; } @@ -225,10 +217,6 @@ function processChildContext( } function pushContextProvider(workInProgress: Fiber): boolean { - if (!isContextProvider(workInProgress)) { - return false; - } - const instance = workInProgress.stateNode; // We push the context as early as possible to ensure stack integrity. // If the instance does not exist yet, we will push null at first, @@ -296,20 +284,33 @@ function findCurrentUnmaskedContext(fiber: Fiber): Object { 'This error is likely caused by a bug in React. Please file an issue.', ); - let node: Fiber = fiber; - while (node.tag !== HostRoot) { - if (isContextProvider(node)) { - return node.stateNode.__reactInternalMemoizedMergedChildContext; + let node = fiber; + do { + switch (node.tag) { + case HostRoot: + return node.stateNode.context; + case ClassComponent: { + const Component = node.type; + if (isContextProvider(Component)) { + return node.stateNode.__reactInternalMemoizedMergedChildContext; + } + break; + } + case ClassComponentLazy: { + const Component = node.type._reactResult; + if (isContextProvider(Component)) { + return node.stateNode.__reactInternalMemoizedMergedChildContext; + } + break; + } } - const parent = node.return; - invariant( - parent, - 'Found unexpected detached subtree parent. ' + - 'This error is likely caused by a bug in React. Please file an issue.', - ); - node = parent; - } - return node.stateNode.context; + node = node.return; + } while (node !== null); + invariant( + false, + 'Found unexpected detached subtree parent. ' + + 'This error is likely caused by a bug in React. Please file an issue.', + ); } export { @@ -321,6 +322,7 @@ export { popTopLevelContextObject, pushTopLevelContextObject, processChildContext, + isContextProvider, pushContextProvider, invalidateContextProvider, findCurrentUnmaskedContext, diff --git a/packages/react-reconciler/src/ReactFiberReconciler.js b/packages/react-reconciler/src/ReactFiberReconciler.js index 1bab73c4704..3af6c4cd471 100644 --- a/packages/react-reconciler/src/ReactFiberReconciler.js +++ b/packages/react-reconciler/src/ReactFiberReconciler.js @@ -37,6 +37,7 @@ import { findCurrentUnmaskedContext, processChildContext, emptyContextObject, + isContextProvider as isLegacyContextProvider, } from './ReactFiberContext'; import {createFiberRoot} from './ReactFiberRoot'; import * as ReactFiberDevToolsHook from './ReactFiberDevToolsHook'; @@ -96,18 +97,14 @@ function getContextForSubtree( const parentContext = findCurrentUnmaskedContext(fiber); if (fiber.tag === ClassComponent) { - const type = fiber.type; - const childContextTypes = type.childContextTypes; - if (childContextTypes !== undefined && childContextTypes !== null) { - return processChildContext(fiber, type, parentContext); + const Component = fiber.type; + if (isLegacyContextProvider(Component)) { + return processChildContext(fiber, Component, parentContext); } } else if (fiber.tag === ClassComponentLazy) { - const type = fiber.type._reactResult; - if (type !== undefined) { - const childContextTypes = type.childContextTypes; - if (childContextTypes !== undefined && childContextTypes !== null) { - return processChildContext(fiber, type, parentContext); - } + const Component = fiber.type._reactResult; + if (isLegacyContextProvider(Component)) { + return processChildContext(fiber, Component, parentContext); } } diff --git a/packages/react-reconciler/src/ReactFiberScheduler.js b/packages/react-reconciler/src/ReactFiberScheduler.js index eba79ed685b..dd22253405f 100644 --- a/packages/react-reconciler/src/ReactFiberScheduler.js +++ b/packages/react-reconciler/src/ReactFiberScheduler.js @@ -111,6 +111,7 @@ import {AsyncMode, ProfileMode} from './ReactTypeOfMode'; import {enqueueUpdate, resetCurrentlyProcessingQueue} from './ReactUpdateQueue'; import {createCapturedValue} from './ReactCapturedValue'; import { + isContextProvider as isLegacyContextProvider, popTopLevelContextObject as popTopLevelLegacyContextObject, popContext as popLegacyContext, } from './ReactFiberContext'; @@ -289,16 +290,15 @@ if (__DEV__ && replayFailedUnitOfWorkWithInvokeGuardedCallback) { popHostContext(failedUnitOfWork); break; case ClassComponent: { - const childContextTypes = failedUnitOfWork.type.childContextTypes; - if (childContextTypes !== null && childContextTypes !== undefined) { + const Component = failedUnitOfWork.type; + if (isLegacyContextProvider(Component)) { popLegacyContext(failedUnitOfWork); } break; } case ClassComponentLazy: { - const childContextTypes = - failedUnitOfWork.type._reactResult.childContextTypes; - if (childContextTypes !== null && childContextTypes !== undefined) { + const Component = failedUnitOfWork.type._reactResult; + if (isLegacyContextProvider(Component)) { popLegacyContext(failedUnitOfWork); } break; diff --git a/packages/react-reconciler/src/ReactFiberUnwindWork.js b/packages/react-reconciler/src/ReactFiberUnwindWork.js index 3f3d8ed99fd..a6da20450f7 100644 --- a/packages/react-reconciler/src/ReactFiberUnwindWork.js +++ b/packages/react-reconciler/src/ReactFiberUnwindWork.js @@ -48,6 +48,7 @@ import { import {logError} from './ReactFiberCommitWork'; import {popHostContainer, popHostContext} from './ReactFiberHostContext'; import { + isContextProvider as isLegacyContextProvider, popContext as popLegacyContext, popTopLevelContextObject as popTopLevelLegacyContextObject, } from './ReactFiberContext'; @@ -385,8 +386,8 @@ function unwindWork( ) { switch (workInProgress.tag) { case ClassComponent: { - const childContextTypes = workInProgress.type.childContextTypes; - if (childContextTypes !== null && childContextTypes !== undefined) { + const Component = workInProgress.type; + if (isLegacyContextProvider(Component)) { popLegacyContext(workInProgress); } const effectTag = workInProgress.effectTag; @@ -397,12 +398,10 @@ function unwindWork( return null; } case ClassComponentLazy: { - const childContextTypes = - workInProgress.type._reactResult.childContextTypes; - if (childContextTypes !== null && childContextTypes !== undefined) { + const Component = workInProgress.type._reactResult; + if (isLegacyContextProvider(Component)) { popLegacyContext(workInProgress); } - popLegacyContext(workInProgress); const effectTag = workInProgress.effectTag; if (effectTag & ShouldCapture) { workInProgress.effectTag = (effectTag & ~ShouldCapture) | DidCapture; From 686aee4978b62a0f38d962ce7db3420f153800d5 Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Wed, 15 Aug 2018 18:21:18 -0700 Subject: [PATCH 11/14] Replace all occurrences of _reactResult with helper --- .../src/ReactFiberBeginWork.js | 19 +++++++++++------- .../src/ReactFiberCompleteWork.js | 3 ++- .../react-reconciler/src/ReactFiberContext.js | 3 ++- .../src/ReactFiberLazyComponent.js | 20 ++++++++++++++++++- .../src/ReactFiberReconciler.js | 3 ++- .../src/ReactFiberScheduler.js | 3 ++- .../__tests__/ReactSuspense-test.internal.js | 2 +- packages/shared/getComponentName.js | 15 ++++++++++---- 8 files changed, 51 insertions(+), 17 deletions(-) diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.js b/packages/react-reconciler/src/ReactFiberBeginWork.js index fdfc31a8f1e..d92ddbbedca 100644 --- a/packages/react-reconciler/src/ReactFiberBeginWork.js +++ b/packages/react-reconciler/src/ReactFiberBeginWork.js @@ -100,7 +100,10 @@ import { resumeMountClassInstance, updateClassInstance, } from './ReactFiberClassComponent'; -import {readLazyComponentType} from './ReactFiberLazyComponent'; +import { + readLazyComponentType, + getResultFromResolvedThenable, +} from './ReactFiberLazyComponent'; import {reassignLazyComponentTag} from './ReactFiber'; const ReactCurrentOwner = ReactSharedInternals.ReactCurrentOwner; @@ -1100,7 +1103,7 @@ function beginWork( } case ClassComponentLazy: { const thenable = workInProgress.type; - const Component = (thenable._reactResult: any); + const Component = getResultFromResolvedThenable(thenable); if (isLegacyContextProvider(Component)) { pushLegacyContextProvider(workInProgress); } @@ -1145,17 +1148,18 @@ function beginWork( } case FunctionalComponent: { const Component = workInProgress.type; + const unresolvedProps = workInProgress.pendingProps; return updateFunctionalComponent( current, workInProgress, Component, - workInProgress.pendingProps, + unresolvedProps, renderExpirationTime, ); } case FunctionalComponentLazy: { const thenable = workInProgress.type; - const Component = (thenable._reactResult: any); + const Component = getResultFromResolvedThenable(thenable); const unresolvedProps = workInProgress.pendingProps; const child = updateFunctionalComponent( current, @@ -1169,17 +1173,18 @@ function beginWork( } case ClassComponent: { const Component = workInProgress.type; + const unresolvedProps = workInProgress.pendingProps; return updateClassComponent( current, workInProgress, Component, - workInProgress.pendingProps, + unresolvedProps, renderExpirationTime, ); } case ClassComponentLazy: { const thenable = workInProgress.type; - const Component = (thenable._reactResult: any); + const Component = getResultFromResolvedThenable(thenable); const unresolvedProps = workInProgress.pendingProps; const child = updateClassComponent( current, @@ -1221,7 +1226,7 @@ function beginWork( } case ForwardRefLazy: const thenable = workInProgress.type; - const Component = (thenable._reactResult: any); + const Component = getResultFromResolvedThenable(thenable); const unresolvedProps = workInProgress.pendingProps; const child = updateForwardRef( current, diff --git a/packages/react-reconciler/src/ReactFiberCompleteWork.js b/packages/react-reconciler/src/ReactFiberCompleteWork.js index 2b63b7f9ab7..71f2dd13ae4 100644 --- a/packages/react-reconciler/src/ReactFiberCompleteWork.js +++ b/packages/react-reconciler/src/ReactFiberCompleteWork.js @@ -72,6 +72,7 @@ import { prepareToHydrateHostTextInstance, popHydrationState, } from './ReactFiberHydrationContext'; +import {getResultFromResolvedThenable} from './ReactFiberLazyComponent'; function markUpdate(workInProgress: Fiber) { // Tag the fiber with an update effect. This turns a Placement into @@ -327,7 +328,7 @@ function completeWork( break; } case ClassComponentLazy: { - const Component = workInProgress.type._reactResult; + const Component = getResultFromResolvedThenable(workInProgress.type); if (isLegacyContextProvider(Component)) { popLegacyContext(workInProgress); } diff --git a/packages/react-reconciler/src/ReactFiberContext.js b/packages/react-reconciler/src/ReactFiberContext.js index b5d5519ee8d..99fa9b79c85 100644 --- a/packages/react-reconciler/src/ReactFiberContext.js +++ b/packages/react-reconciler/src/ReactFiberContext.js @@ -24,6 +24,7 @@ import checkPropTypes from 'prop-types/checkPropTypes'; import * as ReactCurrentFiber from './ReactCurrentFiber'; import {startPhaseTimer, stopPhaseTimer} from './ReactDebugFiberPerf'; import {createCursor, push, pop} from './ReactFiberStack'; +import {getResultFromResolvedThenable} from './ReactFiberLazyComponent'; let warnedAboutMissingGetChildContext; @@ -297,7 +298,7 @@ function findCurrentUnmaskedContext(fiber: Fiber): Object { break; } case ClassComponentLazy: { - const Component = node.type._reactResult; + const Component = getResultFromResolvedThenable(node.type); if (isContextProvider(Component)) { return node.stateNode.__reactInternalMemoizedMergedChildContext; } diff --git a/packages/react-reconciler/src/ReactFiberLazyComponent.js b/packages/react-reconciler/src/ReactFiberLazyComponent.js index b978d76092d..901d6b7353e 100644 --- a/packages/react-reconciler/src/ReactFiberLazyComponent.js +++ b/packages/react-reconciler/src/ReactFiberLazyComponent.js @@ -7,16 +7,34 @@ * @flow */ -type Thenable = { +export type Thenable = { then(resolve: (T) => mixed, reject: (mixed) => mixed): mixed, _reactStatus?: 0 | 1 | 2, _reactResult: any, }; +type ResolvedThenable = { + then(resolve: (T) => mixed, reject: (mixed) => mixed): mixed, + _reactStatus?: 1, + _reactResult: T, +}; + export const Pending = 0; export const Resolved = 1; export const Rejected = 2; +export function getResultFromResolvedThenable( + thenable: ResolvedThenable, +): T { + return thenable._reactResult; +} + +export function refineResolvedThenable( + thenable: Thenable, +): ResolvedThenable | null { + return thenable._reactStatus === Resolved ? thenable._reactResult : null; +} + export function readLazyComponentType(thenable: Thenable): T { const status = thenable._reactStatus; switch (status) { diff --git a/packages/react-reconciler/src/ReactFiberReconciler.js b/packages/react-reconciler/src/ReactFiberReconciler.js index 3af6c4cd471..e9c3b451e37 100644 --- a/packages/react-reconciler/src/ReactFiberReconciler.js +++ b/packages/react-reconciler/src/ReactFiberReconciler.js @@ -60,6 +60,7 @@ import { import {createUpdate, enqueueUpdate} from './ReactUpdateQueue'; import ReactFiberInstrumentation from './ReactFiberInstrumentation'; import * as ReactCurrentFiber from './ReactCurrentFiber'; +import {getResultFromResolvedThenable} from './ReactFiberLazyComponent'; type OpaqueRoot = FiberRoot; @@ -102,7 +103,7 @@ function getContextForSubtree( return processChildContext(fiber, Component, parentContext); } } else if (fiber.tag === ClassComponentLazy) { - const Component = fiber.type._reactResult; + const Component = getResultFromResolvedThenable(fiber.type); if (isLegacyContextProvider(Component)) { return processChildContext(fiber, Component, parentContext); } diff --git a/packages/react-reconciler/src/ReactFiberScheduler.js b/packages/react-reconciler/src/ReactFiberScheduler.js index dd22253405f..4e94658970c 100644 --- a/packages/react-reconciler/src/ReactFiberScheduler.js +++ b/packages/react-reconciler/src/ReactFiberScheduler.js @@ -146,6 +146,7 @@ import { commitDetachRef, } from './ReactFiberCommitWork'; import {Dispatcher} from './ReactFiberDispatcher'; +import {getResultFromResolvedThenable} from './ReactFiberLazyComponent'; export type Deadline = { timeRemaining: () => number, @@ -297,7 +298,7 @@ if (__DEV__ && replayFailedUnitOfWorkWithInvokeGuardedCallback) { break; } case ClassComponentLazy: { - const Component = failedUnitOfWork.type._reactResult; + const Component = getResultFromResolvedThenable(failedUnitOfWork.type); if (isLegacyContextProvider(Component)) { popLegacyContext(failedUnitOfWork); } diff --git a/packages/react-reconciler/src/__tests__/ReactSuspense-test.internal.js b/packages/react-reconciler/src/__tests__/ReactSuspense-test.internal.js index ff641844bcd..ecb99d9ab44 100644 --- a/packages/react-reconciler/src/__tests__/ReactSuspense-test.internal.js +++ b/packages/react-reconciler/src/__tests__/ReactSuspense-test.internal.js @@ -1538,7 +1538,7 @@ describe('ReactSuspense', () => { expect(ReactNoop.flush()).toEqual(['Lazy', 'Sibling', 'A']); expect(ReactNoop.getChildren()).toEqual([span('Sibling'), span('A')]); - // Lazy should not have re-rendered + // Lazy should not re-render stateful.current.setState({text: 'B'}); expect(ReactNoop.flush()).toEqual(['B']); expect(ReactNoop.getChildren()).toEqual([span('Sibling'), span('B')]); diff --git a/packages/shared/getComponentName.js b/packages/shared/getComponentName.js index 39d1f86b296..b5ff03e781f 100644 --- a/packages/shared/getComponentName.js +++ b/packages/shared/getComponentName.js @@ -7,6 +7,8 @@ * @flow */ +import type {Thenable} from 'react-reconciler/src/ReactFiberLazyComponent'; + import warningWithoutStack from 'shared/warningWithoutStack'; import { REACT_ASYNC_MODE_TYPE, @@ -19,7 +21,10 @@ import { REACT_STRICT_MODE_TYPE, REACT_PLACEHOLDER_TYPE, } from 'shared/ReactSymbols'; -import {Resolved, Rejected} from 'react-reconciler/src/ReactFiberLazyComponent'; +import { + refineResolvedThenable, + getResultFromResolvedThenable, +} from 'react-reconciler/src/ReactFiberLazyComponent'; function getComponentName(type: mixed): string | null { if (type == null) { @@ -69,9 +74,11 @@ function getComponentName(type: mixed): string | null { : 'ForwardRef'; } if (typeof type.then === 'function') { - const status = type._reactStatus; - if (status === Resolved || status === Rejected) { - return getComponentName(type._reactResult); + const thenable: Thenable = (type: any); + const resolvedThenable = refineResolvedThenable(thenable); + if (resolvedThenable) { + const Component = getResultFromResolvedThenable(resolvedThenable); + return getComponentName(Component); } } } From 6de7e62b2c51f0bacd1ecf63c1308b69410def55 Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Thu, 16 Aug 2018 08:54:48 -0700 Subject: [PATCH 12/14] Move shared thenable logic to `shared` package --- .../src/ReactFiberBeginWork.js | 6 ++-- .../src/ReactFiberCompleteWork.js | 2 +- .../react-reconciler/src/ReactFiberContext.js | 2 +- .../src/ReactFiberLazyComponent.js | 28 ++------------- .../src/ReactFiberReconciler.js | 2 +- .../src/ReactFiberScheduler.js | 2 +- packages/shared/ReactLazyComponent.js | 36 +++++++++++++++++++ packages/shared/getComponentName.js | 6 ++-- 8 files changed, 47 insertions(+), 37 deletions(-) create mode 100644 packages/shared/ReactLazyComponent.js diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.js b/packages/react-reconciler/src/ReactFiberBeginWork.js index d92ddbbedca..12db7b0dda9 100644 --- a/packages/react-reconciler/src/ReactFiberBeginWork.js +++ b/packages/react-reconciler/src/ReactFiberBeginWork.js @@ -100,10 +100,8 @@ import { resumeMountClassInstance, updateClassInstance, } from './ReactFiberClassComponent'; -import { - readLazyComponentType, - getResultFromResolvedThenable, -} from './ReactFiberLazyComponent'; +import {readLazyComponentType} from './ReactFiberLazyComponent'; +import {getResultFromResolvedThenable} from 'shared/ReactLazyComponent'; import {reassignLazyComponentTag} from './ReactFiber'; const ReactCurrentOwner = ReactSharedInternals.ReactCurrentOwner; diff --git a/packages/react-reconciler/src/ReactFiberCompleteWork.js b/packages/react-reconciler/src/ReactFiberCompleteWork.js index 71f2dd13ae4..c20d0e8ad9b 100644 --- a/packages/react-reconciler/src/ReactFiberCompleteWork.js +++ b/packages/react-reconciler/src/ReactFiberCompleteWork.js @@ -41,6 +41,7 @@ import { } from 'shared/ReactTypeOfWork'; import {Placement, Ref, Update} from 'shared/ReactTypeOfSideEffect'; import invariant from 'shared/invariant'; +import {getResultFromResolvedThenable} from 'shared/ReactLazyComponent'; import { createInstance, @@ -72,7 +73,6 @@ import { prepareToHydrateHostTextInstance, popHydrationState, } from './ReactFiberHydrationContext'; -import {getResultFromResolvedThenable} from './ReactFiberLazyComponent'; function markUpdate(workInProgress: Fiber) { // Tag the fiber with an update effect. This turns a Placement into diff --git a/packages/react-reconciler/src/ReactFiberContext.js b/packages/react-reconciler/src/ReactFiberContext.js index 99fa9b79c85..0a12e724b16 100644 --- a/packages/react-reconciler/src/ReactFiberContext.js +++ b/packages/react-reconciler/src/ReactFiberContext.js @@ -20,11 +20,11 @@ import getComponentName from 'shared/getComponentName'; import invariant from 'shared/invariant'; import warningWithoutStack from 'shared/warningWithoutStack'; import checkPropTypes from 'prop-types/checkPropTypes'; +import {getResultFromResolvedThenable} from 'shared/ReactLazyComponent'; import * as ReactCurrentFiber from './ReactCurrentFiber'; import {startPhaseTimer, stopPhaseTimer} from './ReactDebugFiberPerf'; import {createCursor, push, pop} from './ReactFiberStack'; -import {getResultFromResolvedThenable} from './ReactFiberLazyComponent'; let warnedAboutMissingGetChildContext; diff --git a/packages/react-reconciler/src/ReactFiberLazyComponent.js b/packages/react-reconciler/src/ReactFiberLazyComponent.js index 901d6b7353e..9445ad6341d 100644 --- a/packages/react-reconciler/src/ReactFiberLazyComponent.js +++ b/packages/react-reconciler/src/ReactFiberLazyComponent.js @@ -7,33 +7,9 @@ * @flow */ -export type Thenable = { - then(resolve: (T) => mixed, reject: (mixed) => mixed): mixed, - _reactStatus?: 0 | 1 | 2, - _reactResult: any, -}; +import type {Thenable} from 'shared/ReactLazyComponent'; -type ResolvedThenable = { - then(resolve: (T) => mixed, reject: (mixed) => mixed): mixed, - _reactStatus?: 1, - _reactResult: T, -}; - -export const Pending = 0; -export const Resolved = 1; -export const Rejected = 2; - -export function getResultFromResolvedThenable( - thenable: ResolvedThenable, -): T { - return thenable._reactResult; -} - -export function refineResolvedThenable( - thenable: Thenable, -): ResolvedThenable | null { - return thenable._reactStatus === Resolved ? thenable._reactResult : null; -} +import {Resolved, Rejected, Pending} from 'shared/ReactLazyComponent'; export function readLazyComponentType(thenable: Thenable): T { const status = thenable._reactStatus; diff --git a/packages/react-reconciler/src/ReactFiberReconciler.js b/packages/react-reconciler/src/ReactFiberReconciler.js index e9c3b451e37..280e1cbef20 100644 --- a/packages/react-reconciler/src/ReactFiberReconciler.js +++ b/packages/react-reconciler/src/ReactFiberReconciler.js @@ -31,6 +31,7 @@ import { import getComponentName from 'shared/getComponentName'; import invariant from 'shared/invariant'; import warningWithoutStack from 'shared/warningWithoutStack'; +import {getResultFromResolvedThenable} from 'shared/ReactLazyComponent'; import {getPublicInstance} from './ReactFiberHostConfig'; import { @@ -60,7 +61,6 @@ import { import {createUpdate, enqueueUpdate} from './ReactUpdateQueue'; import ReactFiberInstrumentation from './ReactFiberInstrumentation'; import * as ReactCurrentFiber from './ReactCurrentFiber'; -import {getResultFromResolvedThenable} from './ReactFiberLazyComponent'; type OpaqueRoot = FiberRoot; diff --git a/packages/react-reconciler/src/ReactFiberScheduler.js b/packages/react-reconciler/src/ReactFiberScheduler.js index 4e94658970c..10161f42e45 100644 --- a/packages/react-reconciler/src/ReactFiberScheduler.js +++ b/packages/react-reconciler/src/ReactFiberScheduler.js @@ -52,6 +52,7 @@ import { import getComponentName from 'shared/getComponentName'; import invariant from 'shared/invariant'; import warningWithoutStack from 'shared/warningWithoutStack'; +import {getResultFromResolvedThenable} from 'shared/ReactLazyComponent'; import { scheduleTimeout, @@ -146,7 +147,6 @@ import { commitDetachRef, } from './ReactFiberCommitWork'; import {Dispatcher} from './ReactFiberDispatcher'; -import {getResultFromResolvedThenable} from './ReactFiberLazyComponent'; export type Deadline = { timeRemaining: () => number, diff --git a/packages/shared/ReactLazyComponent.js b/packages/shared/ReactLazyComponent.js new file mode 100644 index 00000000000..608a0ec29f7 --- /dev/null +++ b/packages/shared/ReactLazyComponent.js @@ -0,0 +1,36 @@ +/** + * 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. + * + * @flow + */ + +export type Thenable = { + then(resolve: (T) => mixed, reject: (mixed) => mixed): mixed, + _reactStatus?: 0 | 1 | 2, + _reactResult: any, +}; + +type ResolvedThenable = { + then(resolve: (T) => mixed, reject: (mixed) => mixed): mixed, + _reactStatus?: 1, + _reactResult: T, +}; + +export const Pending = 0; +export const Resolved = 1; +export const Rejected = 2; + +export function getResultFromResolvedThenable( + thenable: ResolvedThenable, +): T { + return thenable._reactResult; +} + +export function refineResolvedThenable( + thenable: Thenable, +): ResolvedThenable | null { + return thenable._reactStatus === Resolved ? thenable._reactResult : null; +} diff --git a/packages/shared/getComponentName.js b/packages/shared/getComponentName.js index b5ff03e781f..e5b39b33c49 100644 --- a/packages/shared/getComponentName.js +++ b/packages/shared/getComponentName.js @@ -7,7 +7,7 @@ * @flow */ -import type {Thenable} from 'react-reconciler/src/ReactFiberLazyComponent'; +import type {Thenable} from 'shared/ReactLazyComponent'; import warningWithoutStack from 'shared/warningWithoutStack'; import { @@ -22,9 +22,9 @@ import { REACT_PLACEHOLDER_TYPE, } from 'shared/ReactSymbols'; import { - refineResolvedThenable, getResultFromResolvedThenable, -} from 'react-reconciler/src/ReactFiberLazyComponent'; + refineResolvedThenable, +} from 'shared/ReactLazyComponent'; function getComponentName(type: mixed): string | null { if (type == null) { From c26a3d4a9d6cac998764ec97059fc02ec5e22aaa Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Thu, 16 Aug 2018 09:07:28 -0700 Subject: [PATCH 13/14] Check type of wrapper object before resolving to `default` export --- .../src/ReactFiberLazyComponent.js | 20 ++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/packages/react-reconciler/src/ReactFiberLazyComponent.js b/packages/react-reconciler/src/ReactFiberLazyComponent.js index 9445ad6341d..faea0eb38bc 100644 --- a/packages/react-reconciler/src/ReactFiberLazyComponent.js +++ b/packages/react-reconciler/src/ReactFiberLazyComponent.js @@ -27,13 +27,19 @@ export function readLazyComponentType(thenable: Thenable): T { resolvedValue => { if (thenable._reactStatus === Pending) { thenable._reactStatus = Resolved; - // If the default value is not empty, assume it's the result of - // an async import() and use that. Otherwise, use resolved value. - const defaultExport = (resolvedValue: any).default; - thenable._reactResult = - defaultExport !== null && defaultExport !== undefined - ? defaultExport - : resolvedValue; + if (typeof resolvedValue === 'object' && resolvedValue !== null) { + // If the `default` property is not empty, assume it's the result + // of an async import() and use that. Otherwise, use the + // resolved value itself. + const defaultExport = (resolvedValue: any).default; + resolvedValue = + typeof defaultExport !== undefined && defaultExport !== null + ? defaultExport + : resolvedValue; + } else { + resolvedValue = resolvedValue; + } + thenable._reactResult = resolvedValue; } }, error => { From 9c55b302bc326a454dfd37a1aad8499c81248b65 Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Thu, 16 Aug 2018 09:17:40 -0700 Subject: [PATCH 14/14] Return resolved tag instead of reassigning --- packages/react-reconciler/src/ReactFiber.js | 7 ++++--- packages/react-reconciler/src/ReactFiberBeginWork.js | 9 ++++++--- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/packages/react-reconciler/src/ReactFiber.js b/packages/react-reconciler/src/ReactFiber.js index 70ff482a0ef..1338b26a61b 100644 --- a/packages/react-reconciler/src/ReactFiber.js +++ b/packages/react-reconciler/src/ReactFiber.js @@ -291,12 +291,12 @@ function shouldConstruct(Component: Function) { ); } -export function reassignLazyComponentTag( +export function resolveLazyComponentTag( fiber: Fiber, Component: Function, ): void { if (typeof Component === 'function') { - fiber.tag = shouldConstruct(Component) + return shouldConstruct(Component) ? ClassComponentLazy : FunctionalComponentLazy; } else if ( @@ -304,8 +304,9 @@ export function reassignLazyComponentTag( Component !== null && Component.$$typeof ) { - fiber.tag = ForwardRefLazy; + return ForwardRefLazy; } + return IndeterminateComponent; } // This is used to create an alternate fiber to do work on. diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.js b/packages/react-reconciler/src/ReactFiberBeginWork.js index 12db7b0dda9..582cefc7aaa 100644 --- a/packages/react-reconciler/src/ReactFiberBeginWork.js +++ b/packages/react-reconciler/src/ReactFiberBeginWork.js @@ -102,7 +102,7 @@ import { } from './ReactFiberClassComponent'; import {readLazyComponentType} from './ReactFiberLazyComponent'; import {getResultFromResolvedThenable} from 'shared/ReactLazyComponent'; -import {reassignLazyComponentTag} from './ReactFiber'; +import {resolveLazyComponentTag} from './ReactFiber'; const ReactCurrentOwner = ReactSharedInternals.ReactCurrentOwner; @@ -628,9 +628,12 @@ function mountIndeterminateComponent( typeof Component.then === 'function' ) { Component = readLazyComponentType(Component); - reassignLazyComponentTag(workInProgress, Component); + const resolvedTag = (workInProgress.tag = resolveLazyComponentTag( + workInProgress, + Component, + )); const resolvedProps = resolveDefaultProps(Component, props); - switch (workInProgress.tag) { + switch (resolvedTag) { case FunctionalComponentLazy: { return updateFunctionalComponent( current,