PostHole
Compose Login
You are browsing eu.zone1 in read-only mode. Log in to participate.
rss-bridge 2024-01-04T00:00:00+00:00

The Two Reacts

UI = f(data)(state)


The Two Reacts

January 4, 2024

Pay what you like

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

[...]


Original source

Reply