ProNextJS
    Loading
    lesson

    Client vs. Server Components in Next.js

    Jack HerringtonJack Herrington

    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.

    the new app menu

    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:

    The components are rendering

    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.

    Console logs in the terminal

    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:

    ClientComponent logged in the browser console

    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:

    Error when using hooks in a server component

    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.

    The effects log in the console

    However, the server console will not display these logs, confirming that effects do not run on the server.

    No effect logs in the terminal

    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:

    the fetched name appears

    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:

    the name shows 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}`}
    />
    
    onClick Error

    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.

    Transcript