diff --git a/packages/react-client/src/ReactFlightClient.js b/packages/react-client/src/ReactFlightClient.js
index 05ecf17268..85b20a4c1f 100644
--- a/packages/react-client/src/ReactFlightClient.js
+++ b/packages/react-client/src/ReactFlightClient.js
@@ -580,6 +580,10 @@ export function parseModelString(
// Special encoding for `undefined` which can't be serialized as JSON otherwise.
return undefined;
}
+ case 'D': {
+ // Date
+ return new Date(Date.parse(value.substring(2)));
+ }
case 'n': {
// BigInt
return BigInt(value.substring(2));
diff --git a/packages/react-client/src/ReactFlightReplyClient.js b/packages/react-client/src/ReactFlightReplyClient.js
index eb3d8f83e6..224af305d6 100644
--- a/packages/react-client/src/ReactFlightReplyClient.js
+++ b/packages/react-client/src/ReactFlightReplyClient.js
@@ -101,6 +101,12 @@ function serializeUndefined(): string {
return '$undefined';
}
+function serializeDateFromDateJSON(dateJSON: string): string {
+ // JSON.stringify automatically calls Date.prototype.toJSON which calls toISOString.
+ // We need only tack on a $D prefix.
+ return '$D' + dateJSON;
+}
+
function serializeBigInt(n: bigint): string {
return '$n' + n.toString(10);
}
@@ -133,10 +139,16 @@ export function processReply(
value: ReactServerValue,
): ReactJSONValue {
const parent = this;
+
+ // Make sure that `parent[key]` wasn't JSONified before `value` was passed to us
if (__DEV__) {
// $FlowFixMe[incompatible-use]
- const originalValue = this[key];
- if (typeof originalValue === 'object' && originalValue !== value) {
+ const originalValue = parent[key];
+ if (
+ typeof originalValue === 'object' &&
+ originalValue !== value &&
+ !(originalValue instanceof Date)
+ ) {
if (objectName(originalValue) !== 'Object') {
console.error(
'Only plain objects can be passed to Server Functions from the Client. ' +
@@ -266,6 +278,17 @@ export function processReply(
}
if (typeof value === 'string') {
+ // TODO: Maybe too clever. If we support URL there's no similar trick.
+ if (value[value.length - 1] === 'Z') {
+ // Possibly a Date, whose toJSON automatically calls toISOString
+ // $FlowFixMe[incompatible-use]
+ const originalValue = parent[key];
+ // $FlowFixMe[method-unbinding]
+ if (originalValue instanceof Date) {
+ return serializeDateFromDateJSON(value);
+ }
+ }
+
return escapeStringValue(value);
}
diff --git a/packages/react-client/src/__tests__/ReactFlight-test.js b/packages/react-client/src/__tests__/ReactFlight-test.js
index 9fc2da89a0..cb24048227 100644
--- a/packages/react-client/src/__tests__/ReactFlight-test.js
+++ b/packages/react-client/src/__tests__/ReactFlight-test.js
@@ -306,6 +306,23 @@ describe('ReactFlight', () => {
);
});
+ it('can transport Date', async () => {
+ function ComponentClient({prop}) {
+ return `prop: ${prop.toISOString()}`;
+ }
+ const Component = clientReference(ComponentClient);
+
+ const model = ;
+
+ const transport = ReactNoopFlightServer.render(model);
+
+ await act(async () => {
+ ReactNoop.render(await ReactNoopFlightClient.read(transport));
+ });
+
+ expect(ReactNoop).toMatchRenderedOutput('prop: 2009-02-13T23:31:30.123Z');
+ });
+
it('can render a lazy component as a shared component on the server', async () => {
function SharedComponent({text}) {
return (
@@ -675,28 +692,39 @@ describe('ReactFlight', () => {
});
it('should warn in DEV if a toJSON instance is passed to a host component', () => {
+ const obj = {
+ toJSON() {
+ return 123;
+ },
+ };
expect(() => {
- const transport = ReactNoopFlightServer.render(
- ,
- );
+ const transport = ReactNoopFlightServer.render();
ReactNoopFlightClient.read(transport);
}).toErrorDev(
'Only plain objects can be passed to Client Components from Server Components. ' +
- 'Date objects are not supported.',
+ 'Objects with toJSON methods are not supported. ' +
+ 'Convert it manually to a simple value before passing it to props.\n' +
+ ' \n' +
+ ' ^^^^^^^^^^^^^^^^^^^^',
{withoutStack: true},
);
});
it('should warn in DEV if a toJSON instance is passed to a host component child', () => {
+ class MyError extends Error {
+ toJSON() {
+ return 123;
+ }
+ }
expect(() => {
const transport = ReactNoopFlightServer.render(
-
Current date: {new Date()}
,
+
Womp womp: {new MyError('spaghetti')}
,
);
ReactNoopFlightClient.read(transport);
}).toErrorDev(
- 'Date objects cannot be rendered as text children. Try formatting it using toString().\n' +
- '
Current date: {Date}
\n' +
- ' ^^^^^^',
+ 'Error objects cannot be rendered as text children. Try formatting it using toString().\n' +
+ '
Womp womp: {Error}
\n' +
+ ' ^^^^^^^',
{withoutStack: true},
);
});
@@ -728,37 +756,46 @@ describe('ReactFlight', () => {
});
it('should warn in DEV if a toJSON instance is passed to a Client Component', () => {
+ const obj = {
+ toJSON() {
+ return 123;
+ },
+ };
function ClientImpl({value}) {
return
{value}
;
}
const Client = clientReference(ClientImpl);
expect(() => {
- const transport = ReactNoopFlightServer.render(
- ,
- );
+ const transport = ReactNoopFlightServer.render();
ReactNoopFlightClient.read(transport);
}).toErrorDev(
'Only plain objects can be passed to Client Components from Server Components. ' +
- 'Date objects are not supported.',
+ 'Objects with toJSON methods are not supported.',
{withoutStack: true},
);
});
it('should warn in DEV if a toJSON instance is passed to a Client Component child', () => {
+ const obj = {
+ toJSON() {
+ return 123;
+ },
+ };
function ClientImpl({children}) {
return
{children}
;
}
const Client = clientReference(ClientImpl);
expect(() => {
const transport = ReactNoopFlightServer.render(
- Current date: {new Date()},
+ Current date: {obj},
);
ReactNoopFlightClient.read(transport);
}).toErrorDev(
'Only plain objects can be passed to Client Components from Server Components. ' +
- 'Date objects are not supported.\n' +
- ' <>Current date: {Date}>\n' +
- ' ^^^^^^',
+ 'Objects with toJSON methods are not supported. ' +
+ 'Convert it manually to a simple value before passing it to props.\n' +
+ ' <>Current date: {{toJSON: function}}>\n' +
+ ' ^^^^^^^^^^^^^^^^^^^^',
{withoutStack: true},
);
});
diff --git a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMReply-test.js b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMReply-test.js
index b49c158721..59af378829 100644
--- a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMReply-test.js
+++ b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMReply-test.js
@@ -188,4 +188,13 @@ describe('ReactFlightDOMReply', () => {
expect(formDataA2.get('greeting')).toBe('hello');
expect(formDataB2.get('greeting')).toBe('hi');
});
+
+ it('can pass a Date as a reply', async () => {
+ const d = new Date(1234567890123);
+ const body = await ReactServerDOMClient.encodeReply(d);
+ const d2 = await ReactServerDOMServer.decodeReply(body, webpackServerMap);
+
+ expect(d).toEqual(d2);
+ expect(d % 1000).toEqual(123); // double-check the milliseconds made it through
+ });
});
diff --git a/packages/react-server/src/ReactFlightReplyServer.js b/packages/react-server/src/ReactFlightReplyServer.js
index c3d5bef11c..4ba9b8785f 100644
--- a/packages/react-server/src/ReactFlightReplyServer.js
+++ b/packages/react-server/src/ReactFlightReplyServer.js
@@ -447,6 +447,10 @@ function parseModelString(
// Special encoding for `undefined` which can't be serialized as JSON otherwise.
return undefined;
}
+ case 'D': {
+ // Date
+ return new Date(Date.parse(value.substring(2)));
+ }
case 'n': {
// BigInt
return BigInt(value.substring(2));
diff --git a/packages/react-server/src/ReactFlightServer.js b/packages/react-server/src/ReactFlightServer.js
index 17741ad786..dc37f750ae 100644
--- a/packages/react-server/src/ReactFlightServer.js
+++ b/packages/react-server/src/ReactFlightServer.js
@@ -571,6 +571,12 @@ function serializeUndefined(): string {
return '$undefined';
}
+function serializeDateFromDateJSON(dateJSON: string): string {
+ // JSON.stringify automatically calls Date.prototype.toJSON which calls toISOString.
+ // We need only tack on a $D prefix.
+ return '$D' + dateJSON;
+}
+
function serializeBigInt(n: bigint): string {
return '$n' + n.toString(10);
}
@@ -687,10 +693,15 @@ export function resolveModelToJSON(
key: string,
value: ReactClientValue,
): ReactJSONValue {
+ // Make sure that `parent[key]` wasn't JSONified before `value` was passed to us
if (__DEV__) {
// $FlowFixMe[incompatible-use]
const originalValue = parent[key];
- if (typeof originalValue === 'object' && originalValue !== value) {
+ if (
+ typeof originalValue === 'object' &&
+ originalValue !== value &&
+ !(originalValue instanceof Date)
+ ) {
if (objectName(originalValue) !== 'Object') {
const jsxParentType = jsxChildrenParents.get(parent);
if (typeof jsxParentType === 'string') {
@@ -892,6 +903,17 @@ export function resolveModelToJSON(
}
if (typeof value === 'string') {
+ // TODO: Maybe too clever. If we support URL there's no similar trick.
+ if (value[value.length - 1] === 'Z') {
+ // Possibly a Date, whose toJSON automatically calls toISOString
+ // $FlowFixMe[incompatible-use]
+ const originalValue = parent[key];
+ // $FlowFixMe[method-unbinding]
+ if (originalValue instanceof Date) {
+ return serializeDateFromDateJSON(value);
+ }
+ }
+
return escapeStringValue(value);
}