Skip to content

Commit 1a9b1a4

Browse files
committed
ssr
1 parent f703ccc commit 1a9b1a4

File tree

4 files changed

+146
-2
lines changed

4 files changed

+146
-2
lines changed

lessons/02-react-render-modes/B-static-site-generation.md

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,20 @@ In your package.json, add `"type": "module"` as a top level item. Also add `"bui
1919

2020
> 💡 We're going to do this with vanilla JS and not JSX so we don't need to bring in Babel/Vite/Webpack/whatever. You could absolutely bring in those if you wanted to, I'm just trying to keep these examples as simple as possible.
2121
22+
Create index.html, put this in there:
23+
24+
```javascript
25+
<!DOCTYPE html>
26+
<html lang="en">
27+
<head>
28+
<title>SSG Example</title>
29+
</head>
30+
<body>
31+
<div id="root"><!--ROOT--></div>
32+
</body>
33+
</html>
34+
```
35+
2236
Create an App.js file, put in there
2337

2438
```javascript
@@ -39,7 +53,7 @@ export default App;
3953
Now create a build.js and put
4054

4155
```javascript
42-
import { renderToString } from "react-dom/server";
56+
import { renderToStaticMarkup } from "react-dom/server";
4357
import { createElement as h } from "react";
4458
import {
4559
readFileSync,
@@ -59,7 +73,7 @@ const distPath = path.join(__dirname, "dist");
5973

6074
const shell = readFileSync(path.join(__dirname, "index.html"), "utf8");
6175

62-
const app = renderToString(h(App));
76+
const app = renderToStaticMarkup(h(App));
6377
const html = shell.replace("<!--ROOT-->", app);
6478

6579
// Create dist folder if it doesn't exist
@@ -81,6 +95,7 @@ writeFileSync(path.join(distPath, "index.html"), html);
8195
- You actually _could_ just have React render everything, `<html>`, `<body>` and all.
8296
- We didn't include any CSS nor JS, but obviously you could.
8397
- For mild amounts of interactivity, you could include JS file with the React run time and hydrate the app, but we don't need to.
98+
- We're using renderToStaticMarkup - this very similar to the renderToString function but doesn't include any hints for React to hydrate later for SSR (server-side rendering, our next lesson). renderToString would work, it'll just include superfluous React stuff we don't need.
8499
85100
This generates one page. You could have it generate many pages any number of ways. We could write a `routes.js` file that defines a route and its route component and then have build.js loop over that. We could use `react-router` or something like and then make our build.js use those routes to generate route. Any of these are viable.
86101
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
> 💡 The previous version of this course does a pretty in-depth dive on migrating a client-side React app to being server-side render. [Check it out here][v5]. Nothing has changed so if you want more SSR magic ✨ this still 100% applies.
2+
3+
When you have a client-side React request, the general flow looks like this:
4+
5+
![User requests app, user waits for HTML and JS bundle to be requested, once downloaded they can finally see the app](/images/request1.png)
6+
7+
Generally speaking, this is acceptable for many apps. A lot of apps you interact with on a daily basis work just fine like this. However, you may run into situations where it may behoove you to alter the performance profile of your app a bit.
8+
9+
![User requests app, server pre-renders app, users sees complete HTML while the React app bootstraps itself](/images/request2.png)
10+
11+
Notice the time to interactive and time to first meaningful paint are different now. In the previous version they were the same moment, we have now separated them.
12+
13+
You may be looking at this thinking "this is better!" and in many cases it is: people see something quickly and before they can decide to take an action, generally the app will be bootstrapped. It will _feel_ faster to the user despite the time to interactive will nearly always be tens of milliseconds later (as your app has to render on the server and that takes time). However be careful with this assumption as it in some cases it _isn't_ faster.
14+
15+
- SSR carries with it complexity - some code can be executed in the browser and can't in Node.js (e.g. Google Analytics! it relies on browser APIs). You now need to cleanly separate what runs in browser and what doesn't.
16+
- On fast devices with fast connections, it will tend to actually be a bit slower to get both first paint and first interactive. If you're writing an app for iPhone 16 users in San Francisco, you really don't need SSR. If you're writing it for rural farmers in Montana or the Indian country-side, maybe SSR could help!
17+
- The key here is _measure_. SSR can be a great tool in your toolbox but make sure it's actually making a positive difference to your users.
18+
19+
## SSR by hand
20+
21+
Okay, so let's write a very simple SSR app.
22+
23+
In a new folder, run
24+
25+
```bash
26+
npx init -y
27+
npm i fastify @fastify/static react react-dom vite
28+
```
29+
30+
Make sure your package.json has `"type": "module"` in it. Add this to the scripts:
31+
32+
```json
33+
"scripts": {
34+
"build": "vite build",
35+
"start": "node ./server.js"
36+
},
37+
```
38+
39+
In the project root, create an index.html
40+
41+
```html
42+
<!DOCTYPE html>
43+
<html lang="en">
44+
<head>
45+
<title>SSR Example</title>
46+
<script async defer type="module" src="./Client.js"></script>
47+
</head>
48+
<body>
49+
<div id="root"><!--ROOT--></div>
50+
</body>
51+
</html>
52+
```
53+
54+
> 💡 We're doing vanilla JS again, but it's easier now to add JSX if you want to. Vite has `--ssr` flag you can add to compile an app for use w/ React doing SSR. We're not covering it today but feel free to try later.
55+
56+
Create an App.js, put in there:
57+
58+
```javascript
59+
import { createElement as h, useState } from "react";
60+
61+
function App() {
62+
const [count, setCount] = useState(0);
63+
return h(
64+
"div",
65+
null,
66+
h("h1", null, "Hello Frontend Masters"),
67+
h("p", null, "This is SSR"),
68+
h("button", { onClick: () => setCount(count + 1) }, `Count: ${count}`)
69+
);
70+
}
71+
72+
export default App;
73+
```
74+
75+
Now create a Client.js file
76+
77+
```javascript
78+
import { hydrateRoot } from "react-dom/client";
79+
import { createElement as h } from "react";
80+
import App from "./App.js";
81+
82+
hydrateRoot(document.getElementById("root"), h(App));
83+
```
84+
85+
This is code that will _only_ execute in the browser. If you have Google Analytics or local storage or anything like that, you'd do those sorts of things that _need_ to happen in the browser but don't need to be run in Node.js. Specifically, hydrateRoot will only run on the browser and can't run on the server.
86+
87+
Now for the spicy bit, let's do server-side rendering
88+
89+
```javascript
90+
import fastify from "fastify";
91+
import fastifyStatic from "@fastify/static";
92+
import { readFileSync } from "node:fs";
93+
import { fileURLToPath } from "node:url";
94+
import path, { dirname } from "node:path";
95+
import { renderToString } from "react-dom/server";
96+
import { createElement as h } from "react";
97+
import App from "./App.js";
98+
99+
const __filename = fileURLToPath(import.meta.url);
100+
const __dirname = dirname(__filename);
101+
102+
const shell = readFileSync(path.join(__dirname, "dist", "index.html"), "utf8");
103+
104+
const app = fastify();
105+
106+
app.register(fastifyStatic, {
107+
root: path.join(__dirname, "dist"),
108+
prefix: "/",
109+
});
110+
111+
const parts = shell.split("<!--ROOT-->");
112+
app.get("/", (req, reply) => {
113+
reply.raw.write(parts[0]);
114+
const reactApp = renderToString(h(App));
115+
reply.raw.write(reactApp);
116+
reply.raw.write(parts[1]);
117+
reply.raw.end();
118+
});
119+
120+
app.listen({
121+
port: 3000,
122+
});
123+
```
124+
125+
> If you're getting hydration errors, you may have a whitespace problem (I did when writing this.) React is _super_ sensitive to anything being different between client and server. I had to make sure that `<div><!--ROOT--></div>` had no newlines in it.
126+
127+
The interesting part is the "/" get handler. We immediately write the head to the user. This allows the browser to see the script tag and immediately start downloading the React app. We then render the app and send it to the user. This means by the time the app is rendered and sent to the user, it'll be pretty close to the time the user finishes downloading the script and should get a faster time to first meaningful pain and a decent time to interactive. We finish it off by sending the rest of the closing tags to the user.
128+
129+
[v5]: https://frontendmasters.com/courses/intermediate-react-v5/server-side-rendering/

public/images/request1.png

195 KB
Loading

public/images/request2.png

235 KB
Loading

0 commit comments

Comments
 (0)