Cookies Psst! Do you accept cookies?

We use cookies to enhance and personalise your experience.
Please accept our cookies. Checkout our Cookie Policy for more information.

Use React 19 with server components without a framework

TLDR: Visit the Github repository.

Here I will expose a simple running project with react 19 and server components without a framework.

The dependencies

I will show you the package.json for the dependencies needed to be installed for this project:

{
  "babel": {
    "presets": [
      [
        "@babel/preset-react",
        {
          "runtime": "automatic"
        }
      ]
    ]
  },
  "scripts": {
    "build": "node scripts/build.js",
    "start": "node --conditions react-server server/server.js",
    "build-and-start": "npm run build && npm run start"
  },
  "dependencies": {
    "@babel/core": "^7.24.7",
    "@babel/plugin-transform-modules-commonjs": "^7.24.7",
    "@babel/preset-react": "^7.24.7",
    "@babel/register": "^7.24.6",
    "babel-loader": "^9.1.3",
    "express": "^4.19.2",
    "react": "^19.0.0-rc-3da26163a3-20240704",
    "react-dom": "^19.0.0-rc-3da26163a3-20240704",
    "react-server-dom-webpack": "^19.0.0-rc-3da26163a3-20240704",
    "webpack": "^5.92.1"
  }
}

The ones for react, react-dom, and react-server-dom-webpack can be installed with the following command:

npm i react@rc react-dom@rc react-server-dom-webpack@rc

The rest of dependencies are installed as usual (npm i dependency-name).

Folder structure of the project

We will have a public folder with an index.html file in it, a server folder with a server.js file, a scripts folder with a build.js file, and a src folder with a index.js file in it.

Content for public/index.html

<!-- public/index.html -->
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta name="description" content="React with Server Components" />
    <meta name="viewport" content="width=device-width,initial-scale=1" />
    <title>React Server Components</title>
    <script defer src="main.js"></script>
  </head>

  <body>
    <div id="root"></div>
  </body>
</html>

The main.js file referenced in the index.html file will be generated by the build command.

scripts/build.js

const path = require("path");
const webpack = require("webpack");
const ReactServerWebpackPlugin = require("react-server-dom-webpack/plugin");

webpack(
  {
    mode: "development",
    entry: [path.resolve(__dirname, "../src/index.js")],
    output: {
      path: path.resolve(__dirname, "../public"),
      filename: "main.js",
    },
    module: {
      rules: [
        {
          test: /\.js$/,
          use: "babel-loader",
          exclude: /node_modules/,
        },
      ],
    },
    plugins: [new ReactServerWebpackPlugin({ isServer: false })],
  },
  (error, stats) => {
    if (error) {
      console.error(error);
    }
  }
);

src/index.js file

import { use } from "react";
import { createFromFetch } from "react-server-dom-webpack/client";
import { createRoot } from "react-dom/client";

const root = createRoot(document.getElementById("root"));
root.render(<Root />);

const cache = new Map();

function Root() {
  let content = cache.get("home");
  if (!content) {
    content = createFromFetch(fetch("/react"));
    cache.set("home", content);
  }

  return <>{use(content)}</>;
}

server/server.js file

const register = require("react-server-dom-webpack/node-register");
register();
const path = require("path");
const { readFileSync } = require("fs");
const babelRegister = require("@babel/register");

babelRegister({
  ignore: [/[\\\/](build|server|node_modules)[\\\/]/],
  presets: [["@babel/preset-react", { runtime: "automatic" }]],
  plugins: ["@babel/transform-modules-commonjs"],
});

const { renderToPipeableStream } = require("react-server-dom-webpack/server");

const express = require("express");

const React = require("react");
const ReactApp = require("../src/app-s").default;

const app = express();

app.get("/", (req, res) => {
  const html = readFileSync(
    path.resolve(__dirname, "../public/index.html"),
    "utf8"
  );
  res.send(html);
});

app.get("/react", (req, res) => {
  const manifest = readFileSync(
    path.resolve(__dirname, "../public/react-client-manifest.json"),
    "utf8"
  );
  const moduleMap = JSON.parse(manifest);
  const { pipe } = renderToPipeableStream(
    React.createElement(ReactApp),
    moduleMap
  );
  pipe(res);
});

app.use(express.static(path.resolve(__dirname, "../public")));
const port = process.env.PORT || 3000;
app.listen(port, () => console.log(`listening on port ${port}`));

as we can see this last file, the server.js, references (imports/requires) another one not seen yet:

const ReactApp = require("../src/app-s").default;

We will see it in next section.

Other files in src folder: the app-s.js file.

In src folder we will have the files for React components (server and client components). The first one to be seen is the app-s.js, a React server component:

import CounterS from "./counter-s";
import Client1S from "./client1-s";
import App from "./app";
import Wrapper from "./wrapper";

export default function () {
  return (
    <Wrapper>
      <App>
        <CounterS />
        <Client1S />
      </App>
    </Wrapper>
  );
}

The App component used in this server component is a client component. CounterS and Client1S are also server components. Wrapper is a special server component:

import { Suspense } from "react";
import ErrorBoundary from "./error-boundary";

export default function ({ children }) {
  return (
    <ErrorBoundary fallback={<div>Something crashed.</div>}>
      <Suspense fallback={<div>loading...</div>}>{children}</Suspense>
    </ErrorBoundary>
  );
}

Client1S.js

import Client1 from "./client1";
import Wrapper from "./wrapper";

export default function Client1S() {
  const promise = new Promise((res, rej) => {
    setTimeout(() => res("foo"), 5000);
  });

  return (
    <Wrapper>
      <Client1 promise={promise} />
    </Wrapper>
  );
}

As you can see, in this server component we create a promise and pass it to the client component.

Client1.js

"use client";

import { use } from "react";

export default function ({ promise }) {
  return <div>{use(promise)}</div>;
}

Note how we begin the component by "use client" directive. This is important. All client components must have this directive.

What happens in this example

In the example that constitutes this project what happens is that we have a Counter component that renders quickly and becomes interactive from the first moment and then we have another component that takes its time in fetching data from the server and shows a loading state.

References

This example is a result from looking at here.

Last Stories

What's your thoughts?

Please Register or Login to your account to be able to submit your comment.