diff --git a/packages/react-dom/src/__tests__/ReactServerRendering-test.js b/packages/react-dom/src/__tests__/ReactServerRendering-test.js
index 27b30f9bb03e2..f15cb5557e5c8 100644
--- a/packages/react-dom/src/__tests__/ReactServerRendering-test.js
+++ b/packages/react-dom/src/__tests__/ReactServerRendering-test.js
@@ -582,11 +582,6 @@ describe('ReactDOMServer', () => {
);
ReactDOMServer.renderToString();
}).toThrow('ReactDOMServer does not yet support lazy-loaded components.');
-
- expect(() => {
- const FooPromise = {then() {}};
- ReactDOMServer.renderToString();
- }).toThrow('ReactDOMServer does not yet support lazy-loaded components.');
});
it('should throw (in dev) when children are mutated during render', () => {
diff --git a/packages/react-dom/src/__tests__/ReactServerRenderingHydration-test.js b/packages/react-dom/src/__tests__/ReactServerRenderingHydration-test.js
index 5d1ad4eb526ca..17b8932e6973c 100644
--- a/packages/react-dom/src/__tests__/ReactServerRenderingHydration-test.js
+++ b/packages/react-dom/src/__tests__/ReactServerRenderingHydration-test.js
@@ -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() {
diff --git a/packages/react-dom/src/server/ReactPartialRenderer.js b/packages/react-dom/src/server/ReactPartialRenderer.js
index dc79c3fb28031..7d6cfc729e291 100644
--- a/packages/react-dom/src/server/ReactPartialRenderer.js
+++ b/packages/react-dom/src/server/ReactPartialRenderer.js
@@ -38,6 +38,7 @@ import {
REACT_PROFILER_TYPE,
REACT_PROVIDER_TYPE,
REACT_CONTEXT_TYPE,
+ REACT_LAZY_TYPE,
} from 'shared/ReactSymbols';
import {
@@ -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.',
+ );
}
}
diff --git a/packages/react-reconciler/src/ReactFiber.js b/packages/react-reconciler/src/ReactFiber.js
index 3f508efe63f2d..3ad8fccdcddfa 100644
--- a/packages/react-reconciler/src/ReactFiber.js
+++ b/packages/react-reconciler/src/ReactFiber.js
@@ -60,6 +60,7 @@ import {
REACT_CONCURRENT_MODE_TYPE,
REACT_SUSPENSE_TYPE,
REACT_PURE_TYPE,
+ REACT_LAZY_TYPE,
} from 'shared/ReactSymbols';
let hasBadMapPolyfill;
@@ -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 = '';
diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.js b/packages/react-reconciler/src/ReactFiberBeginWork.js
index 6dfed8599f8b9..0cf92b4b27342 100644
--- a/packages/react-reconciler/src/ReactFiberBeginWork.js
+++ b/packages/react-reconciler/src/ReactFiberBeginWork.js
@@ -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;
@@ -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.
@@ -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);
}
@@ -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,
@@ -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,
@@ -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,
@@ -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,
diff --git a/packages/react-reconciler/src/ReactFiberCompleteWork.js b/packages/react-reconciler/src/ReactFiberCompleteWork.js
index e3e5df52683ae..d13bd2c66b8df 100644
--- a/packages/react-reconciler/src/ReactFiberCompleteWork.js
+++ b/packages/react-reconciler/src/ReactFiberCompleteWork.js
@@ -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,
@@ -552,7 +552,7 @@ function completeWork(
break;
}
case ClassComponentLazy: {
- const Component = getResultFromResolvedThenable(workInProgress.type);
+ const Component = getResultFromResolvedLazyComponent(workInProgress.type);
if (isLegacyContextProvider(Component)) {
popLegacyContext(workInProgress);
}
diff --git a/packages/react-reconciler/src/ReactFiberContext.js b/packages/react-reconciler/src/ReactFiberContext.js
index 246aabda81b09..4ef224e2636f2 100644
--- a/packages/react-reconciler/src/ReactFiberContext.js
+++ b/packages/react-reconciler/src/ReactFiberContext.js
@@ -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';
@@ -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;
}
diff --git a/packages/react-reconciler/src/ReactFiberLazyComponent.js b/packages/react-reconciler/src/ReactFiberLazyComponent.js
index 05d9d193c9268..f39773a6777bb 100644
--- a/packages/react-reconciler/src/ReactFiberLazyComponent.js
+++ b/packages/react-reconciler/src/ReactFiberLazyComponent.js
@@ -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(thenable: Thenable): T {
- const status = thenable._reactStatus;
+export function readLazyComponentType(lazyComponent: LazyComponent): 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
@@ -39,13 +41,13 @@ export function readLazyComponentType(thenable: Thenable): 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;
}
},
);
diff --git a/packages/react-reconciler/src/ReactFiberReconciler.js b/packages/react-reconciler/src/ReactFiberReconciler.js
index 4d383f3354983..75a7efb59b111 100644
--- a/packages/react-reconciler/src/ReactFiberReconciler.js
+++ b/packages/react-reconciler/src/ReactFiberReconciler.js
@@ -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 {
@@ -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);
}
diff --git a/packages/react-reconciler/src/ReactFiberScheduler.js b/packages/react-reconciler/src/ReactFiberScheduler.js
index 413fd10213138..c3a0e983ebb55 100644
--- a/packages/react-reconciler/src/ReactFiberScheduler.js
+++ b/packages/react-reconciler/src/ReactFiberScheduler.js
@@ -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,
@@ -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);
}
diff --git a/packages/react-reconciler/src/__tests__/ReactLazy-test.internal.js b/packages/react-reconciler/src/__tests__/ReactLazy-test.internal.js
new file mode 100644
index 0000000000000..e39fc79c83884
--- /dev/null
+++ b/packages/react-reconciler/src/__tests__/ReactLazy-test.internal.js
@@ -0,0 +1,309 @@
+let React;
+let ReactTestRenderer;
+let ReactFeatureFlags;
+let Suspense;
+let lazy;
+
+describe('ReactLazy', () => {
+ beforeEach(() => {
+ jest.resetModules();
+ ReactFeatureFlags = require('shared/ReactFeatureFlags');
+ ReactFeatureFlags.debugRenderPhaseSideEffectsForStrictMode = false;
+ ReactFeatureFlags.replayFailedUnitOfWorkWithInvokeGuardedCallback = false;
+ React = require('react');
+ Suspense = React.unstable_Suspense;
+ lazy = React.lazy;
+ ReactTestRenderer = require('react-test-renderer');
+ });
+
+ function Text(props) {
+ ReactTestRenderer.unstable_yield(props.text);
+ return props.text;
+ }
+
+ it('suspends until module has loaded', async () => {
+ const LazyText = lazy(async () => Text);
+
+ const root = ReactTestRenderer.create(
+ }>
+
+ ,
+ {
+ unstable_isConcurrent: true,
+ },
+ );
+
+ expect(root).toFlushAndYield(['Loading...']);
+ expect(root).toMatchRenderedOutput(null);
+
+ await LazyText;
+
+ expect(root).toFlushAndYield(['Hi']);
+ expect(root).toMatchRenderedOutput('Hi');
+
+ // Should not suspend on update
+ root.update(
+ }>
+
+ ,
+ );
+ expect(root).toFlushAndYield(['Hi again']);
+ expect(root).toMatchRenderedOutput('Hi again');
+ });
+
+ it('uses `default` property, if it exists', async () => {
+ const LazyText = lazy(async () => ({default: Text}));
+
+ const root = ReactTestRenderer.create(
+ }>
+
+ ,
+ {
+ unstable_isConcurrent: true,
+ },
+ );
+ expect(root).toFlushAndYield(['Loading...']);
+ expect(root).toMatchRenderedOutput(null);
+
+ await LazyText;
+
+ expect(root).toFlushAndYield(['Hi']);
+ expect(root).toMatchRenderedOutput('Hi');
+
+ // Should not suspend on update
+ root.update(
+ }>
+
+ ,
+ );
+ expect(root).toFlushAndYield(['Hi again']);
+ expect(root).toMatchRenderedOutput('Hi again');
+ });
+
+ it('throws if promise rejects', async () => {
+ const LazyText = lazy(async () => {
+ throw new Error('Bad network');
+ });
+
+ const root = ReactTestRenderer.create(
+ }>
+
+ ,
+ {
+ unstable_isConcurrent: true,
+ },
+ );
+
+ expect(root).toFlushAndYield(['Loading...']);
+ expect(root).toMatchRenderedOutput(null);
+
+ try {
+ await LazyText;
+ } catch (e) {}
+
+ expect(root).toFlushAndThrow('Bad network');
+ });
+
+ it('mount and reorder', async () => {
+ class Child extends React.Component {
+ componentDidMount() {
+ ReactTestRenderer.unstable_yield('Did mount: ' + this.props.label);
+ }
+ componentDidUpdate() {
+ ReactTestRenderer.unstable_yield('Did update: ' + this.props.label);
+ }
+ render() {
+ return ;
+ }
+ }
+
+ const LazyChildA = lazy(async () => Child);
+ const LazyChildB = lazy(async () => Child);
+
+ function Parent({swap}) {
+ return (
+ }>
+ {swap
+ ? [
+ ,
+ ,
+ ]
+ : [
+ ,
+ ,
+ ]}
+
+ );
+ }
+
+ const root = ReactTestRenderer.create(, {
+ unstable_isConcurrent: true,
+ });
+
+ expect(root).toFlushAndYield(['Loading...']);
+ expect(root).toMatchRenderedOutput(null);
+
+ await LazyChildA;
+ await LazyChildB;
+
+ expect(root).toFlushAndYield(['A', 'B', 'Did mount: A', 'Did mount: B']);
+ expect(root).toMatchRenderedOutput('AB');
+
+ // Swap the position of A and B
+ root.update();
+ expect(root).toFlushAndYield(['B', 'A', 'Did update: B', 'Did update: A']);
+ expect(root).toMatchRenderedOutput('BA');
+ });
+
+ it('resolves defaultProps, on mount and update', async () => {
+ function T(props) {
+ return ;
+ }
+ T.defaultProps = {text: 'Hi'};
+ const LazyText = lazy(async () => T);
+
+ const root = ReactTestRenderer.create(
+ }>
+
+ ,
+ {
+ unstable_isConcurrent: true,
+ },
+ );
+
+ expect(root).toFlushAndYield(['Loading...']);
+ expect(root).toMatchRenderedOutput(null);
+
+ await LazyText;
+
+ expect(root).toFlushAndYield(['Hi']);
+ expect(root).toMatchRenderedOutput('Hi');
+
+ T.defaultProps = {text: 'Hi again'};
+ root.update(
+ }>
+
+ ,
+ );
+ expect(root).toFlushAndYield(['Hi again']);
+ expect(root).toMatchRenderedOutput('Hi again');
+ });
+
+ it('resolves defaultProps without breaking memoization', async () => {
+ function LazyImpl(props) {
+ ReactTestRenderer.unstable_yield('Lazy');
+ return (
+
+
+ {props.children}
+
+ );
+ }
+ LazyImpl.defaultProps = {siblingText: 'Sibling'};
+ const Lazy = lazy(async () => LazyImpl);
+
+ class Stateful extends React.Component {
+ state = {text: 'A'};
+ render() {
+ return ;
+ }
+ }
+
+ const stateful = React.createRef(null);
+
+ const root = ReactTestRenderer.create(
+ }>
+
+
+
+ ,
+ {
+ unstable_isConcurrent: true,
+ },
+ );
+ expect(root).toFlushAndYield(['Loading...']);
+ expect(root).toMatchRenderedOutput(null);
+
+ await Lazy;
+
+ expect(root).toFlushAndYield(['Lazy', 'Sibling', 'A']);
+ expect(root).toMatchRenderedOutput('SiblingA');
+
+ // Lazy should not re-render
+ stateful.current.setState({text: 'B'});
+ expect(root).toFlushAndYield(['B']);
+ expect(root).toMatchRenderedOutput('SiblingB');
+ });
+
+ it('includes lazy-loaded component in warning stack', async () => {
+ const LazyFoo = lazy(() => {
+ ReactTestRenderer.unstable_yield('Started loading');
+ const Foo = props => {[, ]}
;
+ return Promise.resolve(Foo);
+ });
+
+ const root = ReactTestRenderer.create(
+ }>
+
+ ,
+ {
+ unstable_isConcurrent: true,
+ },
+ );
+
+ expect(root).toFlushAndYield(['Started loading', 'Loading...']);
+ expect(root).toMatchRenderedOutput(null);
+
+ await LazyFoo;
+
+ expect(() => {
+ expect(root).toFlushAndYield(['A', 'B']);
+ }).toWarnDev(' in Text (at **)\n' + ' in Foo (at **)');
+ expect(root).toMatchRenderedOutput(AB
);
+ });
+
+ it('supports class and forwardRef components', async () => {
+ const LazyClass = lazy(async () => {
+ class Foo extends React.Component {
+ render() {
+ return ;
+ }
+ }
+ return Foo;
+ });
+
+ const LazyForwardRef = lazy(async () => {
+ class Bar extends React.Component {
+ render() {
+ return ;
+ }
+ }
+ return React.forwardRef((props, ref) => {
+ ReactTestRenderer.unstable_yield('forwardRef');
+ return ;
+ });
+ });
+
+ const ref = React.createRef();
+ const root = ReactTestRenderer.create(
+ }>
+
+
+ ,
+ {
+ unstable_isConcurrent: true,
+ },
+ );
+
+ expect(root).toFlushAndYield(['Loading...']);
+ expect(root).toMatchRenderedOutput(null);
+ expect(ref.current).toBe(null);
+
+ await LazyClass;
+ await LazyForwardRef;
+
+ expect(root).toFlushAndYield(['Foo', 'forwardRef', 'Bar']);
+ expect(root).toMatchRenderedOutput('FooBar');
+ expect(ref.current).not.toBe(null);
+ });
+});
diff --git a/packages/react-reconciler/src/__tests__/ReactPure-test.internal.js b/packages/react-reconciler/src/__tests__/ReactPure-test.internal.js
index e9744254bbd8c..e26259320c446 100644
--- a/packages/react-reconciler/src/__tests__/ReactPure-test.internal.js
+++ b/packages/react-reconciler/src/__tests__/ReactPure-test.internal.js
@@ -42,9 +42,12 @@ describe('pure', () => {
function Indirection(props) {
return ;
}
- return Promise.resolve(Indirection);
+ return React.lazy(async () => Indirection);
+ });
+ sharedTests('lazy', (...args) => {
+ const Pure = React.pure(...args);
+ return React.lazy(async () => Pure);
});
- sharedTests('lazy', (...args) => Promise.resolve(React.pure(...args)));
function sharedTests(label, pure) {
describe(`${label}`, () => {
diff --git a/packages/react-reconciler/src/__tests__/ReactSuspenseWithNoopRenderer-test.internal.js b/packages/react-reconciler/src/__tests__/ReactSuspenseWithNoopRenderer-test.internal.js
index 3fbd5e4e77254..5071e52b4b7d4 100644
--- a/packages/react-reconciler/src/__tests__/ReactSuspenseWithNoopRenderer-test.internal.js
+++ b/packages/react-reconciler/src/__tests__/ReactSuspenseWithNoopRenderer-test.internal.js
@@ -6,7 +6,6 @@ let ReactCache;
let Suspense;
let StrictMode;
let ConcurrentMode;
-let lazy;
let cache;
let TextResource;
@@ -28,7 +27,6 @@ describe('ReactSuspenseWithNoopRenderer', () => {
Suspense = React.unstable_Suspense;
StrictMode = React.StrictMode;
ConcurrentMode = React.unstable_ConcurrentMode;
- lazy = React.lazy;
function invalidateCache() {
cache = ReactCache.createCache(invalidateCache);
@@ -50,12 +48,12 @@ describe('ReactSuspenseWithNoopRenderer', () => {
textResourceShouldFail = false;
});
- function div(...children) {
- children = children.map(
- c => (typeof c === 'string' ? {text: c, hidden: false} : c),
- );
- return {type: 'div', children, prop: undefined, hidden: false};
- }
+ // function div(...children) {
+ // children = children.map(
+ // c => (typeof c === 'string' ? {text: c, hidden: false} : c),
+ // );
+ // return {type: 'div', children, prop: undefined, hidden: false};
+ // }
function span(prop) {
return {type: 'span', children: [], prop, hidden: false};
@@ -1416,294 +1414,6 @@ describe('ReactSuspenseWithNoopRenderer', () => {
});
});
- 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('lazy-load using React.lazy', async () => {
- const LazyText = lazy(() => {
- ReactNoop.yield('Started loading');
- return Promise.resolve(Text);
- });
-
- ReactNoop.render(
- }>
-
-
-
- ,
- );
- // Render first two siblings. The lazy component should not have
- // started loading yet.
- ReactNoop.flushThrough(['A', 'B']);
-
- // Flush the rest.
- expect(ReactNoop.flush()).toEqual(['Started loading', 'Loading...']);
- expect(ReactNoop.getChildren()).toEqual([]);
-
- await LazyText;
-
- expect(ReactNoop.flush()).toEqual(['A', 'B', 'C']);
- expect(ReactNoop.getChildren()).toEqual([
- span('A'),
- span('B'),
- span('C'),
- ]);
- });
-
- it('includes lazy-loaded component in warning stack', async () => {
- const LazyFoo = lazy(() => {
- ReactNoop.yield('Started loading');
- const Foo = props => (
- {[, ]}
- );
- return Promise.resolve(Foo);
- });
-
- ReactNoop.render(
- }>
-
- ,
- );
- expect(ReactNoop.flush()).toEqual(['Started loading', 'Loading...']);
- expect(ReactNoop.getChildren()).toEqual([]);
-
- await LazyFoo;
- expect(() => {
- expect(ReactNoop.flush()).toEqual(['A', 'B']);
- }).toWarnDev(' in Text (at **)\n' + ' in Foo (at **)');
- expect(ReactNoop.getChildren()).toEqual([div(span('A'), span('B'))]);
- });
-
- it('supports class and forwardRef components', async () => {
- const LazyClass = lazy(() => {
- class Foo extends React.Component {
- render() {
- return ;
- }
- }
- return Promise.resolve(Foo);
- });
-
- const LazyForwardRef = lazy(() => {
- const Bar = React.forwardRef((props, ref) => (
-
- ));
- return Promise.resolve(Bar);
- });
-
- const ref = React.createRef();
- ReactNoop.render(
- }>
-
-
- ,
- );
- expect(ReactNoop.flush()).toEqual(['Loading...']);
- expect(ReactNoop.getChildren()).toEqual([]);
- expect(ref.current).toBe(null);
-
- await LazyClass;
- await LazyForwardRef;
- expect(ReactNoop.flush()).toEqual(['Foo', 'Bar']);
- expect(ReactNoop.getChildren()).toEqual([span('Foo'), span('Bar')]);
- expect(ref.current).not.toBe(null);
- });
- });
-
it('does not call lifecycles of a suspended component', async () => {
class TextWithLifecycle extends React.Component {
componentDidMount() {
diff --git a/packages/react/src/ReactLazy.js b/packages/react/src/ReactLazy.js
index 85fa687ec3d6a..28415fcf265c4 100644
--- a/packages/react/src/ReactLazy.js
+++ b/packages/react/src/ReactLazy.js
@@ -5,23 +5,16 @@
* LICENSE file in the root directory of this source tree.
*/
-type Thenable = {
- then(resolve: (T) => mixed, reject: (mixed) => mixed): R,
-};
+import type {LazyComponent, Thenable} from 'shared/ReactLazyComponent';
-export function lazy(ctor: () => Thenable) {
- let thenable = null;
+import {REACT_LAZY_TYPE} from 'shared/ReactSymbols';
+
+export function lazy(ctor: () => Thenable): LazyComponent {
return {
- then(resolve, reject) {
- if (thenable === null) {
- // Lazily create thenable by wrapping in an extra thenable.
- thenable = ctor();
- ctor = null;
- }
- return thenable.then(resolve, reject);
- },
+ $$typeof: REACT_LAZY_TYPE,
+ _ctor: ctor,
// React uses these fields to store the result.
- _reactStatus: -1,
- _reactResult: null,
+ _status: -1,
+ _result: null,
};
}
diff --git a/packages/shared/ReactLazyComponent.js b/packages/shared/ReactLazyComponent.js
index 0d980f71834a3..b5a5df9369525 100644
--- a/packages/shared/ReactLazyComponent.js
+++ b/packages/shared/ReactLazyComponent.js
@@ -7,30 +7,36 @@
* @flow
*/
-export type Thenable = {
- then(resolve: (T) => mixed, reject: (mixed) => mixed): mixed,
- _reactStatus?: 0 | 1 | 2,
- _reactResult: any,
+export type Thenable = {
+ then(resolve: (T) => mixed, reject: (mixed) => mixed): R,
};
-type ResolvedThenable = {
- then(resolve: (T) => mixed, reject: (mixed) => mixed): mixed,
- _reactStatus?: 1,
- _reactResult: T,
+export type LazyComponent = {
+ $$typeof: Symbol | number,
+ _ctor: () => Thenable,
+ _status: 0 | 1 | 2,
+ _result: any,
+};
+
+type ResolvedLazyComponentThenable = {
+ $$typeof: Symbol | number,
+ _ctor: () => Thenable,
+ _status: 1,
+ _result: any,
};
export const Pending = 0;
export const Resolved = 1;
export const Rejected = 2;
-export function getResultFromResolvedThenable(
- thenable: ResolvedThenable,
+export function getResultFromResolvedLazyComponent(
+ lazyComponent: ResolvedLazyComponentThenable,
): T {
- return thenable._reactResult;
+ return lazyComponent._result;
}
-export function refineResolvedThenable(
- thenable: Thenable,
-): ResolvedThenable | null {
- return thenable._reactStatus === Resolved ? thenable._reactResult : null;
+export function refineResolvedLazyComponent(
+ lazyComponent: LazyComponent,
+): ResolvedLazyComponentThenable | null {
+ return lazyComponent._status === Resolved ? lazyComponent._result : null;
}
diff --git a/packages/shared/ReactSymbols.js b/packages/shared/ReactSymbols.js
index 774c1bbe20385..2b8211474c384 100644
--- a/packages/shared/ReactSymbols.js
+++ b/packages/shared/ReactSymbols.js
@@ -42,6 +42,7 @@ export const REACT_SUSPENSE_TYPE = hasSymbol
? Symbol.for('react.suspense')
: 0xead1;
export const REACT_PURE_TYPE = hasSymbol ? Symbol.for('react.pure') : 0xead3;
+export const REACT_LAZY_TYPE = hasSymbol ? Symbol.for('react.lazy') : 0xead4;
const MAYBE_ITERATOR_SYMBOL = typeof Symbol === 'function' && Symbol.iterator;
const FAUX_ITERATOR_SYMBOL = '@@iterator';
diff --git a/packages/shared/getComponentName.js b/packages/shared/getComponentName.js
index 4e89efef4acfa..abc5b04916bca 100644
--- a/packages/shared/getComponentName.js
+++ b/packages/shared/getComponentName.js
@@ -7,7 +7,7 @@
* @flow
*/
-import type {Thenable} from 'shared/ReactLazyComponent';
+import type {LazyComponent} from 'shared/ReactLazyComponent';
import warningWithoutStack from 'shared/warningWithoutStack';
import {
@@ -21,8 +21,9 @@ import {
REACT_PROVIDER_TYPE,
REACT_STRICT_MODE_TYPE,
REACT_SUSPENSE_TYPE,
+ REACT_LAZY_TYPE,
} from 'shared/ReactSymbols';
-import {refineResolvedThenable} from 'shared/ReactLazyComponent';
+import {refineResolvedLazyComponent} from 'shared/ReactLazyComponent';
function getWrappedName(
outerType: mixed,
@@ -80,12 +81,12 @@ function getComponentName(type: mixed): string | null {
return getWrappedName(type, type.render, 'ForwardRef');
case REACT_PURE_TYPE:
return getWrappedName(type, type.render, 'Pure');
- }
- if (typeof type.then === 'function') {
- const thenable: Thenable = (type: any);
- const resolvedThenable = refineResolvedThenable(thenable);
- if (resolvedThenable) {
- return getComponentName(resolvedThenable);
+ case REACT_LAZY_TYPE: {
+ const thenable: LazyComponent = (type: any);
+ const resolvedThenable = refineResolvedLazyComponent(thenable);
+ if (resolvedThenable) {
+ return getComponentName(resolvedThenable);
+ }
}
}
}
diff --git a/packages/shared/isValidElementType.js b/packages/shared/isValidElementType.js
index 8682a3fb53932..f662ce19fa699 100644
--- a/packages/shared/isValidElementType.js
+++ b/packages/shared/isValidElementType.js
@@ -17,6 +17,7 @@ import {
REACT_STRICT_MODE_TYPE,
REACT_SUSPENSE_TYPE,
REACT_PURE_TYPE,
+ REACT_LAZY_TYPE,
} from 'shared/ReactSymbols';
export default function isValidElementType(type: mixed) {
@@ -31,7 +32,7 @@ export default function isValidElementType(type: mixed) {
type === REACT_SUSPENSE_TYPE ||
(typeof type === 'object' &&
type !== null &&
- (typeof type.then === 'function' ||
+ (type.$$typeof === REACT_LAZY_TYPE ||
type.$$typeof === REACT_PURE_TYPE ||
type.$$typeof === REACT_PROVIDER_TYPE ||
type.$$typeof === REACT_CONTEXT_TYPE ||