[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:

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:
```jsx
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:
```tsx
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:
```tsx
"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:
```tsx
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:

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:
```tsx
"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:
```tsx
{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"`:
```ts
"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):
```tsx
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:
```bash
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:
```bash
npx create-react-router@latest my-app
```
Routes are listed in a file called `app/routes.ts`, rather than being picked from folder names:
```ts
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:
```tsx
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`:
```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!