-
Notifications
You must be signed in to change notification settings - Fork 47k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add new package with renderToMarkup export #30105
Conversation
The latest updates on your projects. Learn more about Vercel for Git ↗︎
|
We render into an RSC payload using FlightServer, then parse it with FlightClient and then render the result using Fizz.
{ | ||
"name": "react-html", | ||
"version": "19.0.0", | ||
"private": true, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Note that there's no flag here but the whole package is private so doesn't get publish once we land. We can land and iterate.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Was worried this blocks it from being published in react-builds but it works: https://react-builds.vercel.app/api/prs/30105/packages/react-html
packages/react-server/src/forks/ReactFlightServerConfig.dom-legacy.js
Outdated
Show resolved
Hide resolved
"530": "The render was aborted by the server with a promise." | ||
"530": "The render was aborted by the server with a promise.", | ||
"531": "react-html is not supported outside a React Server Components environment.", | ||
"532": "Attempted to render a Client Component from renderToMarkup. This is not supported since it will never hydrate. Only render Server Components with renderToMarkup.", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This may block people from adopting. E.g. you could render client components with renderToStaticMarkup
and were fine with it not being hydrate. renderToMarkup
throwing may require a non-trivial refactor. But let's ship and see.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yea.
- We don't have an option that doesn't involve also allowing them for RSC. Clearly it's a way too easy mistake to import an unmarked Client Component in a Server Component and have it silently no longer work.
- The same mistake applies to e-mails too so it's a good thing.
}, | ||
"homepage": "https://react.dev/", | ||
"peerDependencies": { | ||
"react": "^19.0.0" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we start off here with the correct approach since renderer and React need to be in lock step?
"react": "^19.0.0" | |
"react": "19.0.0" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's change all packages at once if we do that.
Does this support Edge runtimes like Cloudflare Workers or Edge Light? Seems like it does but would be nice to confirm. |
Typo for npm. This isn't covered by tests since our tests manually alias instead of looking at the conditions. Co-authored-by: Sebastian Silbermann <[email protected]>
Co-authored-by: Sebastian Silbermann <[email protected]>
No longer used since there's no Context on the Server.
Yea. It doesn't use any streams and does synchronous scheduling instead of using macrotasks (which maybe it shouldn't). This first PR depends on TextDecoder/TextEncoder being available on the global object which means it's not actually compatible with older Nodes that doesn't have this. However, as noted, this should be fixed in a follow up. |
Name of the package is tbd (straw: `react-html`). It's a new package separate from `react-dom` though and can be used as a standalone package - e.g. also from a React Native app. ```js import {renderToMarkup} from '...'; const html = await renderToMarkup(<Component />); ``` The idea is that this is a helper for rendering HTML that is not intended to be hydrated. It's primarily intended to support a subset of HTML that can be used as embedding and not served as HTML documents from HTTP. For example as e-mails or in RSS/Atom feeds or other distributions. It's a successor to `renderToStaticMarkup`. A few differences: - This doesn't support "Client Components". It can only use the Server Components subset. No useEffect, no useState etc. since it will never be hydrated. Use of those are errors. - You also can't pass Client References so you can't use components marked with `"use client"`. - Unlike `renderToStaticMarkup` this does support async so you can suspend and use data from these components. - Unlike `renderToReadableStream` this does not support streaming or Suspense boundaries and any error rejects the promise. Since there's no feasible way to "client render" or patch up the document. - Form Actions are not supported since in an embedded environment there's no place to post back to across versions. You can render plain forms with fixed URLs though. - You can't use any resource preloading like `preload()` from `react-dom`. ## Implementation This first version in this PR only supports Server Components since that's the thing that doesn't have an existing API. Might add a Client Components version later that errors. We don't want to maintain a completely separate implementation for this use case so this uses the `dom-legacy` build dimension to wire up a build that encapsulates a Flight Server -> Flight Client -> Fizz stream to render Server Components that then get SSR:ed. There's no problem to use a Flight Client in a Server Component environment since it's already supported for Server-to-Server. Both of these use a bundler config that just errors for Client References though since we don't need any bundling integration and this is just a standalone package. Running Fizz in a Server Component environment is a problem though because it depends on "react" and it needs the client version. Therefore, for this build we embed the client version of "react" shared internals into the build. It doesn't need anything to be able to use those APIs since you can't call the client APIs anyway. One unfortunate thing though is that since Flight currently needs to go to binary and back, we need TextEncoder/TextDecoder to be available but this shouldn't really be necessary. Also since we use the legacy stream config, large strings that use byteLengthOfChunk errors atm. This needs to be fixed before shipping. I'm not sure what would be the best layering though that isn't unnecessarily burdensome to maintain. Maybe some kind of pass-through protocol that would also be useful in general - e.g. when Fizz and Flight are in the same process. --------- Co-authored-by: Sebastian Silbermann <[email protected]> DiffTrain build for commit ffec9ec.
Follow up to #30105. This supports `renderToMarkup` in a non-RSC environment (not the `react-server` condition). This is just a Fizz renderer but it errors at runtime when you use state, effects or event handlers that would require hydration - like the RSC version would. (Except RSC can give early errors too.) To do this I have to move the `react-html` builds to a new `markup` dimension out of the `dom-legacy` dimension so that we can configure this differently from `renderToString`/`renderToStaticMarkup`. Eventually that dimension can go away though if deprecated. That also helps us avoid dynamic configuration and we can just compile in the right configuration so the split helps anyway. One consideration is that if a compiler strips out useEffects or inlines initial state from useState, then it would not get called an the error wouldn't happen. Therefore to preserve semantics, a compiler would need to inject some call that can check the current renderer and whether it should throw. There is an argument that it could be useful to not error for these because it's possible to write components that works with SSR but are just optionally hydrated. However, there's also an argument that doing that silently is too easy to lead to mistakes and it's better to error - especially for the e-mail use case where you can't take it back but you can replay a queue that had failures. There are other ways to conditionally branch components intentionally. Besides if you want it to be silent you can still use renderToString (or better yet renderToReadableStream). The primary mechanism is the RSC environment and the client-environment is really the secondary one that's only there to support legacy environments. So this also ensures parity with the primary environment.
Follow up to #30105. This supports `renderToMarkup` in a non-RSC environment (not the `react-server` condition). This is just a Fizz renderer but it errors at runtime when you use state, effects or event handlers that would require hydration - like the RSC version would. (Except RSC can give early errors too.) To do this I have to move the `react-html` builds to a new `markup` dimension out of the `dom-legacy` dimension so that we can configure this differently from `renderToString`/`renderToStaticMarkup`. Eventually that dimension can go away though if deprecated. That also helps us avoid dynamic configuration and we can just compile in the right configuration so the split helps anyway. One consideration is that if a compiler strips out useEffects or inlines initial state from useState, then it would not get called an the error wouldn't happen. Therefore to preserve semantics, a compiler would need to inject some call that can check the current renderer and whether it should throw. There is an argument that it could be useful to not error for these because it's possible to write components that works with SSR but are just optionally hydrated. However, there's also an argument that doing that silently is too easy to lead to mistakes and it's better to error - especially for the e-mail use case where you can't take it back but you can replay a queue that had failures. There are other ways to conditionally branch components intentionally. Besides if you want it to be silent you can still use renderToString (or better yet renderToReadableStream). The primary mechanism is the RSC environment and the client-environment is really the secondary one that's only there to support legacy environments. So this also ensures parity with the primary environment. DiffTrain build for commit 1e241f9.
Stoked for this |
Name of the package is tbd (straw:
react-html
). It's a new package separate fromreact-dom
though and can be used as a standalone package - e.g. also from a React Native app.The idea is that this is a helper for rendering HTML that is not intended to be hydrated. It's primarily intended to support a subset of HTML that can be used as embedding and not served as HTML documents from HTTP. For example as e-mails or in RSS/Atom feeds or other distributions. It's a successor to
renderToStaticMarkup
.A few differences:
"use client"
.renderToStaticMarkup
this does support async so you can suspend and use data from these components.renderToReadableStream
this does not support streaming or Suspense boundaries and any error rejects the promise. Since there's no feasible way to "client render" or patch up the document.preload()
fromreact-dom
.Implementation
This first version in this PR only supports Server Components since that's the thing that doesn't have an existing API. Might add a Client Components version later that errors.
We don't want to maintain a completely separate implementation for this use case so this uses the
dom-legacy
build dimension to wire up a build that encapsulates a Flight Server -> Flight Client -> Fizz stream to render Server Components that then get SSR:ed.There's no problem to use a Flight Client in a Server Component environment since it's already supported for Server-to-Server. Both of these use a bundler config that just errors for Client References though since we don't need any bundling integration and this is just a standalone package.
Running Fizz in a Server Component environment is a problem though because it depends on "react" and it needs the client version. Therefore, for this build we embed the client version of "react" shared internals into the build. It doesn't need anything to be able to use those APIs since you can't call the client APIs anyway.
One unfortunate thing though is that since Flight currently needs to go to binary and back, we need TextEncoder/TextDecoder to be available but this shouldn't really be necessary. Also since we use the legacy stream config, large strings that use byteLengthOfChunk errors atm. This needs to be fixed before shipping. I'm not sure what would be the best layering though that isn't unnecessarily burdensome to maintain. Maybe some kind of pass-through protocol that would also be useful in general - e.g. when Fizz and Flight are in the same process.