diff --git a/packages/react-reconciler/src/__tests__/ReactSuspense-test.internal.js b/packages/react-reconciler/src/__tests__/ReactSuspense-test.internal.js
index ecb99d9ab44..0827816b2b6 100644
--- a/packages/react-reconciler/src/__tests__/ReactSuspense-test.internal.js
+++ b/packages/react-reconciler/src/__tests__/ReactSuspense-test.internal.js
@@ -6,6 +6,7 @@ let SimpleCacheProvider;
let Placeholder;
let StrictMode;
let AsyncMode;
+let lazy;
let cache;
let TextResource;
@@ -25,6 +26,7 @@ describe('ReactSuspense', () => {
Placeholder = React.Placeholder;
StrictMode = React.StrictMode;
AsyncMode = React.unstable_AsyncMode;
+ lazy = React.lazy;
function invalidateCache() {
cache = SimpleCacheProvider.createCache(invalidateCache);
@@ -1543,6 +1545,37 @@ describe('ReactSuspense', () => {
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('does not call lifecycles of a suspended component', async () => {
diff --git a/packages/react/src/React.js b/packages/react/src/React.js
index 15e10114dcd..85e1207a902 100644
--- a/packages/react/src/React.js
+++ b/packages/react/src/React.js
@@ -25,6 +25,7 @@ import {
isValidElement,
} from './ReactElement';
import {createContext} from './ReactContext';
+import {lazy} from './ReactLazy';
import forwardRef from './forwardRef';
import {
createElementWithValidation,
@@ -66,6 +67,7 @@ const React = {
if (enableSuspense) {
React.Placeholder = REACT_PLACEHOLDER_TYPE;
+ React.lazy = lazy;
}
export default React;
diff --git a/packages/react/src/ReactLazy.js b/packages/react/src/ReactLazy.js
new file mode 100644
index 00000000000..e17c33b9793
--- /dev/null
+++ b/packages/react/src/ReactLazy.js
@@ -0,0 +1,27 @@
+/**
+ * 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.
+ */
+
+type Thenable = {
+ then(resolve: (T) => mixed, reject: (mixed) => mixed): R,
+};
+
+export function lazy(ctor: () => Thenable) {
+ let thenable = null;
+ 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);
+ },
+ // React uses these fields to store the result.
+ _reactStatus: -1,
+ _reactResult: null,
+ };
+}