Skip to content

Conversation

@jacob-ebey
Copy link

Summary

This adds a new react-server-dom-vite package implementing the RSC touch points in a way that is suitable for use with the Vite bundler.

Differences from other implementations:

  • No prescribed manifest formats
  • Client reference encoding details are left up to the Vite plugin author.
  • Client reference loading is left up to the Vite plugin author.

Other implementations prescribe a "manifest" format that plugin authors must implement and react-server-dom-xyz packages accept to know how to do bundler specific things (__webpack_require__, etc.).

Instead, this package implements the second argument to the encode and decode bookends with an API that puts plugin authors in control of encoding and loading references.

This runtime-manifest APIs looks like this:

export type ServerManifest = {
  resolveClientReference<T>(
    metadata: ClientReferenceMetadata
  ): ClientReference<T>,
  resolveServerReference<T>(id: ServerReferenceId): ClientReference<T>,
}; // API for loading references

export opaque type ClientReference<T> = {
  get(): T,
  preload(): null | Promise<void>,
};

export opaque type ClientReferenceMetadata = mixed;

export type ServerReferenceId = string;

//--------------------------------------------------

export type ClientManifest = {
  resolveClientReferenceMetadata<T>(
    clientReference: ClientReference<T>
  ): ClientReferenceMetadata,
  resolveServerReference<T>(id: ServerReferenceId): ClientReference<T>,
}; // API for loading client reference metadata and server references

export type ServerReferenceId = string;

export type ClientReferenceMetadata = mixed;

export type ClientReferenceKey = string;

An example implementation supporting arbitrary Vite runtimes through the Environments API can be found here: https://github.com/jacob-ebey/vite-plugins/blob/2ffebb24e284a4bb809cc6cbc0fcbc094b136c4a/packages/vite-react-server-dom/src/index.ts#L233

Peeking at the Plugin implementation, you'll notice the different shapes of the ClientReferenceMetadata between development and production, as well as the different implementations of ClientReference's get and preload.

This is partially due to Vite's development philosophy, as well as the lack of a global module management system such as the webpack or parcel caches.

Note: I've published an experimental version of this package under @jacob-ebey/react-server-dom-vite along with a Vite plugin implementation for experimentation and use in the fixture.

Side note: I believe the implementation of a package like this would allow the webpack, parcel, esm, etc. implementations to simplify and focus their concerns on implementing this layer. It would also make switching bundlers / environments more: reactFunc(node, SWAP_ME()) instead of SWAP_ME(node, ALSO_SWAP_ME).

How did you test this change?

I'm implemented a fixture that can be found and ran at fixtures/flight-vite. I've also implemented basic unit tests that can be found at packages/react-server-dom-vite/src/__tests__/ReactFlightViteDOM-test.js.

@vercel
Copy link

vercel bot commented Dec 13, 2024

The latest updates on your projects. Learn more about Vercel for Git ↗︎

Name Status Preview Comments Updated (UTC)
react-compiler-playground ✅ Ready (Inspect) Visit Preview 💬 Add feedback Dec 19, 2024 7:05pm

},
"dependencies": {
"@jacob-ebey/react-server-dom-vite": "19.0.0-experimental.14",
"@jacob-ebey/vite-react-server-dom": "0.0.6",
Copy link
Collaborator

@sebmarkbage sebmarkbage Dec 13, 2024

Choose a reason for hiding this comment

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

Ideally it should be possible to install only react-server-dom-vite (from React) and the official vite stuff. At least for the bare minimum. You can always have special stuff in bigger frameworks but just for the core integration. As it is right now you need to install this third party thing.

It also makes it hard for us to make suggestions for how to improve the layering between the packages (and the dependency injection protocol). E.g. we can't change anything in React without also changing this package in lock step.

Can this package (@jacob-ebey/vite-react-server-dom) just be inlined into the React repo?

Either into the fixture for things related to just wiring up the host, or if they're related to the vite plugin itself then it can move into react-server-dom-vite.

For example, all that virtual file and manifest generating stuff seems like it should be living inside react-server-dom-vite rather than every user have to reimplement in different ways.

Copy link
Collaborator

@sebmarkbage sebmarkbage Dec 13, 2024

Choose a reason for hiding this comment

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

The implementation inside react-server-dom-vite can be completely different between development and production if needed. We can compile a version for each.

@sebmarkbage
Copy link
Collaborator

sebmarkbage commented Dec 13, 2024

It would also make switching bundlers / environments more: reactFunc(node, SWAP_ME()) instead of SWAP_ME(node, ALSO_SWAP_ME).

I believe this is possible one day but we're not there yet. I'm not happy with the protocol for any of the existing bundlers an we're still adding and changing features. There's also certain things like prepareDestinationForModule that should be more automatic but requires unfortunate configuration and not ever framework might need the same configuration depending on how it's hosted. So I get the philosophy but we're not there yet.

The idea is that also ideally there wouldn't be a need for a plugin author if it was just natively built-in (or official plugin) to Vite like it is in Parcel. So the idea is that if code starts off living in react-server-dom-vite it can gradually move into Vite itself as new features are added natively.

One day I hope we can standardize on a protocol just like how require became a pseudo-standard and it can just be reactFunc(node) where the bundler just natively has support for the concept of client and server references in the same way. For example I don't think the ClientReference proxy data type should be something React specific. In the Meta implementation of RSC this was its own type which is why there's hooks like isClientReference() to support a generic ClientReference type provided by the bundler - which ideally would be standardized.

@jacob-ebey jacob-ebey closed this Dec 14, 2024
@jacob-ebey jacob-ebey reopened this Dec 14, 2024
@jacob-ebey
Copy link
Author

jacob-ebey commented Dec 14, 2024

@sebmarkbage in reply to both comments:

I could inline the plugin, but like the webpack version what is the point if it's not the implementation used outside the fixture? Just like the webpack implementation, no one actually uses it.

All the different react-server-dom-xyz packages just implement the "manifest" API exposed here behind an opaque layer exposed as the "same" API with different signatures? Why not standardize the public bundler API on the "react" details instead of requiring implementing these "touch points" in this repo?

If the "touch points" were exposed as they are here, there is no need or bundler specific packages, and you (react) could simplify and say "RSC is ready for everyone who is willing to implement this API" instead of "RSC is ready for framework authors".

Why compile a version for each instead of exposing a generic API that allows me, or react-server-dom-webpack to implement what's needed on top of core supported concepts?

I hope I'm not missing something, but the two concepts here are "info to dynamically load something", and "how to dynamically load that thing". That's what I've implemented and hope it wasn't out of naivety.

@jacob-ebey
Copy link
Author

I'll roll the plugin into here as the next step, but exposing a programatic manifest is the piece I'd like to see as part of core React. It feels very limiting to restrict "how to load things" behind a layer that specific to whoever decided to implement the bundler plugin.

…hide internal module loading / encoding API behind a more base plugin
@jacob-ebey
Copy link
Author

Thanks @hi-ogawa for rolling a more minimal fixture.

@hi-ogawa
Copy link
Contributor

Thanks @jacob-ebey for putting this together and thanks React team for quick review!

After talking with Jacob, I thought it would help React team if they can see the overall picture of what's currently implemented in framework level (and not Vite core), so I made a minimal example while learning this manifest API and suggested to include it as fixture here. I hope this demonstrates how things are wired together and if you have any suggestion for improvement, I'm interested to hear 🙏

The programatic manifest API Jacob proposed here makes sense to me. Folks in the Vite ecosystem is mostly hacking around __webpack_require__ and __webpack_chunk_load__ globals, but what we have here seems to provide a very clean alternative. I'm hoping that such abstraction living only in react-server-dom-vite wouldn't be an issue for React team's maintenance.

The idea is that also ideally there wouldn't be a need for a plugin author if it was just natively built-in (or official plugin) to Vite like it is in Parcel. So the idea is that if code starts off living in react-server-dom-vite it can gradually move into Vite itself as new features are added natively.

As seen in the minimal fixture, Vite core feature to help RSC is mostly just Vite 6's environment API (and even on Vite 5, currently people are getting around the limitation by managing multiple Vite instances). At this point, we don't have a such blessed official plugin yet and each framework is rolling out an own plugins (which is a part of an entirely separate Vite framework). Even if we eventually have something like that (like we currently have @vitejs/plugin-react), I don't think it would make sense to inline such ultimate implementation in react-server-dom-vite by the similar reason as how @vitejs/plugin-react is commonly used as a separately dependency by the frameworks and also Next.js or others don't keep ast transform or any plugin concepts in react-server-dom-xxx packages.

@sebmarkbage
Copy link
Collaborator

It's still to unopinionated. We want you to have an opinion about the right way of doing things for Vite. E.g. there's a bunch of little issues here (like sync module invocation and CSS loading) that we can discuss in a follow up or I can even fix myself. There's many things we don't like about the current interface that we want to improve. If there's 8 different ways of doing things for Vite we can't go around having a discussion about it in each one.

If it's unopinionated then you don't need us, you can just publish a fork and keep it up to date. The point of landing something here is that it becomes the opinionated "official" way to integrate with Vite and something the React team commits to maintain. We should able to change details here and downstream forks follow. E.g. we change here first all the time and then Next.js forks in the plugin loaders have to scramble to follow.

I'm not that rigid on exact interface. It can be built up of building blocks like syntax parsing plugins etc that are defined elsewhere. It needs some coherent opinion about what it should look like coming together.

@sebmarkbage
Copy link
Collaborator

For the example, we can say that the strategy in flight-vite-mini fixture is the one we recommend. Then I should be able to start moving code from there into react-server-dom-vite since that's how you should've done your plugin anyway, it should be easy enough to migrate even if it's technically a breaking change. That would allow be to do refactors and new architecture changes all within the repo.

@jacob-ebey
Copy link
Author

jacob-ebey commented Jan 3, 2025

@sebmarkbage If you have the time / energy, I'd appreciate if you'd spend a bit of them to address what's important to you here.

As @hi-ogawa mentioned, the Vite ecosystem is pretty "piece things together". CSS loading for example is implemented individually by everyone and does not expect to be standardized. I'm also unsure of what you mean by sync module loading in an inherently async ESM world (unless you mean a module cache for subsequent use?).

It probably doesn't help convince you of the interface, or being an appropriate building block in the Vite ecosystem, but here is this same API being used to run in Cloudflare's Workerd environment on top of Vite (interplay between this and other Vite ecosystem plugins): https://github.com/jacob-ebey/cf-react-server-template/blob/482008b8fd9d43cf8ebfbc992c899c275043370b/vite.config.ts#L18

@galvez
Copy link

galvez commented Jan 31, 2025

@jacob-ebey @hi-ogawa Does it make sense to try and aim to have this as an official @vitejs/ plugin and the opinionated part becomes an official recommendation on the React 19 docs towards one approach or another? I'd be happy to write the necessary bits of documentation for this. I've had the new release of @fastify/react stuck for simply not knowing what is the right approach — currently I'm using a concoction of @hi-ogawa's code and my own via react-server-dom-webpack which feels extremely hacky, so I for one am extremely eager to have a more elegant alternative as the one proposed here.

@rickhanlonii
Copy link
Member

cc @yyx990803 who has kindly offered to help collaborate on this.

For context, to summarize what @sebmarkbage said above: our goal in having these packages live in the React repo are to collaborate with bundlers to understand the layering, and design a protocol that works across all of them.

We're still changing features and refining the protocol for all of the existing bundlers to try and find the right layering. By having the implementation in the React repo, we can make suggestions for how to improve the layering between the packages. If we make updates to the protocol, we can update it in all bundlers in one PR, and versioned together, instead of spread out across many packages.

One day we hope we can standardize on a protocol just like how require became a pseudo-standard and the bundler just natively has support for the concept of client and server references in the same way. To do that, more and more of the protocol would would need to get native support in the bundlers (so there wouldn't be a need for a plugin author, it wold just be natively built-in). Plugins would have bundler options, but wouldn't implement the whole protocol themselves. It sounds like @yyx990803 is on board with that goal.

For this to work though, we really need to focus on one way of doing it (since more opinions increases the difficulty) and it needs to all live in the repo. If that doesn't work for ya'll, you can just publish a fork and keep it up to date. It would make collaboration and making suggestions more difficult, but realistically so would trying to do it all here.

The benefit of landing something here is that it becomes the opinionated "official" way to integrate with Vite and something the React team commits to maintain along with you.

@jacob-ebey
Copy link
Author

I'm just going to drop more experimentation here in-case it helps.

https://github.com/jacob-ebey/vite-environment-attributes

This implements a concept of import { abc } from "xyz" with { env: "vite_env_name" }; to allow multiple input module graphs defined by Vite Environments to be bundled into a single executable module graph allowing for a single process deployment.

@nicobrinkkemper
Copy link

nicobrinkkemper commented Mar 12, 2025

For anyone interested, I made an implementation that works today https://github.com/nicobrinkkemper/vite-plugin-react-server. It uses react-server-dom-esm and configures vite/rollup to "just work" at least in the basic scenario's we used to use something like create-react-app. Of course if a vite loader does eventually drop, I'm happy to put that in instead and let you know the results

@rickhanlonii
Copy link
Member

Update: as of React 19.1, Parcel now has support for RSC in the stable React channel.


I'm curious if you've had a chance to look at this @yyx990803?

I see two options here:

  • Option 1: Update this PR so you so you don't need to install @jacob-ebey/react-server-dom-vite or @jacob-ebey/vite-react-server-dom to use it and all the code for a minimal integration lives in this repo. There can still be ways to allow customization, the same way Parcel allows for, but this would allow us to see how react-server-dom-vite is used so we can help maintain it and standardize the protocol across bundlers.
  • Option 2: Someone else can maintain a separate repo and publishing a vite integration separately, taking ownership over following the changes and making any changes needed to keep it up to date.

If we choose (1), I'm happy to push the changed needed.

@brillout
Copy link
Contributor

brillout commented Apr 6, 2025

Some feedback from our side after building vike-react-rsc.

Alternative RSC design

We'd like to experiment with alternative RSC designs to address what we perceive to be design issues.

In order to experiment with real-world apps, we would like to enable vike-react (classic React) users to progressively adopt vike-react-rsc (RSC). The idea is to treat .jsx files as Client Components by default, while requiring Server Components to explicitly set "use server". So that users can try out and adopt RSC on a component-by-component basis.

We wonder whether this has been discussed before and where we can share RSC ideas with others. A central place for public discussions around RSC would be helpful. (There was a private Facebook group but it doesn't seem to be active anymore.)

Progressive adoption is key for us to experiment in real-world scenarios. Without it, most users will stick with vike-react, as they won't want to refactor their entire app to try out an experimental RSC implementation. We have users eager to try Server Components — but only if there's a progressive path.

Vite Environment API vs Import Queries

We are questioning whether (artificially?) splitting the server build into dist/server/ and dist/rsc/ actually makes sense. In the end both run in the same process, while having dist/server/ import from dist/rsc/ seems to be an antipattern.

Vite’s Environment API makes a lot of sense for targeting different deployment environments, but we wonder whether it's the right abstraction for RSC.

Maybe import queries (e.g. ?rsc) could be a leaner approach. Vike already uses that approach to have a complete parallel module graph. It works by rewriting imports, which can then also be used for importing react with the react-server export condition.

@dai-shi
Copy link
Contributor

dai-shi commented Apr 8, 2025

If we choose (1), I'm happy to push the changed needed.

I believe our general preference is Option 1.

Since @jacob-ebey would probably need some direction,
Ricky, if you could help out, that would be great.

@yyx990803
Copy link

Sorry for the late follow up... GitHub notifications of old PRs often get buried.

To share some context here:

  • @hi-ogawa has been researching an approach that is a bit different from this PR (I'll ask him to share more details in this thread)

  • We are in the process of re-architecting Vite to be based on Rolldown, and intend to move from the [unbundled dev + bundled build] approach to fully bundled for both dev and prod. Considering the amount of moving pieces during this period and the level of integration RSC needs at the bundler-native level, I think we will have to wait until we finish that work before we properly tackle RSC support in Vite (and by extension Rolldown). Re-visiting RSC after Vite fully moves to Rolldown also makes it more straightforward as we will be dealing with just one bundler with more consistent behavior between dev and prod.

    Timeline-wise, we will soon announce the availability of rolldown-vite which is [unbundled esm + rolldown production build], and full-bundled mode will likely be introduced as an opt-in feature before Q3. It will still take time for it to stabilize in Q3, but I think that would already be a good time to start looking into proper RSC integration.

@hi-ogawa
Copy link
Contributor

Thanks everyone for the discussion around Vite RSC integration.

In my opinion, Jacob's abstract manifest API should work well for the Vite ecosystem (both now and future), but I understand that the API surface seems unnecessarily general, which can blur Vite-side technical details and hinder collaboration with the React team.

I have an idea to reduce the API surface and clarify the need for Vite integration, which is to allow only customizing the async module loading mechanism. (TLDR, my proposed API is setRequireModule. Here is the diff on top of this PR hi-ogawa@653cd19)

But, before diving into that, let me provide some background on some technical details of RSC integration in today's Vite. I'm hoping this clarifies what's minimally needed and desired for the Vite ecosystem in general.

  • async module loading
    • Since Vite (current and also likely with Rolldown too) uses ESM for its output module, the client/server reference is defined by the ESM module and export name. I imagine this works more or less like react-server-dom-esm, but react-server-dom-esm has import hard-coded in the package and also the async module is cached, which makes it unfit for the Vite dev use case.
    • The way I do this is I use react-server-dom-webpack instead and use async: true for all references. By defining __webpack_require__ globals, one can customize module loading entirely. Though there's a caveat of cache / promise stability here too, but it can be worked around.
  • reference chunks array is likely not needed
    • On the browser build, Vite optimizes transitive dynamic import chunks by default https://vite.dev/guide/features.html#async-chunk-loading-optimization.
    • For SSR module init/preloading injection (prepareDestination...), the same seems to be achievable by calling ReactDOM.preloadModule manually during async module loading in the SSR context.
    • Module preloading/injection is commonly done by frameworks already, not only for SSR but also for client-side route assets preloading, so there doesn't seem to be a strong benefit to partially handling this logic from react-server-dom-vite.
  • multiple builds coordination is left to the Vite framework.
    • This means how to implement "async module loading" and "SSR preloading" cannot be defined solely by react-server-dom-vite. And that's the reason why we'll probably need an API surface like setRequireModule at least to allow framework customization.
  • CSS loading is left to the Vite framework.
    • Vite doesn't provide guidance on how to manage CSS on the server (as it involves multiple builds), so CSS handling needs to be explored by each framework for now (like splitting with an own convention like Parcel's use server-entry and injecting a hoistable stylesheet link, or splitting by fs-based routing and hoisting them manually during SSR, which I think is a common approach in Vite SSR frameworks currently.)

Additionally, the following two constraints are more fundamental to the current Vite bundler architecture, so they are likely not solvable by any design we explore for react-server-dom-vite.

However, as Evan said, in the future with Rolldown, Vite will have a full-bundled dev mode, and that can eliminate node_modules specific quirks. Additionally, RSC-targeted bundler features inspired by webpack/parcel/turbopack may be explored to achieve efficient builds related to client/server reference discoveries.

Given the above context, the available options seem to be either:

  1. Aim to land react-server-dom-vite by resolving concerns and taking in suggestions by the React team about the manifest API surface while acknowledging the current Vite architecture's limitations, or
  2. Postpone react-server-dom-vite until it can be fully designed for a Rolldown-based Vite architecture. For the time being, we can still use react-server-dom-webpack with a minor workaround, or alternatives can be published as a fork package like suggested in React server dom vite #31768 (comment).

Either way, I think this is a great opportunity to start collaborating with the React team, so I would be happy to answer any questions from both the React team and the Vite framework side and help the process.

Back to my opinion, I think the first option is already beneficial for the Vite ecosystem today (and it can also be beneficial for the future Rolldown-based Vite). As one idea to reduce and clarify the API surface and to have easier collaboration with the React team, what I propose is setRequireModule API (name to be decided), which allows customizing async reference loading. As I linked above, the framework usage is found in the diff of fixtures/flight-vite-mini hi-ogawa@653cd19. I'm interested to know whether this reduced API would work for existing Vite RSC frameworks' use cases.

@jacob-ebey
Copy link
Author

I am entrusting @hi-ogawa and the community with the Vite implementation. Closing this and expect a followup from the Vite team.

@jacob-ebey jacob-ebey closed this Apr 23, 2025
@rickhanlonii
Copy link
Member

Hey @hi-ogawa thanks for jumping in, we're really excited for this collaboration both for the React ecosystem using Vite, and hopefully also for the Vite ecosystem not using React (if directives become more of a standard than a React specific thing).

Option 1 seems great, the setRequireModule approach seems similar to the other bundler integrations. The only thing I would add is that you'll probably find that the reference chunks array is actually needed. There's some context in the parcel integration thread here and the support landed here.

For the CSS, although it is more of a framework concern, it would be good to show how that's intended to work in the fixture, so CSS can load in parallel to rendering. Some more context in the thread here. Same for async module loading (context, and context).

Also linking the prior attempts for Vite, because they have a lot of context too:

Do you want to open a PR?

@hi-ogawa
Copy link
Contributor

hi-ogawa commented May 5, 2025

@rickhanlonii Thanks for the pointers. Yes, I'm planning to create a new PR with past PR's reviews in mind. I'm still iterating a bit how to split the concern in react-server-dom-vite, my helper package, and framework, but user-facing rsc feature is mostly fixed, so I can present that sooner in a draft PR if you don't mind.

The only thing I would add is that you'll probably find that the reference chunks array is actually needed. There's some context in the parcel integration thread here and the support landed here.

I agree that automatically ssr-ing preload link is desired, but right now I have a slightly different idea for that without going through current "prepare destination" flow (but still somewhat baked in react-server-dom-vite). I'll try to show this first, but It's possible that this doesn't actually work out, so if that turns out to be the case, I'll change that back.

For the CSS, although it is more of a framework concern, it would be good to show how that's intended to work in the fixture, so CSS can load in parallel to rendering.

For now I'm not taking css into account and it's left for the framework (and myself) to explore. For fixture, I'll add simple single css entry point scenario first, then if desired, I can extend that to cover a basic css import collection.

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.

10 participants