Back to royalty.blog
March 28, 2025Royalty8 min read

Modern React: Server Components, Client Components, and Server Actions Explained

React 19 introduces an architecture with Server Components, Client Components, and Server Actions, each serving a unique role. In this article, we'll break down what each of these concepts means, how they work together, and when to use them.

ReactNext.jsFrontend
Cover image

Pavel Ulanovskiy Image

Introduction

It's easy to do a job with one tool; easy, but not necessary convenient. Yet, there are other tools that can do same thing at the end of the day. With great power, comes great responsibilities they say and frontend developers have this great power :).

Simply because a set of tools can do be used for similar operations, doesn't mean they should be used randomly, or without reasonable consideration. It's this same way I misused the features of Next.js when I started using it. However, with my hunger for, and readings about best practices, I have acquired a good knowledge of the feature, and know when and how to apply them.

Client Component

In case you're not aware, React is simply a library that allows for building user interfaces. One of its key feature is its component-based architecture. A component is an independent pieces of UI.

React is a library, not a framework. Next.js is a react framework.

Before the introduction of server components performing asynchronous operations within a component was tricky, you'd have to fetch your data in a useEffect hook.

React provides something called hooks. My understanding of hooks is that its React's way of managing states and triggering renders when they change.

// The way it should be
function MyComponent() {
    let count = 0; // we should be able to increment count, right?
    return <>
        <div>
         <button onClick={() => {count = count + 1}}>
            Click to Increase
         </button>{" "}
         {count}
        </div>
    </>
}

The above code wouldn't work as expected. A React function component is a JavaScript function with markup. During rendering, React does not track changes to regular variables like count because React's rendering system is immutable. This means that updates to count won't trigger a re-render of the component. React hooks, such as useState, solve this problem by providing a way to manage state and trigger re-renders when the state changes.

// Unfortunately
function MyComponent() {
    const [count, setCount] = React.useState(0) // not really without this.
    return <>
        <div>
         <button onClick={() => setCount((prev) => prev + 1)}>
            Click to Increase
         </button>{" "}
         {count}
        </div>
    </>
}

React puts interactivity first while still using the same technology: a React component is a JavaScript function that you can sprinkle with markup. - react.dev

I think the little introduction to React is fine, but what does the term client component means?

If we're considering the term client, then it means the bigger picture includes server. Client components are React component, that runs on the client-side.

Every web application is served through a server. In web development terms, the client usually refers to the web browser, which can execute JavaScript code. Client components are bundled JavaScript code that gets executed in your browser. While the server hosting an application can send HTML code for the browser to render, there are also cases where the rendered markup needs to be manipulated dynamically.

With that being said, here is when you will need to use client component:

  • For Interactivity: React hooks don't work with server components. Remember that React hooks are for managing states and triggering re-render when the state changes. Any piece of the UI that requires a state, will be a client component. States cannot be managed on the server.

    Hmmm, there could be a way to have states on the server, idk, but I guess it'll require a lot of polling between the client and the server. Unnecessary complications.

  • If you need the Browser API: APIs like localStorage, document etc. are provided by your browser, so the bundled client component can only access them there. If your component needs to work with localStorage, or needs to use the document to manipulate an element, that component must be a client component.

  • Your Form Should Be A Client Component: Form fields like <input>, <select> etc. depends on states. When states are required, your component should be a client component.

Rules

  • Cannot directly access databases, or perform server-side logic.
  • Use when user interactivity in required.
  • A client component can be rendered within a server component, but not vice-versa.
// ✅ works
<ServerComponent>
    <ClientComponent/>
</ServerComponent>

...

// ❌ wrong
<ClientComponent>
  <ServerComponent/>
</ClientComponent>

If you to use a server component's data or functionality within a client component you can do so by:

  1. Passing Data as Props: Fetch the data in a server component and pass it as props to client Component
  2. Use Server Actions: Call a server action from the client component to perform server-side logic and update the UI.

Server Component

A server component is a component that's executed on the server. The server is the running environment for a server component. It's a more optimized environment compared to the client.

Server component lacks the features of a client component by design to optimize performance and security**. **In a client component, the bundled component is sent to the client, executed on the client, and allows for manipulation. e.g A form for updating profile where the client component makes a request to fetch data from the API, renders the data, and manipulation can take place etc.

In cases where the data sent doesn't need manipulation, a server component has a vital influence here. e.g. for a page that shows a list of invoice:

  • A request is made for the invoice list page.
  • The server component is executed on the server.
  • Inside the server component, we get the data from the database directly or via an API.
  • The received data is rendered according the component's logic.
  • The UI code is sent back to the client.
  • The client renders the received markup code, no need for any client-based code execution.

This is a fairly straightforward system, these sub-tasks are added-up into a single request e.g. GET /dashboard/invoices etc.

If client component were to be employed, we'll be making more requests:

  • The server will fetch the bundled code that'll be executed on the client-side.
  • The client-side code will execute and make a request to the API for fetching the data.

Server Component:

  • Execute on the server, reducing client-side JavaScript
  • Can fetch data directly, interact with databases and perform secure operations.
  • Cannot use React state, effects, or event listeners.
import db from "@/lib/db"; // Assuming we're using Prisma

export default async function UserList() {
  const users = await db.user.findMany();

  return (
    <ul>
      {users.map((user) => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  );
}

Rules:

  • For initial rendering and data fetching.
  • Server component can import and render both server and client components.
<ServerComponent1>
    <InvoiceList/> {/* server component */}
    <NewInvoiceForm/> {/* client component */}
</Servercomponent1>

Server Action

Server action is the bridge between server component and client component. A server action is not a UI component, it's a function without the sprinkle of markup. It's executed on the server, so it can call the database directly.

Since client components cannot call a database directly, the default was to call an API endpoint for such operation. The introduction of server action stops that by being a function that runs on the server, can call your database directly, and provides the data to the client. Server actions handles the data fetching/modification logic, and allows your client components handle the UI. I like to describe it as an implicit, phantom-like API endpoint the client component can call.

Features of Server Action:

  • Asynchronous server functions that modify or fetch data.
  • Used for form submissions, database mutations, etc.
  • Can be used directly in Server or Client Components.

Note: A server action must be an async function.

Conclusion

So, there you have it! Server Components, Client Components, and Server Actions. It might seem like a lot to take in at first, but trust me, once you get the hang of it, you'll be building faster, more secure, and all-around better web apps.

Remember, Server Components are your go-to for initial rendering and data fetching, Client Components are for interactivity, and Server Actions lets your Client Components talk to the server without exposing your database keys to the world.

To get a hang of these features you try experimenting and building things with them (that's how we learn, right?). And as always, keep exploring, and keep pushing the boundaries of what's possible with React! 👋🏾