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
Configure Babel
Add this to your package.json
:
"babel": {
"presets": [
[
"@babel/preset-react",
{
"runtime": "automatic"
}
]
]
}
Add scripts to package.json
Add the following scripts to your package.json
:
"dev": "webpack -w",
"start": "node --conditions react-server server/server.js"
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"
}
]
]
}
}
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 })],
};
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)}</>;
}
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}`));
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>
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" />;
}
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);
}
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} />;
}
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 />
</>
);
}
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>
);
}
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>;
}
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)