ProNextJS
    Loading
    lesson

    Composition in Next.js: Understanding Client and Server Components

    Jack HerringtonJack Herrington

    As we were adding authorization to our application, one thing we did was wrap the application in SessionProvider, which is a client component. Since this was at the top level of our layout, you might wonder why that didn't turn the whole application into client components. The answer lies in composition.

    Client Components Can't Invoke Async Server Components Directly

    When working with client and server components in Next.js, one important rule to keep in mind is that client components cannot directly invoke asynchronous server components.

    Trying to render a server component directly inside a client component will result in an error.

    error when running a server component inside of a client component

    However, there's a way to use server components within client components through composition.

    Composing Server Components in Client Components

    Instead of invoking server components directly, you can pass them as children to a client component:

    <ClientComponent>
      <ServerComponent />
    </ClientComponent>
    

    This allows the client component to contain the server component without directly invoking it.

    By passing the server component as a child to the client component, we can successfully compose them together. The server component remains a server component, while the client component acts as a container.

    This is the same idea we saw with the SessionProvider component. By wrapping the application with SessionProvider, you don't automatically turn everything inside it into client components. Instead, the client component can contain server components through composition.

    Keep in mind, while client components can contain server components through composition, but they can't invoke server components directly.

    Some developers have coined the term "donut components" to describe client components that accept component children. These components have a "hole" in them where you can place either server or client components. Check out Maxi Ferreria's "Delicious Donut Components" article for more on this concept.

    Flexibility in Composition

    Composition in Next.js is not limited to the children prop. You can use any prop that accepts React nodes or JSX elements to achieve composition.

    For example, you could pass a ServerComponent to a content prop:

    <ClientComponent content={<ServerComponent />}/>
    

    However, as we saw earlier you can't provide a function that returns a React node directly as a prop:

    error when passing a function

    Promoting Components to Client Components

    Interestingly, you don't always need to explicitly mark a component as a client component using the 'use client' directive. When a client component invokes another component, that component automatically becomes a client component.

    For example, we can create a Contatiner component in a file called Container.tsx:

    export default function Container({ children }: { children: React.ReactNode }) {
      console.log("Container render");
      return (
        <div className="border-2 rounded-xl border-red-50">
          {children}
        </div>
      )
    }
    

    Importing the Container into the ClientComponent will promote it to a client component:

    // inside ClientComponent.tsx
    
    <Container>{content}</Container>
    
    the red border renders

    In this case, there is a red border around the ServerComponent, but we're still getting renders on the client. We didn't need to explicitly mark Container as a client component with use client because it was invoked by ClientComponent.

    Using Hooks in Promoted Client Components

    When a component is promoted to a client component, you can use hooks inside it without explicitly marking it with 'use client'.

    Bringing useState into the Container component, we could toggle the visibility of the children based on the state without any issues.

    The Container component uses the useState hook to manage the visibility state, and it works as expected without the need for the 'use client' directive.

    The Component Tree

    Let's review the component tree to understand what is happening here.

    We start off with the Page component, and inside that is a ClientComponent. Inside that is the Container, and then inside of that is the ServerComponent.

    Only the ClientComponent is explicitly marked as a client component, but the use client marker is essentially created a zone inside of ClientComponent where any component it invokes is promoted to a client component.

    The component tree

    The ServerComponent remains a server component since it's passed as a child to Container.

    It's important to note that if you try to use Container directly inside Page and have Container use its own server component, it will result in an error. Container needs to be explicitly marked as a client component if it uses hooks and is invoked outside of a client component.

    Understanding the relationship between client and server components and how to use composition techniques like "donut components" will make you more effective at building Next.js applications.

    Remember, client components can contain server components through composition, but they cannot directly invoke server components.

    Transcript