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

Impossible Components

Composing across the stack.


Impossible Components

April 22, 2025

Pay what you like

Suppose I want to greet you in my favorite color.

This would require combining information from two different computers. Your name would be coming from your computer. The color would be on my computer.

You could imagine a component that does this:

import { useState } from 'react';
import { readFile } from 'fs/promises';

async function ImpossibleGreeting() {
const [yourName, setYourName] = useState('Alice');
const myColor = await readFile('./color.txt', 'utf8');
return (
<input placeholder="What's your name?"
value={yourName}
onChange={e => setYourName(e.target.value)}
<p style={{ color: myColor }}>
Hello, {yourName}!
</p>
</>

But this component is impossible. The readFile function can only execute on my computer. The useState will only have a useful value on your computer. We can’t do both at once without giving up the predictable top-down execution flow.

Or can we?


Splitting a Component

Let’s split this component in two parts.

The first part will read the file, which only makes sense on my computer. It is responsible for loading data so we’re going to call this part GreetingBackend:

import { readFile } from 'fs/promises';
import { GreetingFrontend } from './client';

async function GreetingBackend() {
const myColor = await readFile('./color.txt', 'utf8');
return <GreetingFrontend color={myColor} />;

It will read my chosen color and pass it as the color prop to the second part, which is responsible for interactivity. We’re going to call it GreetingFrontend:

'use client';

import { useState } from 'react';

export function GreetingFrontend({ color }) {
const [yourName, setYourName] = useState('Alice');
return (
<input placeholder="What's your name?"
value={yourName}
onChange={e => setYourName(e.target.value)}
<p style={{ color }}>
Hello, {yourName}!
</p>
</>

That second part receives that color, and returns an interactive form. Edit “Alice” to say your name and notice how the greeting updates as you’re typing:

Hello, Alice!

(If your name is Alice, you may leave it as is.)

Notice that the backend runs first. Our mental model here isn’t “frontend loads data from the backend”. Rather, it’s “the backend passes data to the frontend”.

This is React’s top-down data flow, but including the backend into the flow. The backend is the source of truth for the data—so it must be the frontend’s parent.

Have another look at these two parts and see how the data flows down:

import { readFile } from 'fs/promises';
import { GreetingFrontend } from './client';

async function GreetingBackend() {
const myColor = await readFile('./color.txt', 'utf8');
return <GreetingFrontend color={myColor} />;
'use client';

import { useState } from 'react';

export function GreetingFrontend({ color }) {
const [yourName, setYourName] = useState('Alice');
return (
<input placeholder="What's your name?"
value={yourName}
onChange={e => setYourName(e.target.value)}
<p style={{ color }}>
Hello, {yourName}!
</p>
</>

From the backend to the frontend. From my computer to yours.

Together, they form a single, encapsulated abstraction spanning both worlds:

<GreetingBackend />

Hello, Alice!

Together, they form an impossible component.

*(Here and below, the 'use client' syntax hints that we’ll be learning React Server Components. You can try them in Next—or in Parcel if you don’t want a framework.)*


Local State, Local Data

The beautiful thing about this pattern is that I can refer to the entirety of this functionality—its both sides—by writing a JSX tag just for the “backend” part. Since the backend renders the frontend, rendering the backend gives you both.

To demonstrate this, let’s render <GreetingBackend> multiple times:

<GreetingBackend />
<GreetingBackend />
<GreetingBackend />
</>

Hello, Alice!

Hello, Alice!

Hello, Alice!

Verify that you can edit each input independently.

Naturally, the GreetingFrontend state inside of each GreetingBackend is isolated. However, how each GreetingBackend loads its data is also isolated.

To demonstrate this, let’s edit GreetingBackend to take a colorFile prop:

import { readFile } from 'fs/promises';
import { GreetingFrontend } from './client';

async function GreetingBackend({ colorFile }) {
const myColor = await readFile(colorFile, 'utf8');
return <GreetingFrontend color={myColor} />;

Next, let’s add Welcome that renders GreetingBackend for different color files:

import { readFile } from 'fs/promises';
import { GreetingFrontend } from './client';

function Welcome() {
return (
<GreetingBackend colorFile="./color1.txt" />
<GreetingBackend colorFile="./color2.txt" />
<GreetingBackend colorFile="./color3.txt" />
</>

async function GreetingBackend({ colorFile }) {
const myColor = await readFile(colorFile, 'utf8');
return <GreetingFrontend color={myColor} />;

Let’s see what happens:

<Welcome />

Each greeting will read its own file. And each input will be independently editable.

Hello, Alice!

Hello, Alice!

Hello, Alice!

This might remind you of composing “server partials” in Rails or Django, except that instead of HTML you’re rendering fully interactive React component trees.

Now you can see the whole deal:

  • Each GreetingBackend knows how to load its own data. That logic is encapsulated in GreetingBackend—you didn’t need to coordinate them.
  • Each GreetingFrontend knows how to manage its own state. That logic is encapsulated in GreetingFrontend—again, no manual coordination.
  • Each GreetingBackend renders a GreetingFrontend. This lets you think of GreetingBackend as a self-contained unit that does both—an impossible component. It’s a piece of the backend with its own piece of the frontend.

Of course, you can substitute “reading files” with “querying an ORM”, “talking to an LLM with a secret API key”, “hitting an internal microservice”, or anything that requires backend-only resources. Likewise, an “input” represents any interactivity. The point is that you can compose both sides into self-contained components.

Let’s render Welcome again:

<Welcome />

Hello, Alice!

Hello, Alice!

Hello, Alice!

Notice how we didn’t need to plumb any data or state into it.

The <Welcome /> tag is completely self-contained!

And because the backend parts always run first, when you load this page, from the frontend’s perspective, the data is “already there”. There are no flashes of “loading data from the backend”—the backend has already passed the data to the frontend.

Local state.

Local data.

Single roundtrip.

Self-contained.


It’s Not About HTML

Okay, but how is this different from just rendering a bunch of HTML?

Let’s tweak the GreetingFrontend component to do something different:

import { readFile } from 'fs/promises';
import { GreetingFrontend } from './client';

async function GreetingBackend() {
const myColor = await readFile('./color.txt', 'utf8');
return <GreetingFrontend color={myColor} />;
'use client';

import { useState } from 'react';

export function GreetingFrontend({ color }) {
const [yourName, setYourName] = useState('Alice');
return (
<input placeholder="What's your name?"
value={yourName}
onChange={e => setYourName(e.target.value)}
onFocus={() => {
document.body.style.backgroundColor = color;
onBlur={() => {
document.body.style.backgroundColor = '';
<p>
Hello, {yourName}!
</p>
</>

[...]


Original source

Reply