Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions packages/react-client/src/ReactFlightClient.js
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Expand Down
27 changes: 25 additions & 2 deletions packages/react-client/src/ReactFlightReplyClient.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down Expand Up @@ -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. ' +
Expand Down Expand Up @@ -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);
}

Expand Down
69 changes: 53 additions & 16 deletions packages/react-client/src/__tests__/ReactFlight-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -306,6 +306,23 @@ describe('ReactFlight', () => {
);
});

it('can transport Date', async () => {
function ComponentClient({prop}) {
return `prop: ${prop.toISOString()}`;
}
const Component = clientReference(ComponentClient);

const model = <Component prop={new Date(1234567890123)} />;

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 (
Expand Down Expand Up @@ -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(
<input value={new Date()} />,
);
const transport = ReactNoopFlightServer.render(<input value={obj} />);
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' +
' <input value={{toJSON: function}}>\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(
<div>Current date: {new Date()}</div>,
<div>Womp womp: {new MyError('spaghetti')}</div>,
);
ReactNoopFlightClient.read(transport);
}).toErrorDev(
'Date objects cannot be rendered as text children. Try formatting it using toString().\n' +
' <div>Current date: {Date}</div>\n' +
' ^^^^^^',
'Error objects cannot be rendered as text children. Try formatting it using toString().\n' +
' <div>Womp womp: {Error}</div>\n' +
' ^^^^^^^',
{withoutStack: true},
);
});
Expand Down Expand Up @@ -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 <div>{value}</div>;
}
const Client = clientReference(ClientImpl);
expect(() => {
const transport = ReactNoopFlightServer.render(
<Client value={new Date()} />,
);
const transport = ReactNoopFlightServer.render(<Client value={obj} />);
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 <div>{children}</div>;
}
const Client = clientReference(ClientImpl);
expect(() => {
const transport = ReactNoopFlightServer.render(
<Client>Current date: {new Date()}</Client>,
<Client>Current date: {obj}</Client>,
);
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},
);
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
});
});
4 changes: 4 additions & 0 deletions packages/react-server/src/ReactFlightReplyServer.js
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Expand Down
24 changes: 23 additions & 1 deletion packages/react-server/src/ReactFlightServer.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down Expand Up @@ -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') {
Expand Down Expand Up @@ -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);
}

Expand Down