Building React Server Components from Scratch: A Step‑by‑Step Guide
This article walks through implementing React Server Components (RSC) from the ground up, covering basic file‑serving with PHP, migrating to Node.js with JSX, adding component, routing, async support, and client‑side state persistence, while contrasting RSC with traditional MVC approaches.
Editor’s note: The author, a former Ant Group frontend engineer, follows Dan Abramov’s approach to implement React Server Components from scratch, including SSR, with full code available on CodeSandbox.
RSC is just a PHP wrapper?
React Server Component (RSC) is a new feature introduced in React 18 that extends the React runtime to the server, preserving composability with client components and adhering to React’s philosophy. Many community leaders believe it will trigger the next paradigm shift.
However, the community is divided. RSC is not easy to use out of the box; the recommended practice is to integrate it with a meta‑framework. As of August 2023, production‑ready RSC is only available on Vercel’s Next.js, raising concerns that the full experience is tied to Vercel. The learning curve also depends on developers’ backgrounds, with some viewing RSC as an added mental burden.
Because many misunderstand RSC, it is sometimes dismissed as merely a PHP wrapper.
Note: The GitHub discussion only implements the most basic RSC functionality and does not include Suspense or nesting with client components; the purpose is to understand the design ideas.
To demonstrate that RSC is not a simple PHP wrapper, the article first implements the same functionality with plain PHP.
Requirement: read a file from the local disk and display its contents on a page.
├── index.php
└── posts
└── hello-world.txtindex.php:
<?php
$author = "Jae Doe";
$post_content = @file_get_contents("./posts/hello-world.txt");
?>
<html>
<head>
<title>My blog</title>
</head>
<body>
<nav>
<a href="/">Home</a>
<hr>
</nav>
<article>
<?php echo htmlspecialchars($post_content); ?>
</article>
<footer>
<hr>
<p><i>(c) <?php echo htmlspecialchars($author); ?>, <?php echo date("Y"); ?>)</i></p>
</footer>
</body>
</html>hello-world.txt:
Hi everyone! This is my first blog post. I <3 React.If you don’t have PHP installed, you can run the code instantly with Docker:
docker run -it --rm -p 80:80 --name my-apache-php-app -v "$PWD":/var/www/html php:7.2-apacheVisiting http://localhost on port 80 shows the rendered page.
Step 0 – Implementation based on string templates
Using a string template, a minimal Node.js HTTP server reads the file, injects it into an HTML string, and serves it directly. The full code is linked in the article.
Step 1 – Render HTML with JSX
Instead of string templates, JSX can generate HTML. Node.js can run JSX thanks to a custom loader defined in package.json:
"scripts": {
"start": "nodemon -- --experimental-loader ./node-jsx-loader.js ./server.js"
},JSX is compiled to a plain JavaScript object (a React element):
{
$$typeof: Symbol.for("react.element"),
type: 'html',
props: {
children: [
{
$$typeof: Symbol.for("react.element"),
type: 'head',
props: { children: { $$typeof: Symbol.for("react.element"), type: 'title', props: { children: 'My blog' } } }
},
// ...
]
}
}The rendering function renderJSXToHTML recursively walks this structure and produces HTML.
Step 2 – Add component capability
Components are simply functions that return JSX. By extending renderJSXToHTML to handle typeof jsx.type === "function", component rendering is supported:
else if (typeof jsx.type === "function") {
const Component = jsx.type;
const props = jsx.props;
const returnedJsx = Component(props);
return renderJSXToHTML(returnedJsx);
}Step 3 – Add routing
A PHP‑style routing switch is implemented, mapping URLs to view files:
switch ($request) {
case '':
case '/':
require __DIR__ . $viewDir . 'home.php';
break;
case '/views/users':
require __DIR__ . $viewDir . 'users.php';
break;
case '/contact':
require __DIR__ . $viewDir . 'contact.php';
break;
default:
http_response_code(404);
require __DIR__ . $viewDir . '404.php';
}Step 4 – Async components
Async components are supported by awaiting the component call and making renderJSXToHTML async. Example async component:
async function Post({ slug }) {
let content;
try {
content = await readFile("./posts/" + slug + ".txt", "utf8");
} catch (err) {
throwNotFound(err);
}
return (
<section>
<h2><a href={"/" + slug}>{slug}</a></h2>
<article>{content}</article>
</section>
);
}Step 5 – Client‑side state persistence
Traditional MVC loses client state on navigation. By sending JSX instead of static HTML and handling navigation on the client, the input value can be preserved. An example GIF shows the lost state before the fix.
Step 5.1 – Move routing logic to the client
A client.js script intercepts link clicks, prevents full page reloads, and fetches new HTML via fetch to inject into the DOM.
Step 5.2 – Transfer JSX instead of HTML
The server distinguishes between initial HTML and JSX requests using a ?jsx query parameter, sending serialized JSX when needed.
else if (url.searchParams.has("jsx")) {
url.searchParams.delete("jsx");
await sendJSX(res, <Router url={url} />);
} else {
await sendHTML(res, <Router url={url} />);
}Step 5.3 – Render JSX on the client
Using react-dom/client, the received JSX is deserialized (with proper handling of the $$typeof symbol) and rendered, preserving input state across navigations.
Step 5.3.1 – Deserialize JSX
The deserialization step converts the JSON‑stringified React element back into a real React element before rendering.
Step 5.3.2 – Hydration of the first render
Initial server‑rendered HTML is hydrated on the client so that subsequent client‑side updates work correctly, similar to classic React + Redux SSR setups.
Step 6 – Code cleanup and optimization
Reuse recursive JSX parsing for both sendHTML and sendJSX.
Replace custom renderJSXToHTML with React’s official renderToString.
Split services into rsc.js (produces JSX) and ssr.js (serves initial HTML or forwards JSX), allowing independent scaling.
Full optimized code is linked in the article.
In summary, the author demonstrates a complete, from‑scratch implementation of React Server Components, covering basic file serving, JSX rendering, component composition, routing, async support, client‑side state persistence, and production‑grade optimizations.
Alipay Experience Technology
Exploring ultimate user experience and best engineering practices
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
