Next.js Server vs. Client Components

Next.js is a hybrid framework, meaning it allows you to create applications with both server-side rendering and client-side rendering strategies. This architecture allows you to create dynamic web apps, combining the benefits of server-rendered frameworks like Express.js with the interactivity of client-rendered libraries like React.js.

In this lesson, we are going to discuss what are server and client components, and what part of your app should be rendered on the client or server.

Server-side rendering

By default, Next.js renders all the components on the server, and there are reasons for this. Server components come with several advantages over client components:

  • Security: Server components offer enhanced security by accessing resources such as databases and APIs directly from the server. This eliminates the need to expose sensitive information to the client, ensuring data remains secure and protected from potential vulnerabilities.
  • Performance: Another key benefit is improved performance. By handling data fetching on the server, server components reduce the time required to retrieve and process information, which leads to faster response times and reduced initial load times for your application.
  • Caching: Server-side rendering introduces an efficient caching mechanism. Rendered pages can be cached and reused for subsequent requests or shared among other users, further optimizing performance and load times.
  • Better SEO: Server-rendered pages also improves SEO. Since the content is rendered and available when the page loads, search engines can easily crawl and index it, enhancing the visibility and discoverability of your application.

Client-side rendering

Server-side rendering does come with issues, and the main disadvantage is the lack of interactivity. Client components give us access to:

  • Event Handlers: Such as onClick(), onChange(), which are crucial for building interactive webpages.
  • React.js APIs: Such as states (useState()), side effects (useEffect()), and other React features.
  • Browser APIs: Such Clipboard API, Geolocation API, and so on.

You can opt into client components by adding a "use client" directive at the top of your component, which tells Next.js that we need to work with a client component. For example, here is how we can create a counter app in Next.js.

jsx
1"use client";
2
3import { useState } from "react";
4
5export default function Counter() {
6  const [count, setCount] = useState(0);
7
8  return (
9    <div>
10      <p>Count: {count}</p>
11      <button onClick={() => setCount(count + 1)}>Increment</button>
12    </div>
13  );
14}

By declaring "use client" at the top, we gained access to the event handler onClick() and the useState() hook from React.

Also notice that the Counter() is a regular function, not an async function. Next.js does not yet support async/await syntax in client components. If you mark a client component as asynchronous, an error will be thrown.

Async client error

If you need to perform asynchronous operations in client components, you can move them inside side effects or event handlers.

Preferred composition pattern

Both server and client components are very useful under different circumstances. Server components brings better performance and enhanced security, while client components brings interactivity and access to browser APIs.

In modern web development, these are all very important factors, and you should try to reach a balance when designing you web apps.

balanced server client composition

To achieve this balance, there are a few rules you should follow.

First of all, there is a reason that Next.js made server components the default choice. Whenever you create a new component, server side rendering should be preferred.

Server components have direct access to resources stored on the server, especially sensitive information such as API keys and databases. Preferring server components makes sure these data can be accessed securely and quickly without delays.

Also, server components taps into several optimization features offered by Next.js, such as caching, static site generation, incremental static regeneration, and so on. We will cover these optimization features later in this course.

So, when do you use client components?

The answer is simple, whenever you need to introduce interactivity into the webpage, such as a button that opens a pop-up modal window, a toggle that switches the dark mode, and so on, it is time to switch to client side rendering.

One thing you should keep in mind is that the client components should be moved down the component tree, and in most cases, they should be the "leaves".

component tree

For instance, you are creating a sidebar component. The majority of this component are non-interactive, and should be rendered on the server, but it also contains a subscription form. The form interacts with the user and collects their email, and hence it must be rendered on the client.

In this case, instead of making the whole sidebar into a client component, you should split the subscription form into an independent component, and then import it back into the sidebar.

This strategy ensures minimum client side JavaScript, delivering better performance and improving initial loading time.

Do not import server into client

In most cases, the server and client components work exactly like the React components we discussed before, where a component can be exported and imported into other components, and data can flow across components via props.

However, there are a few rules you should follow.

First of all, you can import client component into server component, but not the other way around. Take a look at this example:

text
1src
2├── app
3│   ├── favicon.ico
4│   ├── fonts
5│   ├── globals.css
6│   ├── layout.jsx
7│   └── page.jsx             <===== Home page
8└── components
9    ├── clientComponent.jsx  <===== Client component
10    └── serverComponent.jsx  <===== Server component

To make this example more intuitive, we'll name the components clientComponent and serverComponent.

components/clientComponent.jsx

jsx
1"use client";
2
3export default function ClientComponent() {
4  return <div>This is a client component.</div>;
5}

components/serverComponent.jsx

jsx
1import ClientComponent from "./clientComponent";
2
3export default function ServerComponent() {
4  return (
5    <>
6      <div>This is a server component.</div>
7      <ClientComponent />
8    </>
9  );
10}

Notice that the client component is imported into server component, and then rendered using the JSX syntax, <ClientComponent />.

And don't forget to render the server component in our home page:

app/page.jsx

jsx
1import ServerComponent from "@/components/serverComponent";
2
3export default async function Home() {
4  return <ServerComponent />;
5}

Open the browser and go to http://localhost:3001/, you should see the following page:

Import client into server component

However, the opposite cannot be done, you cannot import server component into client component.

components/clientComponent.jsx

jsx
1"use client";
2
3import ServerComponent from "./serverComponent"; // Do NOT try to do this
4
5export default function ClientComponent() {
6  return (
7    <>
8      <div>This is a client component.</div>
9      <ServerComponent />
10    </>
11  );
12}

You can pass server to client as a prop

But what if, in some rare cases, you need to render a server component inside a client component? This can be achieved by passing the serverComponent to clientComponent as a prop:

components/clientComponent.jsx

jsx
1"use client";
2
3export default function ClientComponent({ children }) {
4  return (
5    <>
6      <div>This is a client component.</div>
7      {children}
8    </>
9  );
10}

components/serverComponent.jsx

jsx
1export default function ServerComponent() {
2  return <div>This is a server component.</div>;
3}

Notice that the clientComponent accepts and renders a prop children, which is intended to be a JSX node in this case.

In the home page, you can pass the serverComponent to clientComponent as its children like this:

app/page.jsx

jsx
1import ServerComponent from "@/components/serverComponent";
2import ClientComponent from "@/components/clientComponent";
3
4export default async function Home() {
5  return (
6    <ClientComponent>
7      <ServerComponent />
8    </ClientComponent>
9  );
10}

Of course, even though this works, this method is still not recommended. As we've discussed above, the client components should be the "leaves" in the component tree. Whenever you find yourself writing this type of code, think about moving the client component further down the tree.

You can pass props from server component to client component

And lastly, data can flow from server to client component via props:

components/clientComponent.jsx

jsx
1"use client";
2
3export default function ClientComponent({ data }) {
4  return <div>{data}</div>;
5}

components/serverComponent.jsx

jsx
1import ClientComponent from "./clientComponent";
2
3export default function ServerComponent() {
4  return (
5    <>
6      <div>This is a server component.</div>
7      <ClientComponent data={"This is a client component."} />
8    </>
9  );
10}