DEV Community

Cover image for Migrating a Vite + React app to use React Server Components
Viktor Lázár
Viktor Lázár

Posted on

Migrating a Vite + React app to use React Server Components

Vite has the best developer experience a web developer could wish for. When you want to create a single page application using React then Vite is your default choice. It's incomparable how much better it is than using create-react-app just a few years ago. Who really misses Webpack? Who misses complex configurations?

Getting started

Starting a new Vite + React app is easy:

pnpm create vite my-react-app --template react
Enter fullscreen mode Exit fullscreen mode

Then you just need to change directory, install dependencies and start your app:

cd my-react-app
pnpm install
pnpm dev
Enter fullscreen mode Exit fullscreen mode

It just works!

Image description

You can achieve great things working only on the client side, what Kent C. Dodds is presenting in this great video is awesome and perfectly viable in a lot of use cases:

But is it really enough? Probably it is most of the time, but everyone wants server-side rendering nowadays, the hype is real and we want it too, so let's continue...

React Server Components

The hype around RSCs are still rising! More and more frameworks are starting to support the new server-side rendering features of the upcoming new major release of React.

Surely Next.js was the first to jump on the train (they built it for themselves) and Vercel started to teach developers around the world about the benefits of using RSCs, including how to work with client components and server actions too.

Other major React based frameworks are picking up RSCs a bit slower though. Remix just demonstrated using RSCs in the loader, while not missing working client components:

Astro is more likely a bit off here as while it supports React, it's implementing it's own similar solutions, like Astro Actions:

RedwoodJS also works on RSC support and Waku supports the new features already.

You can also start building your own framework from scratch or using vinxi. But how many developers are up to do this? It's not even really documented how to do it! What about web developers who are not experienced enough or don't have the time to go down this road? Surely there are some, like Hiroshi implementing an RSC supprting framework using both the current Vite API at https://github.com/hi-ogawa/vite-plugins/tree/main/packages/react-server and using the incoming Vite 6 Environment API at https://github.com/hi-ogawa/vite-environment-examples/tree/main/examples/react-server. Also there's a great article from Daniel Nagy about building your own framework using Vite and RSC support at https://danielnagy.me/posts/Post_usaivhdu3j5d.

But we are drifting too far away from the original simplicity and easy start we had. How to enhance your simple Vite + React single page application with modern server-side rendering?

A bit controversial tutorial you can find is from Vercel, presented by Lee Robinson:

But surely a lot of us were very disappointed when the starting Vite client-side app became a heavy Next.js app, removing all traces of Vite in the end. But we adore Vite here right? We love how simple it is, how fast it is, how easy it is to extend it using plugins, especially compared to custom Webpack plugins.

Almost all other frameworks are using Vite now, especially non-React frameworks, like SvelteKit. When you stick with React, Remix is closest to what we wish for and it will be even more when there will be proper RSC support in Remix. You can already use Remix with Vite and it's great! But it's not supporting RSCs yet and you will get some magic files which will be mostly just deleted as it's not likely you want to modify these. Maybe in a couple months, but expect refactoring!

Is there another choice? A new alternative? Which framework to choose when you want to modify the starting Vite + React app the least and be able to use everything new from React 19?

Vite Forever

So let's correct what Lee did and don't go into the wrong direction by migrating over to Next.js and Webpack, let's stick with Vite at least for a few years, maybe even a decade (or more)!

As we want to use RSCs, let's move everything from the index.html into src/main.jsx and instead of rendering App on the client-side, just use it as a component in this RSC layout:

import App from "./App.jsx";
import "./index.css";

export default function Html() {
  return (
    <html lang="en">
      <head>
        <meta charSet="UTF-8" />
        <link rel="icon" type="image/svg+xml" href="/vite.svg" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>Vite + React</title>
      </head>
      <body>
        <div id="root">
          <App />
        </div>
      </body>
    </html>
  );
}
Enter fullscreen mode Exit fullscreen mode

Beware that you need to fix charSet as it has a different casing when used in JSX than in HTML!

For now, just add a "use client"; directive to App.jsx to make it work instantly as the App component includes a useState React hook.

Now let's uninstall react and react-dom as we will not need them, both were just React 18. Now what? How we will run our app?

pnpm add @lazarv/react-server@latest 
Enter fullscreen mode Exit fullscreen mode

Yes, I'm installing my own React meta-framework to solve the problem I have here. Is it a better choice than Remix? If you want to use the latest React features, then yes. As it supports RSCs, client components and server actions already, while using Vite, ES modules and keeping everything stupid simple. It's probably best for quick prototyping and for indie devs loving to work with bleeding edge experimental tech. The fact that it was already good enough to implement its own documentation site, using its static-site generation capability, shows great potential. With time, it will become even better and more mature, offering an even wider feature set to work with. You can read more about the framework at https://react-server.dev. Get back to our app now!

Just make some more changes on how to start our app:

"scripts": {
  "dev": "react-server ./src/main.jsx",
  "build": "react-server build ./src/main.jsx",
  "lint": "eslint .",
  "start": "react-server start"
}
Enter fullscreen mode Exit fullscreen mode

If you think this is very familiar to how you would use Node.js to run a script, you're right! react-server ./src/main.tsx is the same as executing node server.js, just for React RSCs. If it would be enough, we could just run a single RSC component using npx with this framework, even without installing anything! Just like with Node.js!

Let's start our app using pnpm dev --open!

Image description

It just works! Awesome! But can we fix the loading glitch that we can see on refreshing the page? What causes this?

Optimizing client components

In the current state of our app, the whole App component is a client component. This is not ideal, as only the counter part is interactive in it. We also attached some CSS to this client component which will only be loaded when the client component loads in the browser at runtime. This is causing a glitch for a blink of an eye at page load.

Let's create a new Counter component in a new file at src/Counter.jsx and move the counter button into this new component:

"use client";

import { useState } from "react";

function Counter() {
  const [count, setCount] = useState(0);

  return (
    <button onClick={() => setCount((count) => count + 1)}>
      count is {count}
    </button>
  );
}

export default Counter;
Enter fullscreen mode Exit fullscreen mode

Now let's get back to the App component, remove the "use client"; directive, remove the App.css import and the code which was only part of the Counter component, but import it instead:

import reactLogo from "./assets/react.svg";
import viteLogo from "/vite.svg";
import Counter from "./Counter.jsx";

function App() {
  return (
    <>
      <div>
        <a href="https://vitejs.dev" target="_blank">
          <img src={viteLogo} className="logo" alt="Vite logo" />
        </a>
        <a href="https://react.dev" target="_blank">
          <img src={reactLogo} className="logo react" alt="React logo" />
        </a>
      </div>
      <h1>Vite + React</h1>
      <div className="card">
        <Counter />
        <p>
          Edit <code>src/App.jsx</code> and save to test HMR
        </p>
      </div>
      <p className="read-the-docs">
        Click on the Vite and React logos to learn more
      </p>
    </>
  );
}

export default App;
Enter fullscreen mode Exit fullscreen mode

The easiest resolution of the CSS issue is to import App.css in main.jsx:

import App from "./App.jsx";
import "./index.css";
import "./App.css";

export default function Html() {
  // we don't need to do any change here
}
Enter fullscreen mode Exit fullscreen mode

Now both CSS files will be loaded instantly on page load and there will be no flickering. The counter button will still work as before as we refactored it to be a new client component. Every other part of the App component was static, which is best to be included in RSCs. You can also combine the 2 CSS files as there's no reason why we have two, just fix the CSS imports in the main.jsx after that.

Where to go?

Now we have the same Vite + React app working with server-side rendering. But how to evolve from this state further?

You can start implementing a form using server actions, just as described at https://react.dev/reference/rsc/server-actions.

You can add a router, maybe a file-system based routing solution supporting RSCs, like @lazarv/react-server-router. It's very flexible!

You can export your app as static as there might be no reason to use dynamic server-side rendering and it's enough for you to render your app at build time.

You can add any Vite plugin and remain in this wonderful ecosystem, extending your app at any time.

You can eagerly wait for a version of Vite using Rolldown, to have an even more blazing-fast developer experience!

Conclusion

While RSCs are with us for quite some time now, we are still learning how to use them properly. It's a super-power to have and it will certainly evolve further. But it's more like a beast to tame than an easy to understand feature. We are holding endless possibilities in our hands, but most of us don't know what to do with it. With great power comes great responsibility! But even when we tend to over-engineer even extremely simple problems, we like to work with simple technologies. That's how Vite won and how PHP is still with us. It's easy to use. A lot of us are not god-like developers implementing anything from scratch in a day. We are just mere humans trying hard to make some silly things work. And we fail a lot. But we win in the end with persistence, like long-distance runners.

We should stick to what we already learned in the past. We should not forget, that we already knew how to make things efficiently. We had worse tools, worse tech, worse machines, but it was possible because of creativity and endurance to create wonderful things. Now we should use our new tools, new tech, new machines and we should still use our creativity and imagination to create even more fascinating experiences. We are responsible for making people happy to use our products. Even as software engineers.

Top comments (0)