Two Computers, One User Interface
Any screen you look at — this page, a web app, a native application — is the product of at least two devices. There is your device, displaying the result, and there is the author's device, where the code and data that produced that result originally lived. At some point, that code and data had to travel from one machine to the other, turning into the HTML and JavaScript that your screen eventually renders.
In React, we break down what to display into independent components and compose them together. But components are code, and code runs somewhere. The question is: on whose computer should they run — yours, or the author's? Both options have their own compelling logic.
The Case for the Client
Consider a simple counter component. When you press its button, the number updates instantly. There is no network request, no waiting for a server to respond. This is possible because the component's code is running on your machine, keeping its own state:
import { useState } from "react";
export function Counter() {
const [count, setCount] = useState(0);
return (
<button
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"
onClick={() => setCount(count + 1)}
>
You clicked me {count} times
</button>
);
}Here, count is client state — data stored in your computer's memory that changes with each click. The author cannot predict how many times you'll press the button, so they cannot prepare all the possible outputs in advance. They can only send the initial rendering ("You clicked me 0 times") as HTML. From that point on, the client must run the code to handle the interaction.
It is conceivable to run this on a server instead, having it respond to each button press with fresh UI. That approach works when a user expects a delay — like when clicking a link to navigate. But direct manipulation — dragging a slider, typing, clicking a like button, swiping a card — demands at least some instant feedback or it feels broken. An elevator button only needs to eventually get you to the next floor, but it should at least yield and light up the moment you press it. A door handle must follow your hand immediately, or it feels stuck.
User interfaces need to provide guaranteed low-latency responses for at least some interactions, with zero network roundtrips.
The mental model often used for React is UI is a function of state, or UI = f(state). Since the state lives on your computer, the components that compute the UI from that state must also run on your computer. Or so the argument goes.
The Case for the Server
Now consider a preview card for another blog post on the same site. It displays the post's title and word count. If you check the network tab, you'll see no additional requests. The component is not downloading the full article, embedding its content, or calling an external API to count the words. It works because it runs on the server, right where the data is:
import { readFile } from "fs/promises";
import matter from "gray-matter";
export async function PostPreview({ slug }) {
const fileContent = await readFile("./public/" + slug + "/index.md", "utf8");
const { data, content } = matter(fileContent);
const wordCount = content.split(" ").filter(Boolean).length;
return (
<section className="rounded-md bg-black/5 p-2">
<h5 className="font-bold">
<a href={"/" + slug} target="_blank">
{data.title}
</a>
</h5>
<i>{wordCount.toLocaleString()} words</i>
</section>
);
}When this code needs to read a file, it uses fs.readFile. To parse a Markdown header, it uses gray-matter. To count words, it simply splits the text. Running components where the data lives enables them to read files and preprocess information before sending anything to the user.
Listing all posts with their word counts is just as straightforward, rendering a <PostPreview /> for every post folder:
import { readdir } from "fs/promises";
import { PostPreview } from "./post-preview";
export async function PostList() {
const entries = await readdir("./public/", { withFileTypes: true });
const dirs = entries.filter(entry => entry.isDirectory());
return (
<div className="mb-4 flex h-72 flex-col gap-2 overflow-scroll font-sans">
{dirs.map(dir => (
<PostPreview key={dir.name} slug={dir.name} />
))}
</div>
);
}None of this code could run on your computer because your computer doesn't have the author's files. A timestamp confirms when it ran:
<p className="text-purple-500 font-bold">
{new Date().toString()}
</p>That is the moment the blog was deployed to its static hosting. The components ran during the build, accessing all posts directly. By the time the page is loaded, the components themselves — along with all the fs, gray-matter, and raw file data — are gone. In their place is a <div> containing the rendered output with sections, links, and word counts. The client receives only what it needs to display, not the underlying data used to compute it.
Here, the mental model shifts to UI = f(data), where the data is server-side and the function runs only on the server. Build time counts as "server" in this context.
Choosing Between Two Realities
These two perspectives seem irreconcilable. One requires running components on the client to enable instant interactivity like the counter. The other requires running them on the server because the components use server-only APIs like readFile — that is their entire purpose.
Running everything on the server fails for components like the counter, which can only render their initial state. The server does not have access to the component's current state, and passing that state back and forth is typically too slow, or even impossible when the server code only executes during a build.
UI = f(state), withstateon the client andfon the client, enables components like<Counter />. (Here,fmay also run on the server with the initial state for HTML generation.)UI = f(data), withdataon the server andfrunning only on the server, enables components like<PostPreview />.
The real formula, however, is closer to UI = f(data, state). Handling only one or the other requires losing the ability to handle both cases within a clean abstraction. An ideal paradigm would support both without forcing a choice.
The problem becomes how to split that f — representing all the components — across two fundamentally different computing environments. Is it possible to divide components between the client to preserve interactivity and the server to enable direct data access, while still allowing them to be combined and nested together? That's the question worth exploring.



