Lesson 10 of 12

83% Complete

Server Components (Experimental)

Explore React Server Components, a new paradigm for building React applications with server-side rendering and improved performance.

Overview

In this lesson, we'll build a UserProfileCard component using React Server Components (RSC) with Next.js App Router. This component will display a user's avatar, name, and bio (using mock data), and include a Follow button for interactivity. Along the way, you'll learn the core ideas behind React Server Components and how to separate server and client concerns in your app. By the end, you'll understand how to fetch data on the server, when to use client components for interactivity, and best practices to avoid common mistakes.

Table of Contents:

  • Step-by-step: build it right
    1. Preparing mock data – set up a getUser() function in lib/data.ts to simulate fetching user info.
    2. Creating the UserProfileCard (Server Component) – build the profile card in app/components/UserProfileCard.tsx that fetches and renders user data on the server.
    3. Adding a FollowButton (Client Component) – create a nested Follow button in app/components/FollowButton.tsx for interactivity (state and events).
    4. Using the UserProfileCard in a Next.js page – import and display the component in a Next.js App Router page.
  • Best practices & common mistakes – 5 key tips to use Server and Client Components effectively (and pitfalls to avoid).
  • Recap – a quick summary of what you've learned.

Project Structure

Here's the file structure we'll be working with in this tutorial:

File Structure
app
components
│ ├── UserProfileCard.tsxServer Component
│ └── FollowButton.tsxClient Component
page.tsxNext.js page
layout.tsxApp layout
lib
data.tsMock data functions

Step-by-step: build it right

Preparing mock data for the example

Let's start by setting up a fake data source. Create a file lib/data.ts with an async function getUser() that returns mock user data. This simulates a database or API call on the server:

Loading syntax highlighting...

This simple function will be used in our Server Component to fetch user info. In a real app, you might query a database or external API here. Because it runs on the server, you can safely use secrets or tokens if needed (none will be exposed to the browser).

Creating a server component (UserProfileCard)

Now, let's build the UserProfileCard as a React Server Component. In Next.js's App Router, any component file without the "use client" directive is a Server Component by default. This means it runs entirely on the server, allowing us to fetch data and render HTML there before sending it to the user's browser.

Create app/components/UserProfileCard.tsx:

Loading syntax highlighting...

What's happening? This UserProfileCard component is an async function, which allows us to await the getUser(userId) call. The code runs on the server for each request, so it can securely fetch data (e.g. from a database) and assemble the HTML. We didn't include any 'use client' line at the top, so Next.js knows this is a Server Component. As a result, the logic in this component (the data fetching and JSX generation) will not be sent to the browser – only the rendered result (HTML and props) gets sent. This keeps the page load lean and fast by reducing the amount of JavaScript sent to users.

Notice that in the JSX, we're just outputting static content (image, name, bio). There are no event handlers or browser-specific code here. That's intentional: Server Components can't manage interactive state or handle events directly. If you tried to use a React hook like useState or an onClick in this file, you'd get an error, because those require a Client Component environment. In the next step, we'll add a Client Component to handle interactivity.

Adding a nested client component (FollowButton)

To introduce interactivity (like a follow button that the user can click), we need a Client Component. Client Components run in the browser, so they can use React state, effects, and event handlers. We mark a component as client-only by adding a special directive at the top of the file: 'use client'.

Create app/components/FollowButton.tsx:

Loading syntax highlighting...

Because we included 'use client' at the top, this component is bundled and executed in the browser. Here we use useState to track if the user is "following" or not, and an onClick handler to toggle that state. This kind of interactive code must be in a Client Component – trying to use state or onClick in a server file would fail (we'd get a React error about event handlers or hooks not being allowed in Server Components).

Now that we have a Follow button, we can compose it into our server-rendered profile card. To do this, import FollowButton into UserProfileCard.tsx and include it in the JSX. We'll pass the userId as a prop, so the client component knows which user it's handling (for a real app, this might be used to send a follow/unfollow request):

Loading syntax highlighting...

By including the <FollowButton userId={user.id} /> inside our Server Component's JSX, we're effectively telling React/Next.js: "Render this part on the client." Under the hood, Next.js will inject a placeholder for the FollowButton in the server-rendered HTML and then hydrate it on the client side. The server passes the prop userId (in this case, "1", etc.) down to the client component as serialized data. This allows the FollowButton to know which user to act on without having to fetch that data itself. (In our example, we just log the userId on click, but you could use it to call a follow/unfollow API.)

Using the UserProfileCard in a Next.js page

Now that our component is ready, let's use it in a Next.js route. In the App Router, page components are also Server Components by default, which means they can import and use our UserProfileCard directly. For example, create app/page.tsx (the homepage) and render the profile card:

Loading syntax highlighting...

When a user visits this page, Next.js will render the <UserProfileCard userId="1" /> on the server. During that process, getUser("1") runs on the server (fetching our mock data) and returns the user info. The HTML for the card (with the avatar image, name, and bio) is generated on the server and sent to the browser. Meanwhile, the <FollowButton> is identified as a client component, so its HTML placeholder and props are included, but its interactive logic is loaded separately as JavaScript. The user will see the profile info immediately as static content, and then React hydrates the Follow button, wiring up its click handler in the browser. This hybrid approach gives you the best of both worlds: fast initial load with server-rendered content, and rich interactivity where needed on the client.

Reusability: Because UserProfileCard is a self-contained Server Component (just needs a userId), you can reuse it across pages or even render multiple profile cards on one page. Each instance will fetch its data on the server. And since the heavy lifting is server-side, adding more of these cards won't bloat your JavaScript bundle on the client – the data and HTML for each card come from the server, and the only client-side code reused is the small FollowButton component.

Best practices & common mistakes

  • Use Server Components for data-driven UI: By default, lean on Server Components to fetch data and render markup on the server whenever possible. This keeps sensitive data (API keys, database queries) safely on the server and reduces the amount of JS sent to the browser, improving performance.
  • Use Client Components only when necessary: Only add 'use client' to components that truly need interactivity (state, event handlers, browser APIs). Avoid making large parent components client-side; instead, isolate interactivity into small pieces. This way, most of your UI remains server-rendered and fast, and you send less JavaScript down the wire.
  • Pass data via props between server and client components: A Server Component can pass fetched data to a child Client Component through props (as we did with userId). This is the recommended way to combine them – the Server Component does the data fetching, then renders a Client Component for interactive parts, providing it just the data it needs.
  • Don't use React hooks or browser-only APIs in a Server Component: Calling useState, useEffect, or touching window/document in a server file will throw errors. If a component needs those, mark it as 'use client' (and keep such code out of your server-only files). Conversely, avoid fetching data or using sensitive secrets in a Client Component, since that would have to run in the user's browser.
  • Avoid importing server-only code into client components (and vice versa): If you import a Server Component or server module into a 'use client' file, it can force that code to run on the client (or cause build errors). For example, do not import a database utility or server-only component inside a client component – instead, have the server component fetch what's needed and pass it down. Keep the boundaries clear: server stuff stays on the server, client stuff stays on the client.

⚠️ Warning:

Finally, remember that Server Components do not maintain state between renders. They render on each request (or navigation) and produce HTML. If something in a server component needs to update based on user interaction, that interaction has to come through a client component (e.g. a form submission, action, or a navigation). Server Components are best thought of as rendering static or data-loaded UI, not interactive experiences on their own.

Recap

  • React Server Components allow you to render components on the server, fetch data close to your data source, and send pre-rendered HTML to the client. This results in faster loads and smaller client bundles.
  • In Next.js App Router, components are server-side by default. You add 'use client' at the top of a file to make it a Client Component when you need interactivity (state, events, browser APIs).
  • We built a UserProfileCard as a Server Component that fetches user data via an async function (running on the server) and returns UI. Because it's a server component, it can safely use server-side code and doesn't ship that logic to the browser.
  • We added a FollowButton as a Client Component for interactivity. It manages its own state (following or not) and handles click events to toggle that state. We marked it with 'use client' so it runs in the browser and can use React hooks.
  • We learned how to compose server and client components: the server component renders the client component and passes it data via props. Next.js handles merging the two – the static content is delivered in HTML, and the client-side JavaScript hydrates the interactive parts.
  • Key best practices include keeping most of your UI as server-rendered (for performance and security), using client components sparingly for interactivity, and avoiding common mistakes like using hooks in a server context or needlessly bloating the client bundle.

With these fundamentals, you can confidently build modern Next.js UIs that leverage the power of React Server Components, creating apps that are both fast and dynamic. Happy coding!


Next: Best Practices and Architectural Patterns →