main
js 263 lines 8.53 KB
Raw
1 /**
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 *
4 * This source code is licensed under the MIT license found in the
5 * LICENSE file in the root directory of this source tree.
6 *
7 * @flow
8 */
9
10 import type {
11 Wakeable,
12 Thenable,
13 FulfilledThenable,
14 RejectedThenable,
15 ReactDebugInfo,
16 ReactIOInfo,
17 } from 'shared/ReactTypes';
18
19 import {enableAsyncDebugInfo} from 'shared/ReactFeatureFlags';
20
21 import {REACT_LAZY_TYPE} from 'shared/ReactSymbols';
22
23 import noop from 'shared/noop';
24
25 const Uninitialized = -1;
26 const Pending = 0;
27 const Resolved = 1;
28 const Rejected = 2;
29
30 type UninitializedPayload<T> = {
31 _status: -1,
32 _result: () => Thenable<{default: T, ...}>,
33 _ioInfo?: ReactIOInfo, // DEV-only
34 };
35
36 type PendingPayload = {
37 _status: 0,
38 _result: Wakeable,
39 _ioInfo?: ReactIOInfo, // DEV-only
40 };
41
42 type ResolvedPayload<T> = {
43 _status: 1,
44 _result: {default: T, ...},
45 _ioInfo?: ReactIOInfo, // DEV-only
46 };
47
48 type RejectedPayload = {
49 _status: 2,
50 _result: mixed,
51 _ioInfo?: ReactIOInfo, // DEV-only
52 };
53
54 type Payload<T> =
55 | UninitializedPayload<T>
56 | PendingPayload
57 | ResolvedPayload<T>
58 | RejectedPayload;
59
60 export type LazyComponent<T, P> = {
61 $$typeof: symbol | number,
62 _payload: P,
63 _init: (payload: P) => T,
64
65 // __DEV__
66 _debugInfo?: null | ReactDebugInfo,
67 _store?: {validated: 0 | 1 | 2, ...}, // 0: not validated, 1: validated, 2: force fail
68 };
69
70 function lazyInitializer<T>(payload: Payload<T>): T {
71 if (payload._status === Uninitialized) {
72 let resolveDebugValue: (void | T) => void = null as any;
73 let rejectDebugValue: mixed => void = null as any;
74 if (__DEV__ && enableAsyncDebugInfo) {
75 const ioInfo = payload._ioInfo;
76 if (ioInfo != null) {
77 // Mark when we first kicked off the lazy request.
78 // $FlowFixMe[cannot-write]
79 ioInfo.start = ioInfo.end = performance.now();
80 // Stash a Promise for introspection of the value later.
81 // $FlowFixMe[cannot-write]
82 ioInfo.value = new Promise((resolve, reject) => {
83 resolveDebugValue = resolve;
84 rejectDebugValue = reject;
85 });
86 }
87 }
88 const ctor = payload._result;
89 const thenable = ctor();
90 // Transition to the next state.
91 // This might throw either because it's missing or throws. If so, we treat it
92 // as still uninitialized and try again next time. Which is the same as what
93 // happens if the ctor or any wrappers processing the ctor throws. This might
94 // end up fixing it if the resolution was a concurrency bug.
95 thenable.then(
96 moduleObject => {
97 if (
98 (payload as Payload<T>)._status === Pending ||
99 payload._status === Uninitialized
100 ) {
101 // Transition to the next state.
102 const resolved: ResolvedPayload<T> = payload as any;
103 resolved._status = Resolved;
104 resolved._result = moduleObject;
105 if (__DEV__ && enableAsyncDebugInfo) {
106 const ioInfo = payload._ioInfo;
107 if (ioInfo != null) {
108 // Mark the end time of when we resolved.
109 // $FlowFixMe[cannot-write]
110 ioInfo.end = performance.now();
111 // Surface the default export as the resolved "value" for debug purposes.
112 const debugValue =
113 moduleObject == null ? undefined : moduleObject.default;
114 resolveDebugValue(debugValue);
115 // $FlowFixMe[incompatible-use]
116 // $FlowFixMe[prop-missing]
117 ioInfo.value.status = 'fulfilled';
118 // $FlowFixMe[incompatible-use]
119 // $FlowFixMe[prop-missing]
120 ioInfo.value.value = debugValue;
121 }
122 }
123 // Make the thenable introspectable
124 // TODO we should move the lazy introspection into the resolveLazy
125 // impl or make suspendedThenable be able to be a lazy itself
126 if (thenable.status === undefined) {
127 const fulfilledThenable: FulfilledThenable<{default: T, ...}> =
128 thenable as any;
129 fulfilledThenable.status = 'fulfilled';
130 fulfilledThenable.value = moduleObject;
131 }
132 }
133 },
134 error => {
135 if (
136 (payload as Payload<T>)._status === Pending ||
137 payload._status === Uninitialized
138 ) {
139 // Transition to the next state.
140 const rejected: RejectedPayload = payload as any;
141 rejected._status = Rejected;
142 rejected._result = error;
143 if (__DEV__ && enableAsyncDebugInfo) {
144 const ioInfo = payload._ioInfo;
145 if (ioInfo != null) {
146 // Mark the end time of when we rejected.
147 // $FlowFixMe[cannot-write]
148 ioInfo.end = performance.now();
149 // Hide unhandled rejections.
150 // $FlowFixMe[incompatible-use]
151 ioInfo.value.then(noop, noop);
152 rejectDebugValue(error);
153 // $FlowFixMe[incompatible-use]
154 // $FlowFixMe[prop-missing]
155 ioInfo.value.status = 'rejected';
156 // $FlowFixMe[incompatible-use]
157 // $FlowFixMe[prop-missing]
158 ioInfo.value.reason = error;
159 }
160 }
161 // Make the thenable introspectable
162 // TODO we should move the lazy introspection into the resolveLazy
163 // impl or make suspendedThenable be able to be a lazy itself
164 if (thenable.status === undefined) {
165 const rejectedThenable: RejectedThenable<{default: T, ...}> =
166 thenable as any;
167 rejectedThenable.status = 'rejected';
168 rejectedThenable.reason = error;
169 }
170 }
171 },
172 );
173 if (__DEV__ && enableAsyncDebugInfo) {
174 const ioInfo = payload._ioInfo;
175 if (ioInfo != null) {
176 const displayName = thenable.displayName;
177 if (typeof displayName === 'string') {
178 // $FlowFixMe[cannot-write]
179 ioInfo.name = displayName;
180 }
181 }
182 }
183 if (payload._status === Uninitialized) {
184 // In case, we're still uninitialized, then we're waiting for the thenable
185 // to resolve. Set it as pending in the meantime.
186 const pending: PendingPayload = payload as any;
187 pending._status = Pending;
188 pending._result = thenable;
189 }
190 }
191 if (payload._status === Resolved) {
192 const moduleObject = payload._result;
193 if (__DEV__) {
194 if (moduleObject === undefined) {
195 console.error(
196 'lazy: Expected the result of a dynamic imp' +
197 'ort() call. ' +
198 'Instead received: %s\n\nYour code should look like: \n ' +
199 // Break up imports to avoid accidentally parsing them as dependencies.
200 'const MyComponent = lazy(() => imp' +
201 "ort('./MyComponent'))\n\n" +
202 'Did you accidentally put curly braces around the import?',
203 moduleObject,
204 );
205 }
206 }
207 if (__DEV__) {
208 if (!('default' in moduleObject)) {
209 console.error(
210 'lazy: Expected the result of a dynamic imp' +
211 'ort() call. ' +
212 'Instead received: %s\n\nYour code should look like: \n ' +
213 // Break up imports to avoid accidentally parsing them as dependencies.
214 'const MyComponent = lazy(() => imp' +
215 "ort('./MyComponent'))",
216 moduleObject,
217 );
218 }
219 }
220 return moduleObject.default;
221 } else {
222 throw payload._result;
223 }
224 }
225
226 export function lazy<T>(
227 ctor: () => Thenable<{default: T, ...}>,
228 ): LazyComponent<T, Payload<T>> {
229 const payload: Payload<T> = {
230 // We use these fields to store the result.
231 _status: Uninitialized,
232 _result: ctor,
233 };
234
235 const lazyType: LazyComponent<T, Payload<T>> = {
236 $$typeof: REACT_LAZY_TYPE,
237 _payload: payload,
238 _init: lazyInitializer,
239 };
240
241 if (__DEV__ && enableAsyncDebugInfo) {
242 // TODO: We should really track the owner here but currently ReactIOInfo
243 // can only contain ReactComponentInfo and not a Fiber. It's unusual to
244 // create a lazy inside an owner though since they should be in module scope.
245 const owner = null;
246 const ioInfo: ReactIOInfo = {
247 name: 'lazy',
248 start: -1,
249 end: -1,
250 value: null,
251 owner: owner,
252 debugStack: new Error('react-stack-top-frame'),
253 // eslint-disable-next-line react-internal/no-production-logging
254 debugTask: console.createTask ? console.createTask('lazy()') : null,
255 };
256 payload._ioInfo = ioInfo;
257 // Add debug info to the lazy, but this doesn't have an await stack yet.
258 // That will be inferred by later usage.
259 lazyType._debugInfo = [{awaited: ioInfo}];
260 }
261
262 return lazyType;
263 }