The Two Reacts
UI = f(data)(state)
The Two Reacts
January 4, 2024
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.
Your device and mine.
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.
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 for an intro.
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?
Let’s make a case for each side.
First, I’ll argue that components should run on your computer.
Here’s a little counter button to demonstrate interactivity. Click it a few times!
<Counter />
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.
This is possible because this component’s code is running on your computer:
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 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.
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?
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.
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.
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.
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.
Or so this argument goes.
Next, I’ll argue the opposite—that components should run on my computer.
Here’s a preview card for a different post from this blog:
<PostPreview slug="a-chain-reaction" />
A Chain Reaction
2,452 words
How does a component from this page know the number of words on that page?
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.
So how does this component work?
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>
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.
Suppose I wanted to list all the posts on my blog along with their word counts.
Easy:
<PostList />
A Chain Reaction
2,452 words
A Complete Guide to useEffect
9,913 words
A Lean Syntax Primer
5,460 words
A Social Filesystem
6,291 words
Algebraic Effects for the Rest of Us
3,062 words
Before You memo()
856 words
Beyond Booleans
3,523 words
Coping with Feedback
669 words
Fix Like No One’s Watching
251 words
Functional HTML
3,714 words
Goodbye, Clean Code
1,196 words
Hire Me in Japan
1,333 words
How Are Function Components Different from Classes?
2,519 words
How Does React Tell a Class from a Function?
3,000 words
How Does setState Know What to Do?
1,511 words
How Does the Development Mode Work?
1,930 words
How Imports Work in RSC
4,230 words
How to Fix Any Bug
2,325 words
I'm Doing a Little Consulting
429 words
Impossible Components
4,207 words
Introducing RSC Explorer
1,297 words
JSX Over The Wire
11,212 words
Making setInterval Declarative with React Hooks
2,769 words
My Decade in Review
5,865 words
My Wishlist for Hot Reloading
2,602 words
Name It, and They Will Come
774 words
npm audit: Broken by Design
2,628 words
On let vs const
673 words
One Roundtrip Per Navigation
3,721 words
Open Social
4,473 words
Optimized for Change
225 words
Preparing for a Tech Talk, Part 1: Motivation
1,122 words
Preparing for a Tech Talk, Part 2: What, Why, and How
891 words
Preparing for a Tech Talk, Part 3: Content
1,401 words
Progressive JSON
2,450 words
React as a UI Runtime
6,760 words
React for Two Computers
16,499 words
RSC for Astro Developers
1,803 words
RSC for LISP Developers
614 words
Static as a Server
[...]