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 1f377fee3c6..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; @@ -1307,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 01aa5a29c2b..1338b26a61b 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,32 @@ const createFiber = function( return new FiberNode(tag, pendingProps, key, mode); }; -function shouldConstruct(Component) { - 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 resolveLazyComponentTag( + fiber: Fiber, + Component: Function, +): void { + if (typeof Component === 'function') { + return shouldConstruct(Component) + ? ClassComponentLazy + : FunctionalComponentLazy; + } else if ( + Component !== undefined && + Component !== null && + Component.$$typeof + ) { + return ForwardRefLazy; + } + return IndeterminateComponent; } // This is used to create an alternate fiber to do work on. @@ -390,7 +417,7 @@ export function createFiberFromElement( let fiber; const type = element.type; const key = element.key; - let pendingProps = element.props; + const pendingProps = element.props; let fiberTag; if (typeof type === 'function') { @@ -398,7 +425,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 +446,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,49 +509,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: { - 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, diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.js b/packages/react-reconciler/src/ReactFiberBeginWork.js index 84a835a9e00..582cefc7aaa 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, @@ -81,6 +84,7 @@ import { getUnmaskedContext, hasContextChanged as hasLegacyContextChanged, pushContextProvider as pushLegacyContextProvider, + isContextProvider as isLegacyContextProvider, pushTopLevelContextObject, invalidateContextProvider, } from './ReactFiberContext'; @@ -96,6 +100,9 @@ import { resumeMountClassInstance, updateClassInstance, } from './ReactFiberClassComponent'; +import {readLazyComponentType} from './ReactFiberLazyComponent'; +import {getResultFromResolvedThenable} from 'shared/ReactLazyComponent'; +import {resolveLazyComponentTag} from './ReactFiber'; const ReactCurrentOwner = ReactSharedInternals.ReactCurrentOwner; @@ -145,10 +152,11 @@ export function reconcileChildren( function updateForwardRef( current: Fiber | null, workInProgress: Fiber, + type: any, + nextProps: any, renderExpirationTime: ExpirationTime, ) { - const render = workInProgress.type.render; - const nextProps = workInProgress.pendingProps; + const render = type.render; const ref = workInProgress.ref; if (hasLegacyContextChanged()) { // Normally we can bail out on props equality but if context has changed @@ -250,11 +258,11 @@ function markRef(current: Fiber | null, workInProgress: Fiber) { function updateFunctionalComponent( current, workInProgress, + Component, + nextProps: any, renderExpirationTime, ) { - const fn = workInProgress.type; - const nextProps = workInProgress.pendingProps; - const unmaskedContext = getUnmaskedContext(workInProgress); + const unmaskedContext = getUnmaskedContext(workInProgress, Component); const context = getMaskedContext(workInProgress, unmaskedContext); let nextChildren; @@ -262,10 +270,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,12 +291,20 @@ function updateFunctionalComponent( function updateClassComponent( current: Fiber | null, workInProgress: Fiber, + Component: any, + nextProps, renderExpirationTime: ExpirationTime, ) { // 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; + if (isLegacyContextProvider(Component)) { + hasContext = true; + pushLegacyContextProvider(workInProgress); + } else { + hasContext = false; + } prepareToReadContext(workInProgress, renderExpirationTime); let shouldUpdate; @@ -297,16 +313,23 @@ function updateClassComponent( // In the initial pass we might need to construct the instance. constructClassInstance( workInProgress, - workInProgress.pendingProps, + Component, + nextProps, + renderExpirationTime, + ); + mountClassInstance( + workInProgress, + Component, + nextProps, renderExpirationTime, ); - mountClassInstance(workInProgress, renderExpirationTime); - shouldUpdate = true; } else { // In a resume, we'll already have an instance we can reuse. shouldUpdate = resumeMountClassInstance( workInProgress, + Component, + nextProps, renderExpirationTime, ); } @@ -314,12 +337,15 @@ function updateClassComponent( shouldUpdate = updateClassInstance( current, workInProgress, + Component, + nextProps, renderExpirationTime, ); } return finishClassComponent( current, workInProgress, + Component, shouldUpdate, hasContext, renderExpirationTime, @@ -329,6 +355,7 @@ function updateClassComponent( function finishClassComponent( current: Fiber | null, workInProgress: Fiber, + Component: any, shouldUpdate: boolean, hasContext: boolean, renderExpirationTime: ExpirationTime, @@ -341,7 +368,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 +378,6 @@ function finishClassComponent( ); } - const ctor = workInProgress.type; const instance = workInProgress.stateNode; // Rerender @@ -360,7 +386,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 +439,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; @@ -568,9 +594,25 @@ 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 baseProps; +} + function mountIndeterminateComponent( current, workInProgress, + Component, renderExpirationTime, ) { invariant( @@ -578,9 +620,61 @@ 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; + const props = workInProgress.pendingProps; - const unmaskedContext = getUnmaskedContext(workInProgress); + if ( + typeof Component === 'object' && + Component !== null && + typeof Component.then === 'function' + ) { + Component = readLazyComponentType(Component); + const resolvedTag = (workInProgress.tag = resolveLazyComponentTag( + workInProgress, + Component, + )); + const resolvedProps = resolveDefaultProps(Component, props); + switch (resolvedTag) { + case FunctionalComponentLazy: { + return updateFunctionalComponent( + current, + workInProgress, + Component, + resolvedProps, + renderExpirationTime, + ); + } + case ClassComponentLazy: { + return updateClassComponent( + current, + workInProgress, + Component, + resolvedProps, + renderExpirationTime, + ); + } + case ForwardRefLazy: { + return updateForwardRef( + current, + workInProgress, + Component, + resolvedProps, + 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, + ); + } + } + } + + const unmaskedContext = getUnmaskedContext(workInProgress, Component); const context = getMaskedContext(workInProgress, unmaskedContext); prepareToReadContext(workInProgress, renderExpirationTime); @@ -588,8 +682,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 +705,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,15 +718,19 @@ 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; // 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; + if (isLegacyContextProvider(Component)) { + hasContext = true; + pushLegacyContextProvider(workInProgress); + } else { + hasContext = false; + } workInProgress.memoizedState = value.state !== null && value.state !== undefined ? value.state : null; @@ -638,16 +739,18 @@ function mountIndeterminateComponent( if (typeof getDerivedStateFromProps === 'function') { applyDerivedStateFromProps( workInProgress, + Component, getDerivedStateFromProps, props, ); } adoptClassInstance(workInProgress, value); - mountClassInstance(workInProgress, renderExpirationTime); + mountClassInstance(workInProgress, Component, props, renderExpirationTime); return finishClassComponent( current, workInProgress, + Component, true, hasContext, renderExpirationTime, @@ -656,8 +759,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 +789,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( @@ -994,9 +1095,21 @@ function beginWork( case HostComponent: pushHostContext(workInProgress); break; - case ClassComponent: - pushLegacyContextProvider(workInProgress); + case ClassComponent: { + const Component = workInProgress.type; + if (isLegacyContextProvider(Component)) { + pushLegacyContextProvider(workInProgress); + } + break; + } + case ClassComponentLazy: { + const thenable = workInProgress.type; + const Component = getResultFromResolvedThenable(thenable); + if (isLegacyContextProvider(Component)) { + pushLegacyContextProvider(workInProgress); + } break; + } case HostPortal: pushHostContainer( workInProgress, @@ -1025,24 +1138,65 @@ function beginWork( workInProgress.expirationTime = NoWork; switch (workInProgress.tag) { - case IndeterminateComponent: + case IndeterminateComponent: { + const Component = workInProgress.type; return mountIndeterminateComponent( current, workInProgress, + Component, renderExpirationTime, ); - case FunctionalComponent: + } + case FunctionalComponent: { + const Component = workInProgress.type; + const unresolvedProps = workInProgress.pendingProps; return updateFunctionalComponent( current, workInProgress, + Component, + unresolvedProps, + renderExpirationTime, + ); + } + case FunctionalComponentLazy: { + const thenable = workInProgress.type; + const Component = getResultFromResolvedThenable(thenable); + const unresolvedProps = workInProgress.pendingProps; + const child = updateFunctionalComponent( + current, + workInProgress, + Component, + resolveDefaultProps(Component, unresolvedProps), renderExpirationTime, ); - case ClassComponent: + workInProgress.memoizedProps = unresolvedProps; + return child; + } + case ClassComponent: { + const Component = workInProgress.type; + const unresolvedProps = workInProgress.pendingProps; return updateClassComponent( current, workInProgress, + Component, + unresolvedProps, renderExpirationTime, ); + } + case ClassComponentLazy: { + const thenable = workInProgress.type; + const Component = getResultFromResolvedThenable(thenable); + 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); case HostComponent: @@ -1061,8 +1215,29 @@ function beginWork( workInProgress, renderExpirationTime, ); - case ForwardRef: - return updateForwardRef(current, workInProgress, renderExpirationTime); + case ForwardRef: { + const type = workInProgress.type; + return updateForwardRef( + current, + workInProgress, + type, + workInProgress.pendingProps, + renderExpirationTime, + ); + } + case ForwardRefLazy: + const thenable = workInProgress.type; + const Component = getResultFromResolvedThenable(thenable); + 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 ba7b9d2e0ed..a4b3692a5af 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, newProps: 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 = @@ -384,7 +383,7 @@ function checkClassInstance(workInProgress: Fiber) { '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 ' + @@ -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 unmaskedContext = getUnmaskedContext(workInProgress, ctor); + 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,19 +657,18 @@ function callComponentWillReceiveProps( // Invokes the mount life-cycles on a previously never rendered instance. function mountClassInstance( workInProgress: Fiber, + ctor: any, + newProps: any, renderExpirationTime: ExpirationTime, ): void { - const ctor = workInProgress.type; - if (__DEV__) { - checkClassInstance(workInProgress); + checkClassInstance(workInProgress, ctor, newProps); } const instance = workInProgress.stateNode; - const props = workInProgress.pendingProps; - const unmaskedContext = getUnmaskedContext(workInProgress); + const unmaskedContext = getUnmaskedContext(workInProgress, ctor); - instance.props = props; + instance.props = newProps; instance.state = workInProgress.memoizedState; instance.refs = emptyRefsObject; instance.context = getMaskedContext(workInProgress, unmaskedContext); @@ -700,7 +699,7 @@ function mountClassInstance( processUpdateQueue( workInProgress, updateQueue, - props, + newProps, instance, renderExpirationTime, ); @@ -709,7 +708,12 @@ function mountClassInstance( const getDerivedStateFromProps = ctor.getDerivedStateFromProps; if (typeof getDerivedStateFromProps === 'function') { - applyDerivedStateFromProps(workInProgress, getDerivedStateFromProps, props); + applyDerivedStateFromProps( + workInProgress, + ctor, + getDerivedStateFromProps, + newProps, + ); instance.state = workInProgress.memoizedState; } @@ -729,7 +733,7 @@ function mountClassInstance( processUpdateQueue( workInProgress, updateQueue, - props, + newProps, instance, renderExpirationTime, ); @@ -744,17 +748,17 @@ function mountClassInstance( function resumeMountClassInstance( workInProgress: Fiber, + ctor: any, + newProps: any, renderExpirationTime: ExpirationTime, ): boolean { - const ctor = workInProgress.type; const instance = workInProgress.stateNode; const oldProps = workInProgress.memoizedProps; - const newProps = workInProgress.pendingProps; instance.props = oldProps; const oldContext = instance.context; - const nextLegacyUnmaskedContext = getUnmaskedContext(workInProgress); + const nextLegacyUnmaskedContext = getUnmaskedContext(workInProgress, ctor); const nextLegacyContext = getMaskedContext( workInProgress, nextLegacyUnmaskedContext, @@ -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,17 +887,17 @@ function resumeMountClassInstance( function updateClassInstance( current: Fiber, workInProgress: Fiber, + ctor: any, + newProps: any, renderExpirationTime: ExpirationTime, ): boolean { - const ctor = workInProgress.type; const instance = workInProgress.stateNode; const oldProps = workInProgress.memoizedProps; - const newProps = workInProgress.pendingProps; instance.props = oldProps; const oldContext = instance.context; - const nextLegacyUnmaskedContext = getUnmaskedContext(workInProgress); + const nextLegacyUnmaskedContext = getUnmaskedContext(workInProgress, ctor); const nextLegacyContext = getMaskedContext( workInProgress, nextLegacyUnmaskedContext, @@ -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..c20d0e8ad9b 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,9 +37,11 @@ import { Mode, Profiler, PlaceholderComponent, + ForwardRefLazy, } from 'shared/ReactTypeOfWork'; import {Placement, Ref, Update} from 'shared/ReactTypeOfSideEffect'; import invariant from 'shared/invariant'; +import {getResultFromResolvedThenable} from 'shared/ReactLazyComponent'; import { createInstance, @@ -59,7 +63,8 @@ import { popHostContainer, } from './ReactFiberHostContext'; import { - popContextProvider as popLegacyContextProvider, + isContextProvider as isLegacyContextProvider, + popContext as popLegacyContext, popTopLevelContextObject as popTopLevelLegacyContextObject, } from './ReactFiberContext'; import {popProvider} from './ReactFiberNewContext'; @@ -313,10 +318,20 @@ 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 Component = workInProgress.type; + if (isLegacyContextProvider(Component)) { + popLegacyContext(workInProgress); + } + break; + } + case ClassComponentLazy: { + const Component = getResultFromResolvedThenable(workInProgress.type); + if (isLegacyContextProvider(Component)) { + popLegacyContext(workInProgress); + } break; } case HostRoot: { @@ -479,6 +494,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..0a12e724b16 100644 --- a/packages/react-reconciler/src/ReactFiberContext.js +++ b/packages/react-reconciler/src/ReactFiberContext.js @@ -11,11 +11,16 @@ 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'; import checkPropTypes from 'prop-types/checkPropTypes'; +import {getResultFromResolvedThenable} from 'shared/ReactLazyComponent'; import * as ReactCurrentFiber from './ReactCurrentFiber'; import {startPhaseTimer, stopPhaseTimer} from './ReactDebugFiberPerf'; @@ -41,8 +46,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 @@ -113,19 +121,12 @@ 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 isContextProvider(type: Function): boolean { + const childContextTypes = type.childContextTypes; + return childContextTypes !== null && childContextTypes !== undefined; } -function popContextProvider(fiber: Fiber): void { - if (!isContextProvider(fiber)) { - return; - } - +function popContext(fiber: Fiber): void { pop(didPerformWorkStackCursor, fiber); pop(contextStackCursor, fiber); } @@ -150,9 +151,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. @@ -214,10 +218,6 @@ function processChildContext(fiber: Fiber, parentContext: Object): Object { } 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, @@ -241,6 +241,7 @@ function pushContextProvider(workInProgress: Fiber): boolean { function invalidateContextProvider( workInProgress: Fiber, + type: any, didChange: boolean, ): void { const instance = workInProgress.stateNode; @@ -254,7 +255,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,25 +279,39 @@ 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.', ); - 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 = getResultFromResolvedThenable(node.type); + 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 { @@ -300,12 +319,11 @@ export { cacheContext, getMaskedContext, hasContextChanged, - isContextConsumer, - isContextProvider, - popContextProvider, + popContext, popTopLevelContextObject, pushTopLevelContextObject, processChildContext, + isContextProvider, pushContextProvider, invalidateContextProvider, findCurrentUnmaskedContext, diff --git a/packages/react-reconciler/src/ReactFiberLazyComponent.js b/packages/react-reconciler/src/ReactFiberLazyComponent.js new file mode 100644 index 00000000000..faea0eb38bc --- /dev/null +++ b/packages/react-reconciler/src/ReactFiberLazyComponent.js @@ -0,0 +1,55 @@ +/** + * 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 + */ + +import type {Thenable} from 'shared/ReactLazyComponent'; + +import {Resolved, Rejected, Pending} from 'shared/ReactLazyComponent'; + +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 (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 => { + 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..280e1cbef20 100644 --- a/packages/react-reconciler/src/ReactFiberReconciler.js +++ b/packages/react-reconciler/src/ReactFiberReconciler.js @@ -23,17 +23,22 @@ 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'; +import {getResultFromResolvedThenable} from 'shared/ReactLazyComponent'; import {getPublicInstance} from './ReactFiberHostConfig'; import { findCurrentUnmaskedContext, - isContextProvider, processChildContext, emptyContextObject, + isContextProvider as isLegacyContextProvider, } from './ReactFiberContext'; import {createFiberRoot} from './ReactFiberRoot'; import * as ReactFiberDevToolsHook from './ReactFiberDevToolsHook'; @@ -91,9 +96,20 @@ function getContextForSubtree( const fiber = ReactInstanceMap.get(parentComponent); const parentContext = findCurrentUnmaskedContext(fiber); - return isContextProvider(fiber) - ? processChildContext(fiber, parentContext) - : parentContext; + + if (fiber.tag === ClassComponent) { + const Component = fiber.type; + if (isLegacyContextProvider(Component)) { + return processChildContext(fiber, Component, parentContext); + } + } else if (fiber.tag === ClassComponentLazy) { + const Component = getResultFromResolvedThenable(fiber.type); + if (isLegacyContextProvider(Component)) { + return processChildContext(fiber, Component, parentContext); + } + } + + return parentContext; } function scheduleRootUpdate( diff --git a/packages/react-reconciler/src/ReactFiberScheduler.js b/packages/react-reconciler/src/ReactFiberScheduler.js index b8bad42320a..10161f42e45 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, @@ -51,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, @@ -110,8 +112,9 @@ import {AsyncMode, ProfileMode} from './ReactTypeOfMode'; import {enqueueUpdate, resetCurrentlyProcessingQueue} from './ReactUpdateQueue'; import {createCapturedValue} from './ReactCapturedValue'; import { + isContextProvider as isLegacyContextProvider, popTopLevelContextObject as popTopLevelLegacyContextObject, - popContextProvider as popLegacyContextProvider, + popContext as popLegacyContext, } from './ReactFiberContext'; import {popProvider, resetContextDependences} from './ReactFiberNewContext'; import {popHostContext, popHostContainer} from './ReactFiberHostContext'; @@ -287,9 +290,20 @@ if (__DEV__ && replayFailedUnitOfWorkWithInvokeGuardedCallback) { case HostComponent: popHostContext(failedUnitOfWork); break; - case ClassComponent: - popLegacyContextProvider(failedUnitOfWork); + case ClassComponent: { + const Component = failedUnitOfWork.type; + if (isLegacyContextProvider(Component)) { + popLegacyContext(failedUnitOfWork); + } + break; + } + case ClassComponentLazy: { + const Component = getResultFromResolvedThenable(failedUnitOfWork.type); + if (isLegacyContextProvider(Component)) { + popLegacyContext(failedUnitOfWork); + } break; + } case HostPortal: popHostContainer(failedUnitOfWork); break; @@ -1292,6 +1306,7 @@ function dispatch( while (fiber !== null) { switch (fiber.tag) { case ClassComponent: + case ClassComponentLazy: const ctor = fiber.type; const instance = fiber.stateNode; if ( @@ -1499,7 +1514,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 +1522,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..a6da20450f7 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,8 @@ import { import {logError} from './ReactFiberCommitWork'; import {popHostContainer, popHostContext} from './ReactFiberHostContext'; import { - popContextProvider as popLegacyContextProvider, + isContextProvider as isLegacyContextProvider, + popContext as popLegacyContext, popTopLevelContextObject as popTopLevelLegacyContextObject, } from './ReactFiberContext'; import {popProvider} from './ReactFiberNewContext'; @@ -249,7 +251,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 +348,7 @@ function throwException( return; } case ClassComponent: + case ClassComponentLazy: // Capture and retry const errorInfo = value; const ctor = workInProgress.type; @@ -380,7 +386,22 @@ function unwindWork( ) { switch (workInProgress.tag) { case ClassComponent: { - popLegacyContextProvider(workInProgress); + const Component = workInProgress.type; + if (isLegacyContextProvider(Component)) { + popLegacyContext(workInProgress); + } + const effectTag = workInProgress.effectTag; + if (effectTag & ShouldCapture) { + workInProgress.effectTag = (effectTag & ~ShouldCapture) | DidCapture; + return workInProgress; + } + return null; + } + case ClassComponentLazy: { + const Component = workInProgress.type._reactResult; + if (isLegacyContextProvider(Component)) { + popLegacyContext(workInProgress); + } const effectTag = workInProgress.effectTag; if (effectTag & ShouldCapture) { workInProgress.effectTag = (effectTag & ~ShouldCapture) | DidCapture; @@ -426,7 +447,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 c95e050cfe0..ecb99d9ab44 100644 --- a/packages/react-reconciler/src/__tests__/ReactSuspense-test.internal.js +++ b/packages/react-reconciler/src/__tests__/ReactSuspense-test.internal.js @@ -1347,6 +1347,204 @@ 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('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 re-render + 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 () => { class TextWithLifecycle extends React.Component { componentDidMount() { diff --git a/packages/react-test-renderer/src/ReactTestRenderer.js b/packages/react-test-renderer/src/ReactTestRenderer.js index 9102963b6af..52bccda4c01 100644 --- a/packages/react-test-renderer/src/ReactTestRenderer.js +++ b/packages/react-test-renderer/src/ReactTestRenderer.js @@ -17,7 +17,9 @@ import {findCurrentFiberUsingSlowPath} from 'react-reconciler/reflection'; import { Fragment, FunctionalComponent, + FunctionalComponentLazy, ClassComponent, + ClassComponentLazy, HostComponent, HostPortal, HostText, @@ -27,6 +29,7 @@ import { Mode, ForwardRef, Profiler, + ForwardRefLazy, } from 'shared/ReactTypeOfWork'; import invariant from 'shared/invariant'; import ReactVersion from 'shared/ReactVersion'; @@ -148,6 +151,17 @@ function toTree(node: ?Fiber) { instance: node.stateNode, rendered: childrenToTree(node.child), }; + case ClassComponentLazy: { + const thenable = node.type; + const type = thenable._reactResult; + return { + nodeType: 'component', + type, + props: {...node.memoizedProps}, + instance: node.stateNode, + rendered: childrenToTree(node.child), + }; + } case FunctionalComponent: return { nodeType: 'component', @@ -156,6 +170,17 @@ function toTree(node: ?Fiber) { instance: null, rendered: childrenToTree(node.child), }; + case FunctionalComponentLazy: { + const thenable = node.type; + const type = thenable._reactResult; + return { + nodeType: 'component', + type: type, + props: {...node.memoizedProps}, + instance: node.stateNode, + rendered: childrenToTree(node.child), + }; + } case HostComponent: { return { nodeType: 'host', @@ -173,6 +198,7 @@ function toTree(node: ?Fiber) { case Mode: case Profiler: case ForwardRef: + case ForwardRefLazy: return childrenToTree(node.child); default: invariant( @@ -198,9 +224,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/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/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..e5b39b33c49 100644 --- a/packages/shared/getComponentName.js +++ b/packages/shared/getComponentName.js @@ -7,6 +7,8 @@ * @flow */ +import type {Thenable} from 'shared/ReactLazyComponent'; + import warningWithoutStack from 'shared/warningWithoutStack'; import { REACT_ASYNC_MODE_TYPE, @@ -19,6 +21,10 @@ import { REACT_STRICT_MODE_TYPE, REACT_PLACEHOLDER_TYPE, } from 'shared/ReactSymbols'; +import { + getResultFromResolvedThenable, + refineResolvedThenable, +} from 'shared/ReactLazyComponent'; function getComponentName(type: mixed): string | null { if (type == null) { @@ -67,6 +73,14 @@ function getComponentName(type: mixed): string | null { ? `ForwardRef(${functionName})` : 'ForwardRef'; } + if (typeof type.then === 'function') { + const thenable: Thenable = (type: any); + const resolvedThenable = refineResolvedThenable(thenable); + if (resolvedThenable) { + const Component = getResultFromResolvedThenable(resolvedThenable); + return getComponentName(Component); + } + } } return null; } 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)) );