Skip to content

[Flight] Fix encodeReply for JSX with temporary references#35730

Merged
unstubbable merged 1 commit intofacebook:mainfrom
unstubbable:jsx-promise-encode-reply
Feb 9, 2026
Merged

[Flight] Fix encodeReply for JSX with temporary references#35730
unstubbable merged 1 commit intofacebook:mainfrom
unstubbable:jsx-promise-encode-reply

Conversation

@unstubbable
Copy link
Collaborator

encodeReply throws "React Element cannot be passed to Server Functions from the Client without a temporary reference set" when a React element is the root value of a serializeModel call (either passed directly or resolved from a promise), even when a temporary reference set is provided.

The cause is that resolveToJSON hits the REACT_ELEMENT_TYPE switch case before reaching the existingReference/modelRoot check that regular objects benefit from. The synthetic JSON root created by JSON.stringify is never tracked in writtenObjects, so parentReference is undefined and the code falls through to the throw. This adds a modelRoot check in the REACT_ELEMENT_TYPE case, following the same pattern used for promises and plain objects.

The added JSX as root model test also uncovered a pre-existing crash in the Flight Client: when the JSX element round-trips back, it arrives as a frozen object (client-created elements are frozen in DEV), and Object.defineProperty for _debugInfo fails because frozen objects are non-configurable. The same crash can occur with JSX exported as a client reference. For now, we're adding !Object.isFrozen() guards in moveDebugInfoFromChunkToInnerValue and addAsyncInfo to prevent the crash, which means debug info is silently dropped for frozen elements. The proper fix would likely be to clone the element so each rendering context gets its own mutable copy with correct debug info.

closes #34984
closes #35690

`encodeReply` throws "React Element cannot be passed to Server Functions
from the Client without a temporary reference set" when a React element
is the root value of a `serializeModel` call (either passed directly or
resolved from a promise), even when a temporary reference set is
provided.

The cause is that `resolveToJSON` hits the `REACT_ELEMENT_TYPE` switch
case before reaching the `existingReference`/`modelRoot` check that
regular objects benefit from. The synthetic JSON root created by
`JSON.stringify` is never tracked in `writtenObjects`, so
`parentReference` is `undefined` and the code falls through to the
throw. This adds a `modelRoot` check in the `REACT_ELEMENT_TYPE` case,
following the same pattern used for promises and plain objects.

The added `JSX as root model` test also uncovered a pre-existing crash
in the Flight Client: when the JSX element round-trips back, it arrives
as a frozen object (client-created elements are frozen in DEV), and
`Object.defineProperty` for `_debugInfo` fails because frozen objects
are non-configurable. The same crash can occur with JSX exported as a
client reference. For now, we're adding `!Object.isFrozen()` guards in
`moveDebugInfoFromChunkToInnerValue` and `addAsyncInfo` to prevent the
crash, which means debug info is silently dropped for frozen elements.
The proper fix would likely be to clone the element so each rendering
context gets its own mutable copy with correct debug info.

closes facebook#34984
closes facebook#35690
@meta-cla meta-cla bot added the CLA Signed label Feb 9, 2026
@github-actions github-actions bot added the React Core Team Opened by a member of the React Core Team label Feb 9, 2026
@react-sizebot
Copy link

Comparing: 2dd9b7c...a87af0f

Critical size changes

Includes critical production bundles, as well as any change greater than 2%:

Name +/- Base Current +/- gzip Base gzip Current gzip
oss-stable/react-dom/cjs/react-dom.production.js = 6.84 kB 6.84 kB = 1.88 kB 1.88 kB
oss-stable/react-dom/cjs/react-dom-client.production.js = 610.35 kB 610.35 kB = 107.89 kB 107.89 kB
oss-experimental/react-dom/cjs/react-dom.production.js = 6.84 kB 6.84 kB = 1.88 kB 1.88 kB
oss-experimental/react-dom/cjs/react-dom-client.production.js = 676.28 kB 676.28 kB = 118.85 kB 118.85 kB
facebook-www/ReactDOM-prod.classic.js = 696.77 kB 696.77 kB = 122.49 kB 122.49 kB
facebook-www/ReactDOM-prod.modern.js = 687.15 kB 687.15 kB = 120.89 kB 120.89 kB

Significant size changes

Includes any change greater than 0.2%:

Expand to show
Name +/- Base Current +/- gzip Base gzip Current gzip
oss-experimental/react-server-dom-esm/esm/react-server-dom-esm-client.browser.production.js +0.90% 99.71 kB 100.61 kB +1.30% 20.28 kB 20.54 kB
oss-stable-semver/react-server-dom-esm/esm/react-server-dom-esm-client.browser.production.js +0.90% 99.71 kB 100.61 kB +1.30% 20.28 kB 20.54 kB
oss-stable/react-server-dom-esm/esm/react-server-dom-esm-client.browser.production.js +0.90% 99.71 kB 100.61 kB +1.30% 20.28 kB 20.54 kB
oss-stable-semver/react-server-dom-esm/esm/react-server-dom-esm-client.browser.development.js +0.45% 233.05 kB 234.09 kB +0.69% 51.66 kB 52.01 kB
oss-stable/react-server-dom-esm/esm/react-server-dom-esm-client.browser.development.js +0.45% 233.07 kB 234.12 kB +0.69% 51.68 kB 52.04 kB
oss-experimental/react-server-dom-esm/esm/react-server-dom-esm-client.browser.development.js +0.45% 233.08 kB 234.12 kB +0.69% 51.68 kB 52.04 kB

Generated by 🚫 dangerJS against a87af0f

@unstubbable unstubbable marked this pull request as ready for review February 9, 2026 13:48
});

it('can pass JSX as root model through a round trip using temporary references', async () => {
const jsx = <div />;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this server or client JSX?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Client, which is the default in our tests, and we use ReactServer.createElement if we want server JSX.

Copy link
Collaborator

@eps1lon eps1lon left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's discuss a proper fix.

Copy link
Collaborator

@eps1lon eps1lon left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Going with this since there's another bug where debug info is added multiple times for shared elements. No info is better than wrong info.

@unstubbable unstubbable merged commit b07aa7d into facebook:main Feb 9, 2026
243 checks passed
@unstubbable unstubbable deleted the jsx-promise-encode-reply branch February 9, 2026 15:18
unstubbable pushed a commit to vercel/next.js that referenced this pull request Feb 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA Signed React Core Team Opened by a member of the React Core Team

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants