+3
public/the-two-reacts/components.js
+3
public/the-two-reacts/components.js
+15
public/the-two-reacts/counter.js
+15
public/the-two-reacts/counter.js
···
1
+
"use client";
2
+
3
+
import { useState } from "react";
4
+
5
+
export function Counter() {
6
+
const [count, setCount] = useState(0);
7
+
return (
8
+
<button
9
+
className="dark:color-white rounded-lg bg-purple-700 px-2 py-1 font-sans font-semibold text-white focus:ring active:bg-purple-600"
10
+
onClick={() => setCount(count + 1)}
11
+
>
12
+
You clicked me {count} times
13
+
</button>
14
+
);
15
+
}
+196
public/the-two-reacts/index.md
+196
public/the-two-reacts/index.md
···
1
+
---
2
+
title: "The Two Reacts"
3
+
date: '2024-01-04'
4
+
spoiler: "UI = f(data)(state)"
5
+
---
6
+
7
+
Suppose I want to display something on your screen. Whether I want to display a web page like this blog post, an interactive web app, or even a native app that you might download from some app store, at least *two* devices must be involved.
8
+
9
+
Your device and mine.
10
+
11
+
It starts with some code and data on *my* device. For example, I am editing this blog post as a file on my laptop. If you see it on your screen, it must have already traveled from my device to yours. At some point, somewhere, my code and data turned into the HTML and JavaScript instructing *your* device to display this.
12
+
13
+
So how does that relate to React? React is a UI programming paradigm that lets me break down *what* to display (a blog post, a signup form, or even a whole app) into independent pieces called *components*, and compose them like LEGO blocks. I'll assume you already know and like components; check [react.dev](https://react.dev) for an intro.
14
+
15
+
Components are code, and that code has to run somewhere. But wait--*whose* computer should they run on? Should they run on your computer? Or on mine?
16
+
17
+
Let's make a case for each side.
18
+
19
+
---
20
+
21
+
First, I'll argue that components should run on *your* computer.
22
+
23
+
Here's a little counter button to demonstrate interactivity. Click it a few times!
24
+
25
+
```js
26
+
<Counter />
27
+
```
28
+
29
+
```js eval
30
+
<p>
31
+
<Counter />
32
+
</p>
33
+
```
34
+
35
+
Assuming the JavaScript code for this component has already loaded, the number will increase. Notice that it increases *instantly on press*. There is no delay. No need to wait for the server. No need to download any additional data.
36
+
37
+
This is possible because this component's code is running on *your* computer:
38
+
39
+
```js
40
+
import { useState } from "react";
41
+
42
+
export function Counter() {
43
+
const [count, setCount] = useState(0);
44
+
return (
45
+
<button
46
+
className="dark:color-white rounded-lg bg-purple-700 px-2 py-1 font-sans font-semibold text-white focus:ring active:bg-purple-600"
47
+
onClick={() => setCount(count + 1)}
48
+
>
49
+
You clicked me {count} times
50
+
</button>
51
+
);
52
+
}
53
+
```
54
+
55
+
Here, `count` is a piece of *client state*--a bit of information in your computer's memory that updates every time you press that button. **I don't know how many times you're going to press the button** so I can't predict and prepare all of its possible outputs on *my* computer. The most I'll dare to prepare on my computer is the *initial* rendering output ("You clicked me 0 times") and send it as HTML. But from that point and on, *your computer had to take over* running this code.
56
+
57
+
You could argue that it's *still* not necessary to run this code on your computer. Maybe I could have it running on my server instead? Whenever you press the button, your computer could ask my server for the next rendering output. Isn't that how websites worked before all of those client-side JavaScript frameworks?
58
+
59
+
Asking the server for a fresh UI works well when the user *expects* a little delay--for example, when clicking a link. When the user knows they're navigating to *some different place* in your app, they'll wait. However, any direct manipulation (such as dragging a slider, switching a tab, typing into a post composer, clicking a like button, swiping a card, hovering a menu, dragging a chart, and so on) would feel broken if it didn't reliably provide at least *some* instant feedback.
60
+
61
+
This principle isn't strictly technical--it's an intuition from the everyday life. For example, you wouldn't expect an elevator button to take you to the next floor in an instant. But when you're pushing a door handle, you *do* expect it to follow your hand's movement directly, or it will feel stuck. In fact, even with an elevator button you'd expect at least *some* instant feedback: it should yield to the pressure of your hand. Then it should light up to acknowledge your press.
62
+
63
+
**When you build a user interface, you need to be able to respond to at least some interactions with *guaranteed* low latency and with *zero* network roundtrips.**
64
+
65
+
You might have seen the React mental model being described as a sort of an equation: *UI is a function of state*, or `UI = f(state)`. This doesn't mean that your UI code has to literally be a single function taking state as an argument; it only means that the current state determines the UI. When the state changes, the UI needs to be recomputed. Since the state "lives" on your computer, the code to compute the UI (your components) must also run on your computer.
66
+
67
+
Or so this argument goes.
68
+
69
+
---
70
+
71
+
Next, I'll argue the opposite--that components should run on *my* computer.
72
+
73
+
Here's a preview card for a different post from this blog:
74
+
75
+
```js
76
+
<PostPreview slug="a-chain-reaction" />
77
+
```
78
+
79
+
```js eval
80
+
<div className="mb-8">
81
+
<PostPreview slug="a-chain-reaction" />
82
+
</div>
83
+
```
84
+
85
+
How does a component from *this* page know the number of words on *that* page?
86
+
87
+
If you check the Network tab, you'll see no extra requests. I'm not downloading that entire blog post from GitHub just to count the number of words in it. I'm not embedding the contents of that blog post on this page either. I'm not calling any APIs to count the words. And I sure did not count all those words by myself.
88
+
89
+
So how does this component work?
90
+
91
+
```js
92
+
import { readFile } from "fs/promises";
93
+
import matter from "gray-matter";
94
+
95
+
export async function PostPreview({ slug }) {
96
+
const fileContent = await readFile("./public/" + slug + "/index.md", "utf8");
97
+
const { data, content } = matter(fileContent);
98
+
const wordCount = content.split(" ").filter(Boolean).length;
99
+
100
+
return (
101
+
<section className="rounded-md bg-black/5 p-2">
102
+
<h5 className="font-bold">
103
+
<a href={"/" + slug} target="_blank">
104
+
{data.title}
105
+
</a>
106
+
</h5>
107
+
<i>{wordCount} words</i>
108
+
</section>
109
+
);
110
+
}
111
+
```
112
+
113
+
This component runs on *my* computer. When I want to read a file, I read a file with `fs.readFile`. When I want to parse its Markdown header, I parse it with `gray-matter`. When I want to count the words, I split its text and count them. **There is nothing extra I need to do because my code runs *right where the data is*.**
114
+
115
+
Suppose I wanted to list *all* the posts on my blog along with their word counts.
116
+
117
+
Easy:
118
+
119
+
```js
120
+
<PostList />
121
+
```
122
+
123
+
```js eval
124
+
<PostList />
125
+
```
126
+
127
+
All I needed to do was to render a `<PostPreview />` for every post folder:
128
+
129
+
```js
130
+
import { PostPreview } from "./post-preview";
131
+
import { readdir } from "fs/promises";
132
+
133
+
export async function PostList() {
134
+
const entries = await readdir("./public/", { withFileTypes: true });
135
+
const dirs = entries.filter(entry => entry.isDirectory());
136
+
return (
137
+
<div className="mb-4 flex h-72 flex-col gap-2 overflow-scroll font-sans">
138
+
{dirs.map(dir => (
139
+
<PostPreview key={dir.name} slug={dir.name} />
140
+
))}
141
+
</div>
142
+
);
143
+
}
144
+
```
145
+
146
+
None of this code needed to run on your computer--and indeed, *it couldn't* because your computer doesn't have my files. Let's check *when* this code ran:
147
+
148
+
```js
149
+
<p className="text-purple-500 font-bold">
150
+
{new Date().toString()}
151
+
</p>
152
+
```
153
+
154
+
```js eval
155
+
<p className="text-purple-500 font-bold">
156
+
{new Date().toString()}
157
+
</p>
158
+
```
159
+
160
+
Aha--that's exactly when I last deployed my blog to my static web hosting! My components run during the build process so they have full access to my posts.
161
+
162
+
**Running my components close to their data source lets them read their own data and preprocess it _before_ sending any of that information to your device.**
163
+
164
+
By the time that you load this page, there is no `<PostList>`, `<PostPreview>`, `fileContent`, or `dirs`. There is only a `<div>` with some `<section>`s and `<a>`s and `<i>`s inside each of them. Your device only receives *the UI it needs to display* (the rendered post titles, link URLs, and post word counts) rather than *the raw data* that your components used to compute that UI (the actual posts).
165
+
166
+
With this mental model, *the UI is a function of server data*, or `UI = f(data)`. That data only exists *my* device, so that's where the components should run.
167
+
168
+
Or so the argument goes.
169
+
170
+
---
171
+
172
+
UI is made of components, but we argued for two very different visions:
173
+
174
+
* `UI = f(state)` where `state` is client-side, and `f` runs on the client. This approach allows writing instantly interactive components like `<Counter />`. (Here, `f` may *also* run on the server with the initial state to generate HTML.)
175
+
* `UI = f(data)` where `data` is server-side, and `f` runs on the server only. This approach allows writing data-processing components like `<PostPreview />`. (Here, `f` runs categorically on the server only. Build-time counts as "server".)
176
+
177
+
If we set aside the familiarity bias, both of these approaches are compelling at what they do best. Unfortunately, these visions *seem* mutually incompatible.
178
+
179
+
If we want to allow instant interactivity like needed by `<Counter />`, we *have to* run components on the client. But components like `<PostPreview />` can't run on the client *in principle* because they use server-only APIs like `readFile`. (That's their whole point! Otherwise we might as well run them on the client.)
180
+
181
+
Okay, what if we run all components on the server instead? But on the server, components like `<Counter />` can only render their *initial* state. The server doesn't know their *current* state, and passing that state between the server and the client is too slow (unless it's tiny like a URL) and not even always possible (e.g. my blog's server code only runs on deploy so you can't "pass" stuff to it).
182
+
183
+
Again, it seems like we have to choose between two different Reacts:
184
+
185
+
* The "client" `UI = f(state)` paradigm that lets us write `<Counter />`.
186
+
* The "server" `UI = f(data)` paradigm that lets us write `<PostPreview />`.
187
+
188
+
But in practice, the real "formula" is closer to `UI = f(data, state)`. If you had no `data` or no `state`, it would generalize to those cases. But ideally, I'd prefer my programming paradigm to be able to *handle both cases* without having to pick another abstraction, and I know at least a few of you would like that too.
189
+
190
+
The problem to solve, then, is how to split our “`f`” across two very different programming environments. Is that even possible? Recall we're not talking about some actual function called `f`--here, `f` represents all our components.
191
+
192
+
Is there some way we could split components between your computer and mine in a way that preserves what's great about React? Could we combine and nest components from two different environments? How would that work?
193
+
194
+
How *should* that work?
195
+
196
+
Give it some thought, and next time we'll compare our notes.
+14
public/the-two-reacts/post-list.js
+14
public/the-two-reacts/post-list.js
···
1
+
import { PostPreview } from "./post-preview";
2
+
import { readdir } from "fs/promises";
3
+
4
+
export async function PostList() {
5
+
const entries = await readdir("./public/", { withFileTypes: true });
6
+
const dirs = entries.filter((entry) => entry.isDirectory());
7
+
return (
8
+
<div className="mb-8 flex h-72 flex-col gap-2 overflow-scroll font-sans">
9
+
{dirs.map((dir) => (
10
+
<PostPreview key={dir.name} slug={dir.name} />
11
+
))}
12
+
</div>
13
+
);
14
+
}
+19
public/the-two-reacts/post-preview.js
+19
public/the-two-reacts/post-preview.js
···
1
+
import { readFile } from "fs/promises";
2
+
import matter from "gray-matter";
3
+
4
+
export async function PostPreview({ slug }) {
5
+
const fileContent = await readFile("./public/" + slug + "/index.md", "utf8");
6
+
const { data, content } = matter(fileContent);
7
+
const wordCount = content.split(" ").filter(Boolean).length;
8
+
9
+
return (
10
+
<section className="rounded-md bg-black/5 p-2">
11
+
<h5 className="font-bold">
12
+
<a href={"/" + slug} target="_blank">
13
+
{data.title}
14
+
</a>
15
+
</h5>
16
+
<i>{wordCount} words</i>
17
+
</section>
18
+
);
19
+
}