We're going to be adding autorization to our application, which will involve adding a sign-in button to the page.
With the original Next.js Pages Router, you could add a sign-in button and an onClick
handler to handle the sign-in process. However, in the App Router, if you want a component to be interactive on the client, it needs to be a client component.
In order to understand the differences between client and server components, we'll start by building a small Next.js test application to see how they work in isolation. Then, we'll integrate them into our application.
Setting up the Next.js Application
To get started, create a new Next.js application using the following command:
pnpm dlx create-next-app@latest --use-pnpm
Choose "Yes" for every option presented as before, including Tailwind for styling.
Once the setup is complete, open the project in your preferred code editor.
Creating Server and Client Components
Replace the code in the Home
component inside of the page.tsx
file with some Tailwind classes to center the content:
export default function Home() {
return <main className="mt-5 max-w-xl mx-auto"></main>;
}
Inside the src
folder, create two new files: ServerComponent.tsx
and ClientComponent.tsx
.
Inside of ServerComponent.tsx
, create a server component that displays the text "Server Component":
export default function ServerComponent() {
return (
<div className="my-5">
<h1 className="font-bold text-2xl">Server Component</h1>
</div>
);
}
Similarly, in ClientComponent.tsx
, create a client component that displays the text "Client Component":
export default function ClientComponent() {
return (
<div className="my-5">
<h1 className="font-bold text-2xl">Client Component</h1>
</div>
);
}
Import these components into page.tsx
and use them side by side:
import ServerComponent from './ServerComponent';
import ClientComponent from './ClientComponent';
export default function Home() {
return (
<main className="mt-5 max-w-xl mx-auto">
<ServerComponent />
<ClientComponent />
</main>
);
}
Now in the browser we can see both components side by side:
Determining Where Components Render
To determine where the components are rendering, add console.log
statements to each component:
// ServerComponent.tsx
export default function ServerComponent() {
console.log('ServerComponent');
return (
...
// ClientComponent.tsx
export default function ClientComponent() {
console.log('ClientComponent');
return (
...
After saving the files, you'll see the console logs in the terminal, indicating that the components are rendering on the server.
Client-side console logs will appear in both the terminal and in the browser's console. However, the browser console is currently empty.
To turn the ClientComponent
into an actual client component, add 'use client'
at the top of the file:
'use client';
export default function ClientComponent() {
console.log('ClientComponent');
return (
...
After a full refresh, you'll notice that the ClientComponent
now renders on both the server and the client:
Interactivity and Hooks in Client Components
Another key difference between client and server components is that only client components can use hooks.
In ClientComponent.tsx
, import useState
from React and create a counter state, and everything will work as expected.
"use strict";
import { useState } from "react";
export default function ClientComponent() {
const [counter, setCounter] = useState(0);
return (
<div className="my-5">
<h1 className="font-bold text-2xl">Client Component</h1>
<p>Counter: {counter}</p>
<button
className="bg-blue-500 hover:bg-blue-700 text-white font-bold"
onClick={() => setCounter(counter + 1)}
>
Increment
</button>
</div>
);
}
However, attempting to use useState
in the server component will result in an error indicating that hooks can only be used in client components:
Server Lifecycle for Client Components
Although client components render on the server, effects don't run on the server.
To demonstrate, import useEffect
and useLayoutEffect
in ClientComponent.tsx
and add console logs inside them:
'use client';
import { useEffect, useLayoutEffect } from 'react';
export default function ClientComponent() {
useEffect(() => {
console.log('ClientComponent effect');
}, []);
useLayoutEffect(() => {
console.log('ClientComponent layout effect');
}, []);
return (
...
In the browser, you'll see the effect logs, possibly twice due to strict mode in development.
However, the server console will not display these logs, confirming that effects do not run on the server.
This behavior was the same in the Pages Router, but it's important to understand that if you rely on useEffect
to load your data, it won't run on the server.
Advantages of Server Components
Server components offer several advantages over just using client components:
- Reduced Bundle Size: Server component code is not sent to the client, reducing the bundle size and speeding up the application.
- Security: Server component code only runs on the server, preventing leakage of secrets to the client.
- Data Loading: Server components make it easy to load data from backend services.
Loading Data in Server Components
To demonstrate data loading in server components, we'll use a fake data service called reqres.in
. Update the server component to make an async request to the API endpoint:
export default async function ServerComponent() {
const req = await fetch("https://reqres.in/api/users/2");
const { data } = await req.json();
return (
<div className="my-5">
<h1 className="font-bold text-2xl">Server Component</h1>
<div>
{`${data.first_name} ${data.last_name}`}
</div>
</div>
);
}
Now the server component will display the name of the user fetched from the API:
Attempting to do the same in a client component would require some workarounds since they aren't async. We would have to use promises or async functions inside of useEffect
.
Loading data with RSCs is great since it makes it easy to call backend services and render the result. This is often faster since you're likely to be talking to resources in the same cluster as your server rather than over the open internet.
However, just as RSCs don't support hooks, they also don't support Context. If you try to use context in a server component, you'll get an error. It's important you don't try to work around this, because Next.js does heavy caching of rendered components. Also, because of Suspense and async components, multiple requests can be handled simulataneously which could potentially cause data bleed.
Sending Data from Server to Client Components
Server components can send data to client components. To demonstrate we'll update page.tsx
's Home
component to make the same fetch request as in the server component. Note that Next will see we're making the same request and optimize it to only make one request.
Pass the fetched data as a name
prop to the client component:
export default async function Home() {
const res = await fetch('https://reqres.in/api/users/2');
const { data } = await res.json();
return (
<main className="mt-5 max-w-xl mx-auto">
<ServerComponent />
<ClientComponent name={`${data.first_name} ${data.last_name}`} />
</div>
);
}
Now, we'll update the client component to accept the name
prop and display it:
'use client';
export default function ClientComponent({ name }) {
// rest of component as before
<h1>ClientComponent</h1>
<div>{name}</div>
Now the name will show in both places:
Server components can send strings, numbers, booleans, dates, arrays, and objects to client components, but they cannot send functions.
Trying to pass an onClick
handler prop to the ClientComponent
will result in an error:
<ClientComponent
onClick={() => console.log('clicked')} // this will error!
name={`${data.first_name} ${data.last_name}`}
/>
This error should conceptually make sense, because clicking something on the client isn't going to trigger a server-side function unless you specifically wire up a server action. We'll discuss that more later on.
Now that you have an idea for the difference between client and server components, let's move on to adding authorization to our app.
With this knowledge, you can now proceed to add authorization to your application, leveraging the strengths of both client and server components.