ishan's notes

React Server Components


I was trying to make sense of why new React code shows up with use client and use server annotations everywhere. Apparently, there’s this new construct called Server components now.

Why is it cool

Why is it annoying

Most educational material (sans Dan Abramov, obviously) on React server components has been poor, sorry. They always seem to center around with what is allowed, what are the edge cases, which part of the framework they work with.

It doesn’t feel that tricky once you see what React is trying to get at. And yet.

How to think about it

There are a couple maxims I think, that help make sense of the RSC model.

The RSC model is about trying to render as much of the DOM tree as possible on the server; starting out from the top level component and until it comes across a client component. At that point, the framework sends the incompletely rendered tree to the browser, and the code (and props) for the client component. The React bundle in the browser executes that code to inject the client component, and finish the tree.

Some implications.

Example snippets

wes-bos-rsc-examples.svg

Wes Bos has a good diagram showing how composition works in the server component paradigm. Following are some quick notes about why those work, or don’t.

1. Server Components

export default function MyServerComponent() {
  // Server logic: can read from DB, filesystem, etc.
  const data = "You have 15 files rendered with node v20.8.0 from Server.tsx!";
  return (
    <div>
      <p>{data}</p>
    </div>
  );
}

This shows a simple server component output: it fetches or calculates data on the server and sends back rendered results. It works because the server environment can access things like the filesystem or database. React takes the markup generated by this component and ships it to the client.

2. Client Components

"use client";
import { useState } from "react";

export default function MyClientComponent() {
  const [count, setCount] = useState(0);
  return (
    <button onClick={() => setCount(count + 1)}>
      {count} Clicks
    </button>
  );
}

Here, a client component displays interactive content (like a button). It’s rendered in the client environment because it needs browser APIs or state. You can track clicks or user input, something you can’t do in a purely server component.

3. Client Components Inside Server Components

"use server";
import MyClientComponent from "./MyClientComponent";

export default function ServerWrapper() {
  return (
    <div>
      <p>Server logic here</p>
      <MyClientComponent />
    </div>
  );
}

The server can nest a client component. The tree starts on the server, which renders what it can. Wherever interactivity is needed, it hands off to the client component. That’s valid because the server already prepared the layout or data, and the client piece just needs to plug in.

4. You Can’t Put a Server Inside a Client

"use client";
// This won't work as intended because you can't import/run server code here.
import MyServerComponent from "./MyServerComponent";

export default function ClientWrapper() {
  return (
    <div>
      <p>Trying to render a server component here</p>
      <MyServerComponent />
    </div>
  );
}

A client component can’t directly import or run a server component, as the browser doesn’t have access to server-only logic (e.g., reading from disk or using server credentials). Attempting this would break the tree because the client can’t execute that server function.

NOTE: The framework can’t possibly know that ClientWrapper is trying to render MyServerComponent until the ClientWrapper function is already run. Maybe a compiler pass could do it someday. But that’s the reason for this rule.

5. Passing a Server Component Through Props

Server “Container” Component

"use server";
import MyServerComponent from "./MyServerComponent";
import ClientShell from "./ClientShell";

export default function Container() {
  return (
    <ClientShell
      serverBlock={<MyServerComponent />}
    />
  );
}

Client Shell Component

"use client";

export default function ClientShell({ serverBlock }) {
  return (
    <div>
      <p>This is the client shell</p>
      {serverBlock}
    </div>
  );
}

While you can’t embed a server component in a client component, the server can still pass its own rendered output as props. The server computes MyServerComponent, then sends it over along with the client component code. It gets used by React in the browser to execute the ClientShell component function.

NOTE: The difference with the previous example is that the server component is computed outside the client component, so the RSC framework could execute it on the server.

6. A Client Can Run Code on the Server

Server Action

"use server";

export async function getServerTime() {
  return new Date().toLocaleTimeString();
}

Client Component

"use client";
import { useState } from "react";
import { getServerTime } from "./actions";

export default function TimeClient() {
  const [serverTime, setServerTime] = useState("");

  async function handleRefresh() {
    const time = await getServerTime();
    setServerTime(time);
  }
  return (
    <div>
      <button onClick={handleRefresh}>Get Server Time</button>
      <p>Server says: {serverTime}</p>
    </div>
  );
}

The client triggers server logic through something like an API call or an action. These are regular API endpoints, so it is a regular browser-server interaction. The server computes the time and sends it back to the client, which updates the UI.

7. Passing Client Data to the Server and Back

Server Action

"use server";

export async function searchProducts(query) {
  // Example server logic: database or external API call.
  return [`Result 1 for ${query}`, `Result 2 for ${query}`];
}

Client Component

"use client";
import { useState } from "react";
import { searchProducts } from "./actions";

export default function ProductSearch() {
  const [query, setQuery] = useState("");
  const [results, setResults] = useState([]);

  async function handleSearch() {
    const response = await searchProducts(query);
    setResults(response);
  }

  return (
    <div>
      <input 
        placeholder="Search..."
        value={query}
        onChange={(e) => setQuery(e.target.value)}
      />
      <button onClick={handleSearch}>Go</button>

      <ul>
        {results.map((r, idx) => <li key={idx}>{r}</li>)}
      </ul>
    </div>
  );
}

The user can input data in the client component and send it to the server for processing (for example, searching a database). The server returns updated results, which React displays on the client side. Again, this is regular client-server interaction, like a typical web app.