Skip to content

Commit 46fd105

Browse files
committed
no framework
1 parent 65c4874 commit 46fd105

File tree

3 files changed

+433
-0
lines changed

3 files changed

+433
-0
lines changed
Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
There's a lot to getting this set up by hand, but bear with me, and I promsie at the end of this you're going to understand RSCs at a depth that allows you to make trade-offs of when to use them and when to avoid them.
2+
3+
So, first, we're going to use Webpack and Babel directly (despite normally I'd suggest Vite.) Why? Because this allows us to use the React team's code directly without a layer indirection between Vite and Webpack. In general I suggest Vite for React devs.
4+
5+
So let's get our project started. In a new directory run
6+
7+
```bash
8+
npm init -y
9+
10+
```
11+
12+
You can either run that, or just copy this package.json into your project and run `npm i`.
13+
14+
```json
15+
{
16+
"name": "no-framework",
17+
"version": "1.0.0",
18+
"main": "index.js",
19+
"scripts": {},
20+
"keywords": [],
21+
"author": "Brian Holt",
22+
"license": "Apache-2.0",
23+
"description": "React Server Components without a framework!",
24+
"dependencies": {
25+
"@babel/core": "^7.26.8",
26+
"@babel/plugin-transform-modules-commonjs": "^7.26.3",
27+
"@babel/preset-react": "^7.26.3",
28+
"@babel/register": "^7.25.9",
29+
"@fastify/static": "^8.1.0",
30+
"babel-loader": "^9.2.1",
31+
"css-loader": "^7.1.2",
32+
"doodle.css": "^0.0.2",
33+
"fastify": "^5.2.1",
34+
"html-webpack-plugin": "^5.6.3",
35+
"nodemon": "^3.1.9",
36+
"pino-pretty": "^13.0.0",
37+
"promised-sqlite3": "^2.1.0",
38+
"react": "^19.0.0",
39+
"react-dom": "^19.0.0",
40+
"react-server-dom-webpack": "^19.0.0",
41+
"sqlite3": "^5.1.7",
42+
"style-loader": "^4.0.0",
43+
"webpack": "^5.97.1",
44+
"webpack-cli": "^6.0.1"
45+
}
46+
}
47+
```
48+
49+
Either works! We need a lot of machinery to get this to work but the high level is we're going to be building a Fastify server that is going to be serving RSCs via the React Flight format.
50+
51+
Let's set up Webpack. Create a webpack.config.js
52+
53+
```javascript
54+
const path = require("node:path");
55+
const HtmlWebpackPlugin = require("html-webpack-plugin");
56+
const ReactServerWebpackPlugin = require("react-server-dom-webpack/plugin");
57+
58+
const mode = process.env.NODE_ENV || "development";
59+
const development = mode === "development";
60+
61+
const config = {
62+
mode,
63+
entry: "./src/Client.jsx",
64+
module: {
65+
rules: [
66+
{
67+
test: /\.jsx?$/,
68+
use: "babel-loader",
69+
exclude: /node_modules/,
70+
},
71+
{
72+
test: /\.css$/i,
73+
use: ["style-loader", "css-loader"],
74+
},
75+
],
76+
},
77+
resolve: {
78+
extensions: [".js", ".jsx"],
79+
},
80+
plugins: [
81+
new HtmlWebpackPlugin({
82+
inject: true,
83+
publicPath: "/assets/",
84+
template: "./index.html",
85+
}),
86+
new ReactServerWebpackPlugin({ isServer: false }),
87+
],
88+
output: {
89+
chunkFilename: development
90+
? "[id].chunk.js"
91+
: "[id].[contenthash].chunk.js",
92+
path: path.resolve(__dirname, "dist"),
93+
filename: "[name].js",
94+
clean: true,
95+
},
96+
optimization: {
97+
runtimeChunk: "single",
98+
},
99+
};
100+
101+
module.exports = config;
102+
```
103+
104+
This isn't a Webpack class so let's not dive too deep here - we're just making a Webpack config that's going to compile our React from JSX to usable JS code, use style-loader to load CSS, use the HTML plugin to generate a valid HTML for us, and make sure it's all living in one bundle so our React Flight protocol can find client side components in the bundle.
105+
106+
Let's set up the Babel config. Make a babel.config.js file.
107+
108+
```javascript
109+
const development = (process.env.NODE_ENV || "development") === "development";
110+
111+
module.exports = {
112+
presets: [
113+
[
114+
"@babel/preset-react",
115+
{
116+
runtime: "automatic",
117+
useSpread: true,
118+
development: true,
119+
},
120+
],
121+
],
122+
};
123+
```
124+
125+
Now everything will work with Babel.
126+
127+
Let's make an index.html
128+
129+
```html
130+
<!DOCTYPE html>
131+
<html lang="en">
132+
<head>
133+
<meta charset="utf-8" />
134+
<meta name="viewport" content="width=device-width, initial-scale=1" />
135+
<title>React Server Components without a Framework!</title>
136+
<link rel="stylesheet" href="/index.css" />
137+
</head>
138+
<body class="doodle">
139+
<div id="root"><!--ROOT--></div>
140+
</body>
141+
</html>
142+
```
143+
144+
Looks quite similar to our previous ones. Let's make a /public/index.css. [Copy it from here][css]
145+
146+
Copy [this SQLite file][sqlite] to your root directory as well.
147+
148+
Lastly add these scripts to your package.json
149+
150+
```json
151+
"scripts": {
152+
"dev:client": "webpack --watch",
153+
"dev:server": "node --watch --conditions react-server server/main.js"
154+
},
155+
```
156+
157+
Okay, this should give us everything that's needed for our app to function before we actually write the server and the React app. One bit to highlight is `const ReactServerWebpackPlugin = require("react-server-dom-webpack/plugin");` in our Webpack file - this is the magic plugin that will allow Webpack to _not_ render server components and only include client components. Other than that, this is a very standard React + Webpack set up.
158+
159+
Another new thing you might be the `--conditions react-server` part of running our server app. This lets Node.js know how to resolve its modules - we're in a react-server condition so only import those and know not to import client modules. I had never used this feature of Node.js before but it's pretty cool, even if it's a bit niche.
160+
161+
[css]:
162+
[sqlite]:
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
Let's put together the most simple React app so that we can render it using our newly-created framework.
2+
3+
Let's make a directory, `src` and put a file in there a file called App.jsx
4+
5+
```javascript
6+
import { Suspense } from "react";
7+
import ServerComponent from "./ServerComponent";
8+
import ClientComponent from "./ClientComponent";
9+
10+
export default function App() {
11+
console.log("rendering App server component");
12+
return (
13+
<Suspense fallback={<h1>Loading...</h1>}>
14+
<h1>Notes App</h1>
15+
<ServerComponent />
16+
<ClientComponent />
17+
</Suspense>
18+
);
19+
}
20+
```
21+
22+
We're going to putting a lot of console logs in here just so you can see where things are happening and in what order. Great, so we'll have two components: a client and a server component to show you the difference. The client component won't render at all on the server and will be included in the bundle. Likewise the server component will only be rendered in the server and won't be included in the bundle.
23+
24+
So where does the App component render? On the server. By default _everything_ renders on the server. By default you can't use any hooks that have interactivity like useState; you have to declare it a client component to do that.
25+
26+
Okay, so let's make our client component. Make ClientComponent.jsx and put in there
27+
28+
```javascript
29+
"use client";
30+
31+
import { useState } from "react";
32+
33+
export default function ClientComponent() {
34+
console.log("rendering ClientComponent client component");
35+
const [counter, setCounter] = useState(0);
36+
37+
return (
38+
<fieldset>
39+
<legend>Client Component</legend>
40+
<p>Counter: {counter}</p>
41+
<button onClick={() => setCounter(counter + 1)}>Increment</button>
42+
</fieldset>
43+
);
44+
}
45+
```
46+
47+
Nothing you haven't seen before except the weird `"use client";` at the top. This is how you declare a component is to be rendered on the client and not on the server. After that it's just React as you know and love it. One note: `"use server;"` is not necessary, it's assumed.
48+
49+
Okay, let's make `ServerComponent.jsx`
50+
51+
```javascript
52+
import { AsyncDatabase } from "promised-sqlite3";
53+
import path from "node:path";
54+
55+
// this page assumes that you are logged in as user 1
56+
export default async function MyNotes() {
57+
console.log("rendering MyNotes server component");
58+
async function fetchNotes() {
59+
console.log("running server function fetchNotes");
60+
const dbPath = path.resolve(__dirname, "../../notes.db");
61+
const db = await AsyncDatabase.open(dbPath);
62+
const from = await db.all(
63+
"SELECT n.id as id, n.note as note, f.name as from_user, t.name as to_user FROM notes n JOIN users f ON f.id = n.from_user JOIN users t ON t.id = n.to_user WHERE from_user = ?",
64+
["1"]
65+
);
66+
return {
67+
from,
68+
};
69+
}
70+
71+
const notes = await fetchNotes();
72+
73+
return (
74+
<fieldset>
75+
<legend>Server Component</legend>
76+
<div>
77+
<table>
78+
<thead>
79+
<tr>
80+
<th>From</th>
81+
<th>To</th>
82+
<th>Note</th>
83+
</tr>
84+
</thead>
85+
<tbody>
86+
{notes.from.map(({ id, note, from_user, to_user }) => (
87+
<tr key={id}>
88+
<td>{from_user}</td>
89+
<td>{to_user}</td>
90+
<td>{note}</td>
91+
</tr>
92+
))}
93+
</tbody>
94+
</table>
95+
</div>
96+
</fieldset>
97+
);
98+
}
99+
```
100+
101+
- We're doing SQL in React!? Well, yes, but also no. This all happening server-side before it's beening sent down to the browser. So it's basically the same as doing an API request but it's doing the full React lifecycle here on the server. That's one of the nicest aspects of RSC - you get to write all of this as if it was being done server side but instead of having a server portion and a client portion, it's all the same file!
102+
- Notice that it's an `async` function - this is a fun ability that only server components have. Since it's all rendering once and on the server, you can do async functions for react server components.
103+
104+
This is deliberately a pared-down feature set as we're going to only do the bare minimum to implement a by-hand RSC-server implementation. Once we get into Next.js I'll show you more advance React server component features.
105+
106+
Okay, one more file, call it Client.jsx and put this in there.
107+
108+
```javascript
109+
import { createRoot } from "react-dom/client";
110+
import { createFromFetch } from "react-server-dom-webpack/client";
111+
import "doodle.css/doodle.css";
112+
113+
console.log("fetching flight response");
114+
const fetchPromise = fetch("/react-flight");
115+
const root = createRoot(document.getElementById("root"));
116+
const p = createFromFetch(fetchPromise);
117+
console.log("rendering root", p);
118+
root.render(p);
119+
```
120+
121+
`createFromFetch` allows us to turn a fetch request to an API endpoint into a React component directly. This is the magic of React server components and probably one you'll never write by hand again - your framework will always do this for you. But I wanted to demystify what it's doing for you. You make a request to an API endpoint, get a promise, and hand it to React to render. That's it!

0 commit comments

Comments
 (0)