Skip to content

[Flight] Support Temporal objects - #38193

Open
madebyjulz wants to merge 1 commit into
react:mainfrom
madebyjulz:feat/flight-temporal
Open

madebyjulz wants to merge 1 commit into
react:mainfrom
madebyjulz:feat/flight-temporal

Conversation

@madebyjulz

Copy link
Copy Markdown

Summary

Temporal objects can't be passed through Flight today. They have a toJSON method, so they arrive on the other side as plain strings, with a DEV warning that they're not supported (#34142). As Temporal replaces Date, that means manually calling toString() and Temporal.*.from() at every boundary.

This adds support for all eight Temporal types (Instant, ZonedDateTime, PlainDate, PlainTime, PlainDateTime, PlainYearMonth, PlainMonthDay, Duration) in both directions: from Server Components to Client Components, and as arguments to Server Functions. It's behind a new experimental flag, enableFlightTemporal.

Wire format. Like Date ($D) and BigInt ($n), they're encoded inline as a tagged string: $t, a character that identifies the type, then the result of toJSON(). The type has to be encoded because it can't always be inferred from the string. For example, a PlainYearMonth in a non-ISO calendar is formatted as a full date.

Encoding. Objects are identified by their Symbol.toStringTag brand rather than instanceof, so this works with a polyfill that isn't installed on the global (the case in the issue) and with objects from other Realms. On the server this happens where toJSON is already called, so plain objects don't pay for the check. In the reply encoder, JSON.stringify has already called toJSON, so strings are compared to their original value only if they start with a character that a Temporal string can start with, similar to the existing trailing Z check for Date.

Decoding. Objects are revived using the global Temporal. If it isn't available, decoding throws an error that says to load a polyfill. ZonedDateTime is revived with {offset: 'use'} so that it represents the same exact time even if the time zone database of the receiver has different rules than the sender's, which is otherwise an error. The reply decoder rejects Temporal strings longer than 300 characters before parsing them, like the existing limit for BigInt.

Not included. The DEV debug info serializer is unchanged, so Temporal objects in replayed console logs and in Server Component props in DevTools look the same as before. Supporting them there needs a decision on what to do when the client has no Temporal, since debug info is sent regardless of whether the app passes Temporal objects to the client.

This also adds temporal-polyfill as a dev dependency since the Node.js version used in CI doesn't have Temporal. It's pinned to 0.3.x because 1.x is ESM-only and can't be loaded by Jest here.

How did you test this change?

Added 14 tests to ReactFlight-test.js and ReactFlightDOMReply-test.js covering: all eight types in both directions, non-ISO calendars, top-level values, values resolved by a Promise, objects from a polyfill that isn't the global one, strings that look like Temporal objects staying strings, the ZonedDateTime offset behavior, the error when Temporal is missing, the length limit and malformed input in a reply, and the unchanged behavior when the flag is off.

yarn prettier
yarn linc
yarn flow-ci <renderer>   # dom-node, dom-browser, dom-edge-webpack, dom-edge-turbopack,
                          # dom-node-turbopack, dom-browser-turbopack, dom-node-esm,
                          # dom-bun, noop, fabric, dom-fb
yarn test --silent --no-watchman ReactFlight
yarn test --prod --silent --no-watchman ReactFlight
yarn test -r=experimental --silent --no-watchman ReactFlight
yarn test -r=experimental --prod --silent --no-watchman ReactFlight
yarn test-stable --silent --no-watchman ReactFlight
yarn test-stable --prod --silent --no-watchman ReactFlight
yarn test-www --silent --no-watchman ReactFlight
yarn test-www --variant=false --silent --no-watchman ReactFlight
yarn test-classic --silent --no-watchman ReactFlight

All pass on Node.js 24, where the polyfill is the global Temporal. The new tests also pass on Node.js 26 with the native implementation.

The new error codes (613 to 615) were added to codes.json by hand.

Temporal objects have a toJSON method, so today they arrive on the other
side of Flight as plain strings, with a DEV warning that they're not
supported.

This adds support for all eight Temporal types (Instant, ZonedDateTime,
PlainDate, PlainTime, PlainDateTime, PlainYearMonth, PlainMonthDay and
Duration) in both directions: from Server Components to Client
Components, and as arguments to Server Functions. It's behind a new
experimental flag, enableFlightTemporal.

Like Date and BigInt, they're encoded inline as a tagged string: "$t", a
character that identifies the type, then the result of toJSON(). The type
has to be encoded because it can't always be inferred from the string.
E.g. a PlainYearMonth in a non-ISO calendar is formatted as a full date.

Objects are identified by their Symbol.toStringTag brand instead of
instanceof so that this works with a polyfill that isn't installed on the
global, and with objects from other Realms. They're revived using the
global Temporal. If it isn't available, decoding throws an error that
says to load a polyfill.

A ZonedDateTime is revived with {offset: 'use'} so that it represents the
same exact time even if the time zone database of the receiver has
different rules than the sender's. The reply decoder rejects Temporal
strings longer than 300 characters before parsing them, like the existing
limit for BigInt.

The DEV debug info serializer is unchanged.

This also adds temporal-polyfill as a dev dependency since the Node.js
version used in CI doesn't have Temporal.
@meta-cla meta-cla Bot added the CLA Signed label Nov 10, 2026
@github-actions

Copy link
Copy Markdown

A size report will appear here when the build finishes.

Generated by sizebot against b2147b0

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants