DEV Community

Cover image for React 19 setup and use of server actions
roggc
roggc

Posted on • Updated on

React 19 setup and use of server actions

TLDR: Github repo

npm init -y

Create a folder and run npm init -y in it.

Install dependencies

Install the dependencies with the following command:

npm i webpack webpack-cli react@rc react-dom@rc react-server-dom-webpack@rc babel-loader @babel/core @babel/register @babel/preset-react @babel/plugin-transform-modules-commonjs express
Enter fullscreen mode Exit fullscreen mode

Configure Babel

Add this to your package.json:

  "babel": {
    "presets": [
      [
        "@babel/preset-react",
        {
          "runtime": "automatic"
        }
      ]
    ]
  }
Enter fullscreen mode Exit fullscreen mode

Add scripts to package.json

Add the following scripts to your package.json:

"dev": "webpack -w",
"start": "node --conditions react-server server/server.js"
Enter fullscreen mode Exit fullscreen mode

This is how your package.json must look

{
  "name": "react-19-essay",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "dev": "webpack -w",
    "start": "node --conditions react-server server/server.js"
  },
  "author": "",
  "license": "ISC",
  "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-f38c22b244-20240704",
    "react-dom": "^19.0.0-rc-f38c22b244-20240704",
    "react-server-dom-webpack": "^19.0.0-rc-f38c22b244-20240704",
    "webpack": "^5.92.1",
    "webpack-cli": "^5.1.4"
  },
  "babel": {
    "presets": [
      [
        "@babel/preset-react",
        {
          "runtime": "automatic"
        }
      ]
    ]
  }
}
Enter fullscreen mode Exit fullscreen mode

Create the webpack.config.js file

At the root of the project, create a webpack.config.js file with the following content:

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

module.exports = {
  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 })],
};
Enter fullscreen mode Exit fullscreen mode

From the look to this configuration file, we know we need a src/index.js file.

Create a src/index.js file

Create a src folder and put in it the following file (index.js):

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)}</>;
}
Enter fullscreen mode Exit fullscreen mode

You see how we use createFromFetch from react-server-dom-webpack/client.

You also see how we fetch to /react endpoint.

Create a server/server.js file

Create a server folder and put in it the following file (server.js):

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").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}`));
Enter fullscreen mode Exit fullscreen mode

You see how we use renderToPipeableStream from react-server-dom-webpack/server. You also see how we require a src/app.js file. You also see how we use the public folder for static files.

Create a public/index.html file

Create a public file and put in it the following file (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>fun!</title>
    <script defer src="main.js"></script>
  </head>

  <body>
    <div id="root"></div>
  </body>
</html>
Enter fullscreen mode Exit fullscreen mode

The src folder

Until here the setup. Now we will focus on creating code for our app. First we need to create the app.js file, the entry point of our app (a React component).

"use client";

import Comp1 from "./non-bloking-server-actions-components/comp1/callable";

export default function () {
  return <Comp1 name="Albert" />;
}
Enter fullscreen mode Exit fullscreen mode

Comp1 is in this case a client component that calls a none bloking server action:

"use client";

import action from "./action";

export default function (props) {
  return action(props);
}
Enter fullscreen mode Exit fullscreen mode

action is the none bloking server action:

"use server";

import Output from "./output";

export default function ({ name }) {
  const messagePromise = new Promise((res) =>
    setTimeout(() => res("hello x " + name), 2000)
  );

  return <Output greetingPromise={messagePromise} />;
}
Enter fullscreen mode Exit fullscreen mode

Output is a React Client Component:

"use client";

import Counter from "../../pure-client-components/counter";
import Wrapper from "../../wrapper";

export default function ({ greetingPromise }) {
  return (
    <>
      <div>
        <Wrapper>{greetingPromise}</Wrapper>
      </div>
      <Counter />
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode

Wrapper is this React Client Component:

"use client";

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

export default function Wrapper({ children }) {
  return (
    <ErrorBoundary fallback={<div>Something crashed.</div>}>
      <Suspense fallback={<div>Loading...</div>}>{children}</Suspense>
    </ErrorBoundary>
  );
}
Enter fullscreen mode Exit fullscreen mode

and Counter is a regular React Client Component:

"use client";

import { useState } from "react";

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

  const increment = () => {
    setCount(count + 1);
  };

  return <button onClick={increment}>{count}</button>;
}
Enter fullscreen mode Exit fullscreen mode

Conclusion

You see how with the use of non blocking server actions that return client components, server components are not necessary. What we see in this example is counter available and interactive from the first moment, and a loading indicator for a greeting message.

References

The original setup for a working implementation of React Server Components without a framework was taken from here.

Top comments (0)