diff --git a/packages/react-client/src/ReactFlightClient.js b/packages/react-client/src/ReactFlightClient.js index e14b6a7699..e3d0b6754e 100644 --- a/packages/react-client/src/ReactFlightClient.js +++ b/packages/react-client/src/ReactFlightClient.js @@ -559,6 +559,22 @@ export function parseModelString( throw chunk.reason; } } + case 'I': { + // $Infinity + return Infinity; + } + case '-': { + // $-0 or $-Infinity + if (value[2] === '0') { + return -0; + } else { + return -Infinity; + } + } + case 'N': { + // $NaN + return NaN; + } case 'u': { // matches "$undefined" // Special encoding for `undefined` which can't be serialized as JSON otherwise. diff --git a/packages/react-client/src/ReactFlightReplyClient.js b/packages/react-client/src/ReactFlightReplyClient.js index 2e85d050e9..6f9581c36e 100644 --- a/packages/react-client/src/ReactFlightReplyClient.js +++ b/packages/react-client/src/ReactFlightReplyClient.js @@ -71,6 +71,24 @@ function serializeSymbolReference(name: string): string { return '$S' + name; } +function serializeNumber(number: number): string | number { + if (Number.isFinite(number)) { + if (number === 0 && 1 / number === -Infinity) { + return '$-0'; + } else { + return number; + } + } else { + if (number === Infinity) { + return '$Infinity'; + } else if (number === -Infinity) { + return '$-Infinity'; + } else { + return '$NaN'; + } + } +} + function serializeUndefined(): string { return '$undefined'; } @@ -224,10 +242,14 @@ export function processReply( return escapeStringValue(value); } - if (typeof value === 'boolean' || typeof value === 'number') { + if (typeof value === 'boolean') { return value; } + if (typeof value === 'number') { + return serializeNumber(value); + } + if (typeof value === 'undefined') { return serializeUndefined(); } diff --git a/packages/react-client/src/__tests__/ReactFlight-test.js b/packages/react-client/src/__tests__/ReactFlight-test.js index 821db2e52c..9fc2da89a0 100644 --- a/packages/react-client/src/__tests__/ReactFlight-test.js +++ b/packages/react-client/src/__tests__/ReactFlight-test.js @@ -263,6 +263,30 @@ describe('ReactFlight', () => { expect(ReactNoop).toMatchRenderedOutput(null); }); + it('can transport weird numbers', async () => { + const nums = [0, -0, Infinity, -Infinity, NaN]; + function ComponentClient({prop}) { + expect(prop).not.toBe(nums); + expect(prop).toEqual(nums); + expect(prop.every((p, i) => Object.is(p, nums[i]))).toBe(true); + return `prop: ${prop}`; + } + const Component = clientReference(ComponentClient); + + const model = ; + + const transport = ReactNoopFlightServer.render(model); + + await act(async () => { + ReactNoop.render(await ReactNoopFlightClient.read(transport)); + }); + + expect(ReactNoop).toMatchRenderedOutput( + // already checked -0 with expects above + 'prop: 0,0,Infinity,-Infinity,NaN', + ); + }); + it('can transport BigInt', async () => { function ComponentClient({prop}) { return `prop: ${prop} (${typeof prop})`; 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 e09addaf5e..f21fea4d41 100644 --- a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMReply-test.js +++ b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMReply-test.js @@ -76,6 +76,18 @@ describe('ReactFlightDOMReply', () => { expect(items).toEqual(['A', 'B', 'C']); }); + it('can pass weird numbers as a reply', async () => { + const nums = [0, -0, Infinity, -Infinity, NaN]; + const body = await ReactServerDOMClient.encodeReply(nums); + const nums2 = await ReactServerDOMServer.decodeReply( + body, + webpackServerMap, + ); + + expect(nums).toEqual(nums2); + expect(nums.every((n, i) => Object.is(n, nums2[i]))).toBe(true); + }); + it('can pass a BigInt as a reply', async () => { const body = await ReactServerDOMClient.encodeReply(90071992547409910000n); const n = await ReactServerDOMServer.decodeReply(body, webpackServerMap); diff --git a/packages/react-server/src/ReactFlightReplyServer.js b/packages/react-server/src/ReactFlightReplyServer.js index 7c7dce1e0e..e91fc7d5e8 100644 --- a/packages/react-server/src/ReactFlightReplyServer.js +++ b/packages/react-server/src/ReactFlightReplyServer.js @@ -397,6 +397,22 @@ function parseModelString( key, ); } + case 'I': { + // $Infinity + return Infinity; + } + case '-': { + // $-0 or $-Infinity + if (value[2] === '0') { + return -0; + } else { + return -Infinity; + } + } + case 'N': { + // $NaN + return NaN; + } case 'u': { // matches "$undefined" // Special encoding for `undefined` which can't be serialized as JSON otherwise. diff --git a/packages/react-server/src/ReactFlightServer.js b/packages/react-server/src/ReactFlightServer.js index 1c127c886b..17741ad786 100644 --- a/packages/react-server/src/ReactFlightServer.js +++ b/packages/react-server/src/ReactFlightServer.js @@ -549,6 +549,24 @@ function serializeProviderReference(name: string): string { return '$P' + name; } +function serializeNumber(number: number): string | number { + if (Number.isFinite(number)) { + if (number === 0 && 1 / number === -Infinity) { + return '$-0'; + } else { + return number; + } + } else { + if (number === Infinity) { + return '$Infinity'; + } else if (number === -Infinity) { + return '$-Infinity'; + } else { + return '$NaN'; + } + } +} + function serializeUndefined(): string { return '$undefined'; } @@ -877,10 +895,14 @@ export function resolveModelToJSON( return escapeStringValue(value); } - if (typeof value === 'boolean' || typeof value === 'number') { + if (typeof value === 'boolean') { return value; } + if (typeof value === 'number') { + return serializeNumber(value); + } + if (typeof value === 'undefined') { return serializeUndefined(); }