Skip to content

Commit 581f0c4

Browse files
authored
[Flight] add support for Lazy components in Flight server (#24068)
* [Flight] add support for Lazy components in Flight server Lazy components suspend until resolved just like in Fizz. Add tests to confirm Lazy works with Shared Components and Client Component references. * Support Lazy elements React.Lazy can now return an element instead of a Component. This commit implements support for Lazy elements when server rendering. * add lazy initialization to resolveModelToJson adding lazying initialization toResolveModelToJson means we use attemptResolveElement's full logic on whatever the resolved type ends up being. This better aligns handling of misued Lazy types like a lazy element being used as a Component or a lazy Component being used as an element.
1 parent 82762be commit 581f0c4

File tree

2 files changed

+236
-14
lines changed

2 files changed

+236
-14
lines changed

packages/react-client/src/__tests__/ReactFlight-test.js

+207
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,213 @@ describe('ReactFlight', () => {
138138
expect(ReactNoop).toMatchRenderedOutput(<span>Hello, Seb Smith</span>);
139139
});
140140

141+
it('can render a lazy component as a shared component on the server', async () => {
142+
function SharedComponent({text}) {
143+
return (
144+
<div>
145+
shared<span>{text}</span>
146+
</div>
147+
);
148+
}
149+
150+
let load = null;
151+
const loadSharedComponent = () => {
152+
return new Promise(res => {
153+
load = () => res({default: SharedComponent});
154+
});
155+
};
156+
157+
const LazySharedComponent = React.lazy(loadSharedComponent);
158+
159+
function ServerComponent() {
160+
return (
161+
<React.Suspense fallback={'Loading...'}>
162+
<LazySharedComponent text={'a'} />
163+
</React.Suspense>
164+
);
165+
}
166+
167+
const transport = ReactNoopFlightServer.render(<ServerComponent />);
168+
169+
act(() => {
170+
const rootModel = ReactNoopFlightClient.read(transport);
171+
ReactNoop.render(rootModel);
172+
});
173+
expect(ReactNoop).toMatchRenderedOutput('Loading...');
174+
await load();
175+
176+
act(() => {
177+
const rootModel = ReactNoopFlightClient.read(transport);
178+
ReactNoop.render(rootModel);
179+
});
180+
expect(ReactNoop).toMatchRenderedOutput(
181+
<div>
182+
shared<span>a</span>
183+
</div>,
184+
);
185+
});
186+
187+
it('errors on a Lazy element being used in Component position', async () => {
188+
function SharedComponent({text}) {
189+
return (
190+
<div>
191+
shared<span>{text}</span>
192+
</div>
193+
);
194+
}
195+
196+
let load = null;
197+
198+
const LazyElementDisguisedAsComponent = React.lazy(() => {
199+
return new Promise(res => {
200+
load = () => res({default: <SharedComponent text={'a'} />});
201+
});
202+
});
203+
204+
function ServerComponent() {
205+
return (
206+
<React.Suspense fallback={'Loading...'}>
207+
<LazyElementDisguisedAsComponent text={'b'} />
208+
</React.Suspense>
209+
);
210+
}
211+
212+
const transport = ReactNoopFlightServer.render(<ServerComponent />);
213+
214+
act(() => {
215+
const rootModel = ReactNoopFlightClient.read(transport);
216+
ReactNoop.render(rootModel);
217+
});
218+
expect(ReactNoop).toMatchRenderedOutput('Loading...');
219+
spyOnDevAndProd(console, 'error');
220+
await load();
221+
expect(console.error).toHaveBeenCalledTimes(1);
222+
});
223+
224+
it('can render a lazy element', async () => {
225+
function SharedComponent({text}) {
226+
return (
227+
<div>
228+
shared<span>{text}</span>
229+
</div>
230+
);
231+
}
232+
233+
let load = null;
234+
235+
const lazySharedElement = React.lazy(() => {
236+
return new Promise(res => {
237+
load = () => res({default: <SharedComponent text={'a'} />});
238+
});
239+
});
240+
241+
function ServerComponent() {
242+
return (
243+
<React.Suspense fallback={'Loading...'}>
244+
{lazySharedElement}
245+
</React.Suspense>
246+
);
247+
}
248+
249+
const transport = ReactNoopFlightServer.render(<ServerComponent />);
250+
251+
act(() => {
252+
const rootModel = ReactNoopFlightClient.read(transport);
253+
ReactNoop.render(rootModel);
254+
});
255+
expect(ReactNoop).toMatchRenderedOutput('Loading...');
256+
await load();
257+
258+
act(() => {
259+
const rootModel = ReactNoopFlightClient.read(transport);
260+
ReactNoop.render(rootModel);
261+
});
262+
expect(ReactNoop).toMatchRenderedOutput(
263+
<div>
264+
shared<span>a</span>
265+
</div>,
266+
);
267+
});
268+
269+
it('errors with lazy value in element position that resolves to Component', async () => {
270+
function SharedComponent({text}) {
271+
return (
272+
<div>
273+
shared<span>{text}</span>
274+
</div>
275+
);
276+
}
277+
278+
let load = null;
279+
280+
const componentDisguisedAsElement = React.lazy(() => {
281+
return new Promise(res => {
282+
load = () => res({default: SharedComponent});
283+
});
284+
});
285+
286+
function ServerComponent() {
287+
return (
288+
<React.Suspense fallback={'Loading...'}>
289+
{componentDisguisedAsElement}
290+
</React.Suspense>
291+
);
292+
}
293+
294+
const transport = ReactNoopFlightServer.render(<ServerComponent />);
295+
296+
act(() => {
297+
const rootModel = ReactNoopFlightClient.read(transport);
298+
ReactNoop.render(rootModel);
299+
});
300+
expect(ReactNoop).toMatchRenderedOutput('Loading...');
301+
spyOnDevAndProd(console, 'error');
302+
await load();
303+
expect(console.error).toHaveBeenCalledTimes(1);
304+
});
305+
306+
it('can render a lazy module reference', async () => {
307+
function ClientComponent() {
308+
return <div>I am client</div>;
309+
}
310+
311+
const ClientComponentReference = moduleReference(ClientComponent);
312+
313+
let load = null;
314+
const loadClientComponentReference = () => {
315+
return new Promise(res => {
316+
load = () => res({default: ClientComponentReference});
317+
});
318+
};
319+
320+
const LazyClientComponentReference = React.lazy(
321+
loadClientComponentReference,
322+
);
323+
324+
function ServerComponent() {
325+
return (
326+
<React.Suspense fallback={'Loading...'}>
327+
<LazyClientComponentReference />
328+
</React.Suspense>
329+
);
330+
}
331+
332+
const transport = ReactNoopFlightServer.render(<ServerComponent />);
333+
334+
act(() => {
335+
const rootModel = ReactNoopFlightClient.read(transport);
336+
ReactNoop.render(rootModel);
337+
});
338+
expect(ReactNoop).toMatchRenderedOutput('Loading...');
339+
await load();
340+
341+
act(() => {
342+
const rootModel = ReactNoopFlightClient.read(transport);
343+
ReactNoop.render(rootModel);
344+
});
345+
expect(ReactNoop).toMatchRenderedOutput(<div>I am client</div>);
346+
});
347+
141348
it('should error if a non-serializable value is passed to a host component', () => {
142349
function EventHandlerProp() {
143350
return (

packages/react-server/src/ReactFlightServer.js

+29-14
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,12 @@ function attemptResolveElement(
200200
return [REACT_ELEMENT_TYPE, type, key, props];
201201
}
202202
switch (type.$$typeof) {
203+
case REACT_LAZY_TYPE: {
204+
const payload = type._payload;
205+
const init = type._init;
206+
const wrappedType = init(payload);
207+
return attemptResolveElement(wrappedType, key, ref, props);
208+
}
203209
case REACT_FORWARD_REF_TYPE: {
204210
const render = type.render;
205211
return render(props, undefined);
@@ -452,10 +458,6 @@ export function resolveModelToJSON(
452458
switch (value) {
453459
case REACT_ELEMENT_TYPE:
454460
return '$';
455-
case REACT_LAZY_TYPE:
456-
throw new Error(
457-
'React Lazy Components are not yet supported on the server.',
458-
);
459461
}
460462

461463
if (__DEV__) {
@@ -477,23 +479,36 @@ export function resolveModelToJSON(
477479
while (
478480
typeof value === 'object' &&
479481
value !== null &&
480-
(value: any).$$typeof === REACT_ELEMENT_TYPE
482+
((value: any).$$typeof === REACT_ELEMENT_TYPE ||
483+
(value: any).$$typeof === REACT_LAZY_TYPE)
481484
) {
482485
if (__DEV__) {
483486
if (isInsideContextValue) {
484487
console.error('React elements are not allowed in ServerContext');
485488
}
486489
}
487-
// TODO: Concatenate keys of parents onto children.
488-
const element: React$Element<any> = (value: any);
490+
489491
try {
490-
// Attempt to render the server component.
491-
value = attemptResolveElement(
492-
element.type,
493-
element.key,
494-
element.ref,
495-
element.props,
496-
);
492+
switch ((value: any).$$typeof) {
493+
case REACT_ELEMENT_TYPE: {
494+
// TODO: Concatenate keys of parents onto children.
495+
const element: React$Element<any> = (value: any);
496+
// Attempt to render the server component.
497+
value = attemptResolveElement(
498+
element.type,
499+
element.key,
500+
element.ref,
501+
element.props,
502+
);
503+
break;
504+
}
505+
case REACT_LAZY_TYPE: {
506+
const payload = (value: any)._payload;
507+
const init = (value: any)._init;
508+
value = init(payload);
509+
break;
510+
}
511+
}
497512
} catch (x) {
498513
if (typeof x === 'object' && x !== null && typeof x.then === 'function') {
499514
// Something suspended, we'll need to create a new segment and resolve it later.

0 commit comments

Comments
 (0)