Skip to content

Commit

Permalink
Lazy components must use React.lazy (facebook#13885)
Browse files Browse the repository at this point in the history
Removes support for using arbitrary promises as the type of a React
element. Instead, promises must be wrapped in React.lazy. This gives us
flexibility later if we need to change the protocol.

The reason is that promises do not provide a way to call their
constructor multiple times. For example:

const promiseForA = new Promise(resolve => {
  fetchA(a => resolve(a));
});

Given a reference to `promiseForA`, there's no way to call `fetchA`
again. Calling `then` on the promise doesn't run the constructor again;
it only attaches another listener.

In the future we will likely introduce an API like `React.eager` that
is similar to `lazy` but eagerly calls the constructor. That gives us
the ability to call the constructor multiple times. E.g. to increase
the priority, or to retry if the first operation failed.
  • Loading branch information
Andrew Clark authored and jetoneza committed Jan 23, 2019
1 parent d74638d commit ed40a84
Show file tree
Hide file tree
Showing 18 changed files with 416 additions and 393 deletions.
5 changes: 0 additions & 5 deletions packages/react-dom/src/__tests__/ReactServerRendering-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -582,11 +582,6 @@ describe('ReactDOMServer', () => {
);
ReactDOMServer.renderToString(<LazyFoo />);
}).toThrow('ReactDOMServer does not yet support lazy-loaded components.');

expect(() => {
const FooPromise = {then() {}};
ReactDOMServer.renderToString(<FooPromise />);
}).toThrow('ReactDOMServer does not yet support lazy-loaded components.');
});

it('should throw (in dev) when children are mutated during render', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -444,15 +444,18 @@ describe('ReactDOMServerHydration', () => {
});

it('should be able to use lazy components after hydrating', async () => {
const Lazy = new Promise(resolve => {
setTimeout(
() =>
resolve(function World() {
return 'world';
}),
1000,
);
});
const Lazy = React.lazy(
() =>
new Promise(resolve => {
setTimeout(
() =>
resolve(function World() {
return 'world';
}),
1000,
);
}),
);
class HelloWorld extends React.Component {
state = {isClient: false};
componentDidMount() {
Expand Down
14 changes: 6 additions & 8 deletions packages/react-dom/src/server/ReactPartialRenderer.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import {
REACT_PROFILER_TYPE,
REACT_PROVIDER_TYPE,
REACT_CONTEXT_TYPE,
REACT_LAZY_TYPE,
} from 'shared/ReactSymbols';

import {
Expand Down Expand Up @@ -1005,14 +1006,11 @@ class ReactDOMServerRenderer {
this.stack.push(frame);
return '';
}
default:
if (typeof elementType.then === 'function') {
invariant(
false,
'ReactDOMServer does not yet support lazy-loaded components.',
);
}
break;
case REACT_LAZY_TYPE:
invariant(
false,
'ReactDOMServer does not yet support lazy-loaded components.',
);
}
}

Expand Down
10 changes: 4 additions & 6 deletions packages/react-reconciler/src/ReactFiber.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ import {
REACT_CONCURRENT_MODE_TYPE,
REACT_SUSPENSE_TYPE,
REACT_PURE_TYPE,
REACT_LAZY_TYPE,
} from 'shared/ReactSymbols';

let hasBadMapPolyfill;
Expand Down Expand Up @@ -461,12 +462,9 @@ export function createFiberFromElement(
case REACT_PURE_TYPE:
fiberTag = PureComponent;
break getTag;
default: {
if (typeof type.then === 'function') {
fiberTag = IndeterminateComponent;
break getTag;
}
}
case REACT_LAZY_TYPE:
fiberTag = IndeterminateComponent;
break getTag;
}
}
let info = '';
Expand Down
15 changes: 8 additions & 7 deletions packages/react-reconciler/src/ReactFiberBeginWork.js
Original file line number Diff line number Diff line change
Expand Up @@ -104,12 +104,13 @@ import {
updateClassInstance,
} from './ReactFiberClassComponent';
import {readLazyComponentType} from './ReactFiberLazyComponent';
import {getResultFromResolvedThenable} from 'shared/ReactLazyComponent';
import {getResultFromResolvedLazyComponent} from 'shared/ReactLazyComponent';
import {
resolveLazyComponentTag,
createFiberFromFragment,
createWorkInProgress,
} from './ReactFiber';
import {REACT_LAZY_TYPE} from 'shared/ReactSymbols';

const ReactCurrentOwner = ReactSharedInternals.ReactCurrentOwner;

Expand Down Expand Up @@ -728,7 +729,7 @@ function mountIndeterminateComponent(
if (
typeof Component === 'object' &&
Component !== null &&
typeof Component.then === 'function'
Component.$$typeof === REACT_LAZY_TYPE
) {
// We can't start a User Timing measurement with correct label yet.
// Cancel and resume right after we know the tag.
Expand Down Expand Up @@ -1422,7 +1423,7 @@ function beginWork(
}
case ClassComponentLazy: {
const thenable = workInProgress.type;
const Component = getResultFromResolvedThenable(thenable);
const Component = getResultFromResolvedLazyComponent(thenable);
if (isLegacyContextProvider(Component)) {
pushLegacyContextProvider(workInProgress);
}
Expand Down Expand Up @@ -1498,7 +1499,7 @@ function beginWork(
}
case FunctionComponentLazy: {
const thenable = workInProgress.type;
const Component = getResultFromResolvedThenable(thenable);
const Component = getResultFromResolvedLazyComponent(thenable);
const unresolvedProps = workInProgress.pendingProps;
const child = updateFunctionComponent(
current,
Expand All @@ -1523,7 +1524,7 @@ function beginWork(
}
case ClassComponentLazy: {
const thenable = workInProgress.type;
const Component = getResultFromResolvedThenable(thenable);
const Component = getResultFromResolvedLazyComponent(thenable);
const unresolvedProps = workInProgress.pendingProps;
const child = updateClassComponent(
current,
Expand Down Expand Up @@ -1565,7 +1566,7 @@ function beginWork(
}
case ForwardRefLazy: {
const thenable = workInProgress.type;
const Component = getResultFromResolvedThenable(thenable);
const Component = getResultFromResolvedLazyComponent(thenable);
const unresolvedProps = workInProgress.pendingProps;
const child = updateForwardRef(
current,
Expand Down Expand Up @@ -1608,7 +1609,7 @@ function beginWork(
}
case PureComponentLazy: {
const thenable = workInProgress.type;
const Component = getResultFromResolvedThenable(thenable);
const Component = getResultFromResolvedLazyComponent(thenable);
const unresolvedProps = workInProgress.pendingProps;
const child = updatePureComponent(
current,
Expand Down
4 changes: 2 additions & 2 deletions packages/react-reconciler/src/ReactFiberCompleteWork.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ import {
} from 'shared/ReactWorkTags';
import {Placement, Ref, Update} from 'shared/ReactSideEffectTags';
import invariant from 'shared/invariant';
import {getResultFromResolvedThenable} from 'shared/ReactLazyComponent';
import {getResultFromResolvedLazyComponent} from 'shared/ReactLazyComponent';

import {
createInstance,
Expand Down Expand Up @@ -552,7 +552,7 @@ function completeWork(
break;
}
case ClassComponentLazy: {
const Component = getResultFromResolvedThenable(workInProgress.type);
const Component = getResultFromResolvedLazyComponent(workInProgress.type);
if (isLegacyContextProvider(Component)) {
popLegacyContext(workInProgress);
}
Expand Down
4 changes: 2 additions & 2 deletions packages/react-reconciler/src/ReactFiberContext.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ 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 {getResultFromResolvedLazyComponent} from 'shared/ReactLazyComponent';

import * as ReactCurrentFiber from './ReactCurrentFiber';
import {startPhaseTimer, stopPhaseTimer} from './ReactDebugFiberPerf';
Expand Down Expand Up @@ -298,7 +298,7 @@ function findCurrentUnmaskedContext(fiber: Fiber): Object {
break;
}
case ClassComponentLazy: {
const Component = getResultFromResolvedThenable(node.type);
const Component = getResultFromResolvedLazyComponent(node.type);
if (isContextProvider(Component)) {
return node.stateNode.__reactInternalMemoizedMergedChildContext;
}
Expand Down
28 changes: 15 additions & 13 deletions packages/react-reconciler/src/ReactFiberLazyComponent.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,26 +7,28 @@
* @flow
*/

import type {Thenable} from 'shared/ReactLazyComponent';
import type {LazyComponent} from 'shared/ReactLazyComponent';

import {Resolved, Rejected, Pending} from 'shared/ReactLazyComponent';

export function readLazyComponentType<T>(thenable: Thenable<T>): T {
const status = thenable._reactStatus;
export function readLazyComponentType<T>(lazyComponent: LazyComponent<T>): T {
const status = lazyComponent._status;
switch (status) {
case Resolved:
const Component: T = thenable._reactResult;
const Component: T = lazyComponent._result;
return Component;
case Rejected:
throw thenable._reactResult;
throw lazyComponent._result;
case Pending:
throw thenable;
throw lazyComponent;
default: {
thenable._reactStatus = Pending;
lazyComponent._status = Pending;
const ctor = lazyComponent._ctor;
const thenable = ctor();
thenable.then(
resolvedValue => {
if (thenable._reactStatus === Pending) {
thenable._reactStatus = Resolved;
if (lazyComponent._status === Pending) {
lazyComponent._status = 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
Expand All @@ -39,13 +41,13 @@ export function readLazyComponentType<T>(thenable: Thenable<T>): T {
} else {
resolvedValue = resolvedValue;
}
thenable._reactResult = resolvedValue;
lazyComponent._result = resolvedValue;
}
},
error => {
if (thenable._reactStatus === Pending) {
thenable._reactStatus = Rejected;
thenable._reactResult = error;
if (lazyComponent._status === Pending) {
lazyComponent._status = Rejected;
lazyComponent._result = error;
}
},
);
Expand Down
4 changes: 2 additions & 2 deletions packages/react-reconciler/src/ReactFiberReconciler.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ import {
import getComponentName from 'shared/getComponentName';
import invariant from 'shared/invariant';
import warningWithoutStack from 'shared/warningWithoutStack';
import {getResultFromResolvedThenable} from 'shared/ReactLazyComponent';
import {getResultFromResolvedLazyComponent} from 'shared/ReactLazyComponent';

import {getPublicInstance} from './ReactFiberHostConfig';
import {
Expand Down Expand Up @@ -107,7 +107,7 @@ function getContextForSubtree(
return processChildContext(fiber, Component, parentContext);
}
} else if (fiber.tag === ClassComponentLazy) {
const Component = getResultFromResolvedThenable(fiber.type);
const Component = getResultFromResolvedLazyComponent(fiber.type);
if (isLegacyContextProvider(Component)) {
return processChildContext(fiber, Component, parentContext);
}
Expand Down
6 changes: 4 additions & 2 deletions packages/react-reconciler/src/ReactFiberScheduler.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ import {
import getComponentName from 'shared/getComponentName';
import invariant from 'shared/invariant';
import warningWithoutStack from 'shared/warningWithoutStack';
import {getResultFromResolvedThenable} from 'shared/ReactLazyComponent';
import {getResultFromResolvedLazyComponent} from 'shared/ReactLazyComponent';

import {
scheduleTimeout,
Expand Down Expand Up @@ -312,7 +312,9 @@ if (__DEV__ && replayFailedUnitOfWorkWithInvokeGuardedCallback) {
break;
}
case ClassComponentLazy: {
const Component = getResultFromResolvedThenable(failedUnitOfWork.type);
const Component = getResultFromResolvedLazyComponent(
failedUnitOfWork.type,
);
if (isLegacyContextProvider(Component)) {
popLegacyContext(failedUnitOfWork);
}
Expand Down

0 comments on commit ed40a84

Please sign in to comment.