Skip to content

Commit 1580a43

Browse files
committed
Support Lazy but error if an element is passed to a Reply
1 parent bb0944f commit 1580a43

File tree

3 files changed

+126
-23
lines changed

3 files changed

+126
-23
lines changed

packages/react-client/src/ReactFlightReplyClient.js

Lines changed: 90 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ import type {
1414
RejectedThenable,
1515
ReactCustomFormAction,
1616
} from 'shared/ReactTypes';
17+
import type {LazyComponent} from 'react/src/ReactLazy';
18+
1719
import {enableRenderableContext} from 'shared/ReactFeatureFlags';
1820

1921
import {
@@ -84,9 +86,9 @@ export type ReactServerValue =
8486

8587
type ReactServerObject = {+[key: string]: ReactServerValue};
8688

87-
// function serializeByValueID(id: number): string {
88-
// return '$' + id.toString(16);
89-
// }
89+
function serializeByValueID(id: number): string {
90+
return '$' + id.toString(16);
91+
}
9092

9193
function serializePromiseID(id: number): string {
9294
return '$@' + id.toString(16);
@@ -206,6 +208,78 @@ export function processReply(
206208
}
207209

208210
if (typeof value === 'object') {
211+
switch ((value: any).$$typeof) {
212+
case REACT_ELEMENT_TYPE: {
213+
throw new Error(
214+
'React Element cannot be passed to Server Functions from the Client.' +
215+
(__DEV__ ? describeObjectForErrorMessage(parent, key) : ''),
216+
);
217+
}
218+
case REACT_LAZY_TYPE: {
219+
// Resolve lazy as if it wasn't here. In the future this will be encoded as a Promise.
220+
const lazy: LazyComponent<any, any> = (value: any);
221+
const payload = lazy._payload;
222+
const init = lazy._init;
223+
if (formData === null) {
224+
// Upgrade to use FormData to allow us to stream this value.
225+
formData = new FormData();
226+
}
227+
try {
228+
const resolvedModel = init(payload);
229+
// We always outline this as a separate part even though we could inline it
230+
// because it ensures a more deterministic encoding.
231+
pendingParts++;
232+
const lazyId = nextPartId++;
233+
const partJSON = JSON.stringify(resolvedModel, resolveToJSON);
234+
// $FlowFixMe[incompatible-type] We know it's not null because we assigned it above.
235+
const data: FormData = formData;
236+
// eslint-disable-next-line react-internal/safe-string-coercion
237+
data.append(formFieldPrefix + lazyId, partJSON);
238+
pendingParts--;
239+
return serializeByValueID(lazyId);
240+
} catch (x) {
241+
if (
242+
typeof x === 'object' &&
243+
x !== null &&
244+
typeof x.then === 'function'
245+
) {
246+
// Suspended
247+
pendingParts++;
248+
const lazyId = nextPartId++;
249+
const thenable: Thenable<any> = (x: any);
250+
thenable.then(
251+
partValue => {
252+
try {
253+
const partJSON = JSON.stringify(partValue, resolveToJSON);
254+
// $FlowFixMe[incompatible-type] We know it's not null because we assigned it above.
255+
const data: FormData = formData;
256+
// eslint-disable-next-line react-internal/safe-string-coercion
257+
data.append(formFieldPrefix + lazyId, partJSON);
258+
pendingParts--;
259+
if (pendingParts === 0) {
260+
resolve(data);
261+
}
262+
} catch (reason) {
263+
reject(reason);
264+
}
265+
},
266+
reason => {
267+
// In the future we could consider serializing this as an error
268+
// that throws on the server instead.
269+
reject(reason);
270+
},
271+
);
272+
return serializeByValueID(lazyId);
273+
} else {
274+
// In the future we could consider serializing this as an error
275+
// that throws on the server instead.
276+
reject(x);
277+
return null;
278+
}
279+
}
280+
}
281+
}
282+
209283
// $FlowFixMe[method-unbinding]
210284
if (typeof value.then === 'function') {
211285
// We assume that any object with a .then property is a "Thenable" type,
@@ -219,14 +293,18 @@ export function processReply(
219293
const thenable: Thenable<any> = (value: any);
220294
thenable.then(
221295
partValue => {
222-
const partJSON = JSON.stringify(partValue, resolveToJSON);
223-
// $FlowFixMe[incompatible-type] We know it's not null because we assigned it above.
224-
const data: FormData = formData;
225-
// eslint-disable-next-line react-internal/safe-string-coercion
226-
data.append(formFieldPrefix + promiseId, partJSON);
227-
pendingParts--;
228-
if (pendingParts === 0) {
229-
resolve(data);
296+
try {
297+
const partJSON = JSON.stringify(partValue, resolveToJSON);
298+
// $FlowFixMe[incompatible-type] We know it's not null because we assigned it above.
299+
const data: FormData = formData;
300+
// eslint-disable-next-line react-internal/safe-string-coercion
301+
data.append(formFieldPrefix + promiseId, partJSON);
302+
pendingParts--;
303+
if (pendingParts === 0) {
304+
resolve(data);
305+
}
306+
} catch (reason) {
307+
reject(reason);
230308
}
231309
},
232310
reason => {
@@ -294,17 +372,7 @@ export function processReply(
294372
);
295373
}
296374
if (__DEV__) {
297-
if ((value: any).$$typeof === REACT_ELEMENT_TYPE) {
298-
console.error(
299-
'React Element cannot be passed to Server Functions from the Client.%s',
300-
describeObjectForErrorMessage(parent, key),
301-
);
302-
} else if ((value: any).$$typeof === REACT_LAZY_TYPE) {
303-
console.error(
304-
'React Lazy cannot be passed to Server Functions from the Client.%s',
305-
describeObjectForErrorMessage(parent, key),
306-
);
307-
} else if (
375+
if (
308376
(value: any).$$typeof ===
309377
(enableRenderableContext ? REACT_CONTEXT_TYPE : REACT_PROVIDER_TYPE)
310378
) {

packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMReply-test.js

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ global.TextDecoder = require('util').TextDecoder;
1717

1818
// let serverExports;
1919
let webpackServerMap;
20+
let React;
2021
let ReactServerDOMServer;
2122
let ReactServerDOMClient;
2223

@@ -31,6 +32,7 @@ describe('ReactFlightDOMReply', () => {
3132
const WebpackMock = require('./utils/WebpackMock');
3233
// serverExports = WebpackMock.serverExports;
3334
webpackServerMap = WebpackMock.webpackServerMap;
35+
React = require('react');
3436
ReactServerDOMServer = require('react-server-dom-webpack/server.browser');
3537
jest.resetModules();
3638
ReactServerDOMClient = require('react-server-dom-webpack/client');
@@ -241,4 +243,36 @@ describe('ReactFlightDOMReply', () => {
241243
}
242244
expect(error.message).toBe('Connection closed.');
243245
});
246+
247+
it('resolves a promise and includes its value', async () => {
248+
let resolve;
249+
const promise = new Promise(r => (resolve = r));
250+
const bodyPromise = ReactServerDOMClient.encodeReply({promise: promise});
251+
resolve('Hi');
252+
const result = await ReactServerDOMServer.decodeReply(await bodyPromise);
253+
expect(await result.promise).toBe('Hi');
254+
});
255+
256+
it('resolves a React.lazy and includes its value', async () => {
257+
let resolve;
258+
const lazy = React.lazy(() => new Promise(r => (resolve = r)));
259+
const bodyPromise = ReactServerDOMClient.encodeReply({lazy: lazy});
260+
resolve('Hi');
261+
const result = await ReactServerDOMServer.decodeReply(await bodyPromise);
262+
expect(result.lazy).toBe('Hi');
263+
});
264+
265+
it('errors when called with JSX by default', async () => {
266+
let error;
267+
try {
268+
await ReactServerDOMClient.encodeReply(<div />);
269+
} catch (x) {
270+
error = x;
271+
}
272+
expect(error).toEqual(
273+
expect.objectContaining({
274+
message: expect.stringContaining(''),
275+
}),
276+
);
277+
});
244278
});

scripts/error-codes/codes.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -494,5 +494,6 @@
494494
"506": "Functions are not valid as a child of Client Components. This may happen if you return %s instead of <%s /> from render. Or maybe you meant to call this function rather than return it.%s",
495495
"507": "Expected the last optional `callback` argument to be a function. Instead received: %s.",
496496
"508": "The first argument must be a React class instance. Instead received: %s.",
497-
"509": "ReactDOM: Unsupported Legacy Mode API."
497+
"509": "ReactDOM: Unsupported Legacy Mode API.",
498+
"510": "React Element cannot be passed to Server Functions from the Client.%s"
498499
}

0 commit comments

Comments
 (0)