I come from a background of developing Single-Page-Applications in React. When first starting to learn NextJS 13 I had difficulties wrapping my head around the concept of client and server components. Intuitively I thought that server components are only rendered on the server and client components only in the browser. Reading through lots of questions on StackOverflow and Reddit, I got the feeling that I was not the only one having this misconception. So this is why I did some research and testing on my own in order to clear things up for everyone.
After reading this article I hope you will have a better understanding of server and client components and see that client components are also rendered on the server. So let's dive right into it!
In this tutorial, we are going to use the Emotion CSS-in-JS library to demonstrate the behavior of client components. The emotion "styled" function for creating styled components uses React's context API internally to make a theme object available to every component for styling purposes. This inherently makes all Emotion components a NextJS client component. We are using Emotion in this tutorial but most, if not all of the concepts shown in this tutorial, will apply to every client component, regardless of whether they are created by Emotion or not.
What you should be familiar with to follow this tutorial:
NextJS
Usage of styled components using the Emotion library (optional)
Typescript (optional)
There are two ways to define a client component in NextJS 13 - explicitly or implicitly.
Explicitly
You can explicitly tell Next to create a client component by including the "use client" directive at the top of your component. See this example from the official documentation:
Implicitly
In version 13 of Next, the framework tries to determine automatically if a component should be a client or server component. If you reference browser-specific data like the "window" object (which is not available on the server), or React-specific APIs like "useState, "useEffect" or "useContext", NextJS will throw an error and let you know that the component has to include the "use client" directive. This error is not displayed if the component you are creating is wrapped in a client parent component as children of client components are by default also of type client.
I went ahead and created a new Next 13 project with Typescript and the app router. I stripped out the default page and styling and installed the dependencies "@emotion/styled" and "@emotion/react".
I then created a ThemeProvider with a basic theme and put it in the "RootLayout" in "layout.tsx". As the ThemeProvider is using the React context API under the hood, the layout also needs to be declared as a client component using the "use client" directive at the top of the file.
Note: You can also keep the layout component as a server component (which it is by default) if you import and export the Emotion ThemeProvider in a separate file and mark it as a client component itself.
"use client";
import { ThemeProvider } from "@emotion/react";
const theme = {
colors: {
primary: "orange",
secondary: "gray",
},
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body>
<ThemeProvider theme={theme}>{children}</ThemeProvider>
</body>
</html>
);
}
The client component we want to render for demonstration purposes is a styled button. In Emotion it is created like this:
"use client";
import styled from "@emotion/styled";
interface ButtonProps {
version: "primary" | "secondary";
}
const Button = styled.button<ButtonProps>`
padding: 7px;
border: none;
border-radius: 5px;
color: white;
background-color: ${({ theme, version }) => theme.colors[version]};
`;
export default Button;
It takes the version (primary or secondary) as a prop and sets the background color coming from the ThemeProvider in the "layout.tsx" component. As it is using the "styled" function from Emotion, which is (again) depending on the context API, we also have to mark it as a client component.
When using the Theme inside of the styled function, Typescript does not know the type of the theme object passed to the ThemeProvide. So to get autocomplete working and fix Typescript issues when accessing the theme inside the "Button.tsx" file, you have to declare the type of your theme in an "emotion.d.ts" file. It should look something like this:
import "@emotion/react";
declare module "@emotion/react" {
export interface Theme {
colors: {
primary: string;
secondary: string;
};
}
}
To show the client component "Button.tsx", include it in the home page. I also added a click handler that alerts a message. This will be important for the explanation of the rendering of client components later.
"use client";
import Button from "./components/Button";
export default function Home() {
return (
<div>
<h1>Next13 client component rendering</h1>
<Button
version={"primary"}
onClick={() => alert("Hello Client Component")}
>
Styled Button
</Button>
</div>
);
}
This is my file structure for reference. Note that your project structure might not include the optional source directory, but that is fine.
If you start the dev server using "npm run dev", the result should be something like this:
Don't believe me? Let's have a look at the browser's network tab to find out what is really happening.
The network tab shows that on the initial page load an html document is sent from the server to the client. If we preview the response of this document, we see that the document contains the fully styled button component that we created as a client component.
So the client component really has been pre-rendered on the server side! But where do the styles come from? For that we can examine the raw code of the HTML document from the network response.
The HTML contains a style tag with the CSS that we applied on the button component. So it seems like they are automatically injected into the body and when the component is loaded, it already has the correct styles applied. What is missing though is the click handler with the alert message that we set earlier. What we can also see is that chunks of a client Javascript bundle are loaded from within the body tag of the HTML. These are there for hydration purposes.
Client components in Next 13 are actually rendered both on the server and client side. But there is a differentiation between the initial page load and subsequent nagivation on the client. To understand this better, we can consult the official NextJS documentation for client component rendering. It states:
"In Next.js, Client Components are rendered differently depending on whether the request is part of a full page load (an initial visit to your application or a page reload triggered by a browser refresh) or a subsequent navigation.
Full page load
To optimize the initial page load, Next.js will use React's APIs to render a static HTML preview on the server for both Client and Server Components. This means, when the user first visits your application, they will see the content of the page immediately, without having to wait for the client to download, parse, and execute the Client Component JavaScript bundle."
This means that what we saw in the demonstration is actually aligned with what we can read in the docs. The fact that we cannot see our click handler in the HTML output is due to this click handler being part of a client side Javascript bundle sent over to the browser, alongside with the statically generated HTML page. So when the initial HTML page is loaded, NextJS hydration takes place and registers our click handler back with our button component.
If you were wondering...Client component code like hooks can actually be called on the server side. If you try logging the initial state created with a useState hook, this actually works. It will console log the correct value if the value can be determined on the server already. But this code will be extracted from the JSX of the component and sent separately to the browser within a Javascript file.
Server components on the other hand can never have click handlers or call React hooks, etc. This is why they are excluded from the hydration process on the client side.
Client components are actually rendered on the server for initial page loads and are hydrated on the client using Javascript files sent alongside with the statically generated HTML of the page. On subsequent navigation (without page reload) the client component will be fully handled in the browser.
Thank you very much for reading through this tutorial. I hope you now have a better understanding of how client components in NextJS 13 work. If you liked this tutorial, I would be happy about feedback. If you encounter errors, feel free to reach out to me.
Happy coding! :)
Chris