REACT (https://www.kirupa.com/react/index.htm)
BOOK (https://www.amazon.com/exec/obidos/ASIN/0134546318/kirupacom)
Server Components and React Frameworks
by kirupa (https://www.kirupa.com/me/index.htm) | filed under Learning React (https://www.kirupa.com/react/index.htm)
Everything you have built so far in these tutorials runs in the browser. Your JavaScript gets downloaded, React runs it, and the UI shows up on the screen. That is a perfectly good way to build apps, and it is how a Vite project works out of the box. At some point, though, you will start seeing words like server rendering, Server Components, 'use client', and Next.js thrown around as if everyone already knows what they mean. They sound like a big, scary new thing you have to learn before you can build anything real.
They are not. They are a few ideas that are easy to understand once you see them next to each other. In this tutorial, we will go through each of those ideas, look at the code for them, and then talk about frameworks. We will also spend time on an honest question that doesn't get asked enough: do you even need any of this?
Client-Only Rendering
Let's start with what you already know. When you build an app with Vite (the one from setting up your React environment (https://www.kirupa.com/react/setting_up_react_environment.htm)) and put it on a server, the HTML file that gets sent to the browser is almost empty. It has a
and a script tag, and that's about it. Everything else happens after the JavaScript shows up:
[Image: Client-only rendering goes from an empty HTML shell to downloading JavaScript, running React, and finally showing content. Server rendering goes from HTML with real content to downloading JavaScript, hydrating, and being fully interactive.] (https://www.kirupa.com/react/images/csr_vs_ssr_rendering_144.png)
The top row is what we call client-only rendering, or CSR. The browser gets an empty shell, downloads your JavaScript, React runs, and only then does anything useful appear. For an app that sits behind a login, like a dashboard or an admin tool, this is fine. Nobody is going to be upset that the first 300 milliseconds were a blank page.
For a blog, a store, or anything people find through a search engine, it can be less ideal. Search engines and link previews do their best to run JavaScript, but a page that already contains its content is an easier thing for them to read. It also means people on slower phones and connections stare at a blank page for longer, because nothing shows until the JavaScript has downloaded and run.
Server Rendering and Hydration
The bottom row of that diagram is the alternative. With server-side rendering, or SSR, your components get run on a server first. The server turns them into HTML and sends that to the browser. The page is readable right away, before any JavaScript has loaded. Then the JavaScript shows up, and React connects your components to the HTML that is already there, attaching event handlers so that buttons and inputs start working. That last step is called hydration.
Hydration is done with a function called hydrateRoot, which is the server-friendly sibling of the createRoot you have been using:
import { hydrateRoot } from "react-dom/client";
import App from "./App";
hydrateRoot(document.getElementById("root"), );
You almost never write this yourself. A framework does it for you. The important part is the idea: the server sends real HTML, and the browser makes it interactive.
There is one thing that trips people up here. SSR still sends all of your component code to the browser, because React needs it to hydrate. SSR makes the first paint faster, but it doesn't make your JavaScript smaller. That is where the next idea comes in.
Server Components
Server Components are a different thing from SSR, even though the names sound similar. A Server Component is a component that runs only on the server. It never ships to the browser. Its code isn't in your JavaScript bundle, and it never runs again on the client. All the browser receives is the output it produced.
That gives Server Components a few superpowers. Because they run on the server, they can read from a database, read a file, or call an API using a secret key, and they can do it with a plain async function and await. No useEffect and no loading state dance like we did in working with external data (https://www.kirupa.com/react/working_with_external_data.htm). Here is one, written the way a Next.js project would have it:
import { getHeroes } from "@/lib/heroes";
export default async function Home() {
const heroes = await getHeroes();
return (
Heroes
{heroes.map((hero) => (
{hero}
))}
);
}
Notice that the component itself is async. That is something a normal component running in the browser can't be. The getHeroes function could be talking to a database, and the code (along with any secrets it uses) stays on the server. In my own test project, I put a hero name in the server-only data and then looked through the JavaScript files the browser downloaded. The name was in the HTML, but not in any of the JavaScript. That is the whole trick.
There are tradeoffs, of course. A Server Component can't use state or effects, because it never lives in the browser to have state. It can't handle a click or a keystroke either. Anything that needs useState, useEffect, event handlers, or browser APIs has to be a Client Component. Don't let the name fool you, by the way: Client Components are just the regular components you have been writing this whole time. They can still be server rendered for that fast first paint. The word "client" means they are also sent to and run in the browser.
The 'use client' Directive
So how does React know which components are which? In a project that supports Server Components, components are Server Components by default. To opt a component into being a Client Component, you add a line at the very top of its file:
"use client";
import { useState } from "react";
export default function LikeButton() {
const [likes, setLikes] = useState(0);
return ;
}
That "use client" line is called a directive. It marks the boundary between server code and client code. Everything in that file, and everything that file imports, becomes part of the JavaScript that gets sent to the browser. Everything else stays on the server.
A Server Component can then import and render that Client Component like any other:
import { getHeroes } from "@/lib/heroes";
import LikeButton from "./LikeButton";
export default async function Home() {
const heroes = await getHeroes();
return (
{heroes.map((hero) => (
{hero}
))}
);
}
This is the part that is nice to see in a picture. Your app turns into a tree where some branches run on the server and some run in the browser:
[Image: A component tree where Page, Header, and HeroList are Server Components, while SearchBox and two LikeButton components are Client Components.] (https://www.kirupa.com/react/images/server_client_components_144.png)
The blue boxes ran on the server, and only their output is sent. The green boxes are Client Components, and they are the ones that ship JavaScript. Notice that a Server Component (HeroList) happily contains Client Components (the LikeButton boxes). The rule going the other direction is stricter. A Client Component can't import a Server Component, because by the time it is in the browser the server component's code isn't there. If you need to put server-rendered content inside a client component, you pass it in as children, the same way you would with any other component:
"use client";
import { useState } from "react";
import type { ReactNode } from "react";
export default function Expandable({ children }: { children: ReactNode }) {
const [open, setOpen] = useState(false);
return (
{open && children}
);
}
The server page can then hand it something to show, and that content is still rendered on the server:
{heroes.length} heroes, counted on the server.
I ran that exact code in a real project, and the button toggles the paragraph just fine. This pattern is a big part of why composing with children (https://www.kirupa.com/react/components.htm) is such a useful habit.
One more thing that is easy to get wrong: you only need "use client" at the boundary. If LikeButton imports ten other components, you don't add the directive to all ten. They all come along for the ride. So the goal is to push the boundary down as far as you can, keeping the interactive bits small and leaving the rest on the server.
Server Functions and 'use server'
Server Components cover reading data. What about changing it, like saving a form? For that, React has Server Functions. These are functions that live on the server, but that you can call from a Client Component (or hand to a form) as if they were regular functions. React takes care of the network request behind the scenes.
You mark them with a different directive, "use server":
"use server";
import { revalidatePath } from "next/cache";
import { saveHero } from "@/lib/heroes";
export async function addHero(formData: FormData) {
const name = String(formData.get("name") ?? "").trim();
if (name === "") {
return;
}
await saveHero(name);
revalidatePath("/");
}
The revalidatePath call is specific to Next.js. It tells the framework that the page at / has stale data and needs to be re-rendered. The rest is plain React. Now we can use addHero directly as the action of a form, just like you saw in forms in React (https://www.kirupa.com/react/forms_in_react.htm):
import { addHero } from "./actions";
export default function AddHeroForm() {
return (
);
}
When you submit the form, React sends the form data to the server, runs addHero there, and then the page updates with the new hero in the list. I tried this in a real Next.js project. I typed in a name, clicked Add, and the list updated without me writing any fetch call, any API route, or any loading state.
There are a few things to be careful about, and I'd rather you hear them from me than from a security audit:
- "use server" does not mean "Server Component." This is the most common mix-up. Server Components have no directive at all, because they are the default. The "use server" directive is only for Server Functions.
- Server Functions are public endpoints. Behind the scenes, each one becomes a URL that anyone can send a request to. Treat everything coming in as untrusted input: validate it, and check that the person calling it is allowed to do what they are asking.
- They must be async. The call crosses the network, so they always return a promise.
- The arguments must be serializable. You can pass strings, numbers, plain objects, and form data. You can't pass functions or class instances.
- They are for changing things, not fetching things. To read data, use a Server Component or a data-loading feature of your framework.
You Need a Framework for This
Here is the catch with everything we have just covered. React gives you the ideas and the primitives: Server Components, the directives, Server Functions. React does not give you the other half, which is a server that runs them, a bundler that splits the server and client code, and a router that decides which component to show for which URL. Wiring that up by hand is a lot of work, and the low-level pieces involved are really meant for the people who build bundlers and frameworks, not for app developers.
That is what a framework is for. It takes the pieces and hands them to you in a form you can use. Let's look at the two I tried out for this tutorial.
Next.js
Next.js (https://nextjs.org) is the most widely used React framework, and its App Router is where most of the Server Components code you see online comes from. Creating a project is one command:
npx create-next-app@latest my-app
It asks you a few questions about TypeScript, linting, and where to put your source files. I answered those and ran the dev server:
cd my-app
npm run dev
▲ Next.js 16.4.2 (Turbopack)
- Local: http://localhost:3000
✓ Ready in 370ms
The version number will be higher by the time you read this. Inside the project, the src/app folder is where everything happens. A folder is a route, a page.tsx file is what shows up for that route, and a layout.tsx wraps pages with shared UI. All of those files are Server Components unless you add "use client". The code you have seen in this tutorial is the code from that project.
When I ran npm run build, Next printed which routes it made:
Route (app)
┌ ○ /
└ ○ /_not-found
○ (Static) prerendered as static content
That ○ next to the route means Next worked out that my page didn't need to run per request, so it prerendered it to plain HTML at build time. Next.js changes a lot from release to release, especially around how it caches things, so use its docs as the source of truth for the details rather than a tutorial like this one.
React Router Framework Mode
If you read creating a single-page app using React Router (https://www.kirupa.com/react/creating_single_page_app_react_using_react_router.htm), you have already met this library. It turns out React Router can also act as a full framework. They call this framework mode, and it is built on Vite. Creating a project looks similar:
npx create-react-router@latest my-app
Routes are listed in a file called app/routes.ts, rather than being picked from folder names:
import { type RouteConfig, index, route } from "@react-router/dev/routes";
export default [
index("routes/home.tsx"),
route("heroes", "routes/heroes.tsx"),
] satisfies RouteConfig;
And a route file exports a loader for reading data, an action for handling form submissions, and the component itself:
import { Form } from "react-router";
import type { Route } from "./+types/heroes";
const heroes = ["Batman", "Sherlock Holmes"];
export async function loader() {
return { heroes };
}
export async function action({ request }: Route.ActionArgs) {
const formData = await request.formData();
const name = String(formData.get("name") ?? "").trim();
if (name !== "") {
heroes.push(name);
}
}
export default function Heroes({ loaderData }: Route.ComponentProps) {
return (
Heroes
{loaderData.heroes.map((hero) => (
{hero}
))}
);
}
It does the same thing as the Next.js example: it lists heroes and lets you add one. The approach is different, though. There is no "use server" here. The loader and action are special exports the framework knows how to find, and your data gets to the component through loaderData. After the action runs, React Router re-runs the loader for you, so the list updates on its own. The +types import is generated for you, which means the props are typed without any extra effort.
By default, a new React Router project server renders your routes. If you'd rather ship a plain client-side app, you flip one setting in react-router.config.ts:
import type { Config } from "@react-router/dev/config";
export default {
ssr: true,
} satisfies Config;
Set ssr to false and you get a single-page app. It also supports prerendering routes to static HTML at build time.
One honest note: at the time I'm writing this, Server Components support in React Router is still labeled as unstable and experimental. The loader and action approach above is the stable, supported way to use it. If Server Components are what you are after today, Next.js is where that is the most mature.
Others Worth Knowing
These two are not the only options. The React docs also list Expo for building native apps with React Native, and there are newer frameworks such as TanStack Start and RedwoodSDK. Frameworks come and go faster than React itself does, so I won't give you a ranking. Check the React docs page on creating a React app (https://react.dev/learn/creating-a-react-app) for the current list, and treat anything I write here about specific frameworks as a snapshot in time.
Do You Actually Need a Framework?
Now for the question I promised. The React team's official advice is to start new apps with a framework. That makes sense for a lot of people, because a framework solves routing, data loading, code splitting, and server rendering in a way that works together. But "recommended" and "required" are very different words, and a lot of apps don't need one.
A plain Vite project (what we have used in all the previous tutorials) is a great choice when:
- Your app lives behind a login, like an internal tool, a dashboard, or an admin panel, where search engines don't matter.
- You are learning React. Fewer moving parts means you can focus on React itself, and you are not fighting framework conventions at the same time.
- You have an existing backend (an API, a CMS, whatever) and you just need a front end for it.
- You are building something that is mostly client-side anyway, such as an editor, a game, or a drawing tool.
A framework starts to pay off when:
- Your pages are public and content-heavy, so first-load speed and being readable by search engines matter.
- You want to read from a database or an API on the server without building a separate backend for it.
- You have lots of routes and you want data loading and code splitting handled for you, instead of figuring it out one route at a time.
A framework also has costs. You inherit its conventions, its upgrade cycle, and the need to know which of your code runs where. When something breaks, it can be a server-side error or a client-side error, and the debugging is different for each. If a plain Vite app does everything you need, there is no shame in keeping it that way. It's also easier to move to a framework later than most people expect, because your components are still just components.
The good news is that everything you learned in the earlier tutorials (components, props, state, hooks, forms) carries over to both approaches without changes. Server Components and frameworks are about where your components run and how your app gets served. They don't replace anything you have learned so far.
Conclusion
Let's quickly recap. Client-only rendering sends an empty page and builds the UI in the browser. Server rendering sends real HTML first and then hydrates it. Server Components are components that run only on the server and never ship their code to the browser, and the "use client" directive marks where the browser takes over. Server Functions, marked with "use server", let you change data on the server, and they need to be treated as public endpoints. A framework like Next.js or React Router is what makes all of this usable, but you only need one when your app calls for it.
If you are curious about the framework route, my suggestion is to create a throwaway project with each of the two commands from this tutorial and look around. Both took a few minutes for me, and poking at a real project teaches you more than reading about one.
Next tutorial: Using React with TypeScript (https://www.kirupa.com/react/react_with_typescript.htm)
Onwards!