ProNextJS
    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

    As we were adding authorization to our application, one thing we did was wrap the application in a session provider, which is a client component, right? So why didn't that turn the whole application into client components, since we did that at the top level of our layout? Great question. To get to the bottom of that, let's talk about composition.

    And we'll start off with something that's really important that you need to know about client components. They can't invoke asynchronous server components directly. Let me demonstrate that. So we go back to our experimentation zone, and we have our client component. Let's just bring in server component here and see what happens.

    So I've got our async server component, let's just put it in here. Let's save. All right, so now we're getting our blow up on the client, and we see that we can't invoke a server component from a client component, but maybe we can contain server components.

    So instead of invoking server components directly, let's open up children, and then we'll simply drop the children down at the end here. Now over in our page, we can see the client component wants children, so let's go and

    add them. Now we're sending the rendered server component to the client component as children. Let's see if that worked. And it does.

    All right, I hope that little demonstration there dispels that second biggest misunderstanding that I see when it comes to client components and server components. Folks think that because a client component contains a server component, that that server component is coerced into a client component. And that's just not the case if you use composition.

    So that's the secret of why SessionProvider didn't turn everything inside of it into client components. Client components can contain server components through composition, they just can't invoke server components directly. Some folks have started calling client components that can take children donut components, because

    taking children opens a hole in the component that you can then fill with server components or client components, whichever you choose, through composition. Now I put a link to this donut component blog post in the instructions. It's a fantastic read, and it will really help you understand more about how to make use of composition.

    But in the meantime, let's dive a little bit deeper, because it's not just children that you can use for composition. So let's go back into our code, and I'm just going to change children to content.

    And now over here, I can specify content property, and then give it server component. And it works just the same. So what this means is that children itself is not a specially blessed property.

    It can be any property you want, or as many properties as you want, as long as they're taking React nodes or JSX elements, which is the rendered output of a component. What you can't do is a render function, like this.

    Again, we get that same thing that we did basically with the onClick handler. Now I'm going to geek out a little bit here. I am going to show you how to make this work, but this is a bit of a deep dive. So hold on to your hats. So instead of just giving it a regular function like this, we're going to give it an async

    function that returns a server component, and then we're going to declare it as a server function. Now back over here, content is no longer going to be a React node directly.

    It's going to be a function that returns a promise to give me a React node. And then we're going to invoke that function. Okay, cool. Now that doesn't work for the same reasons effectively that we had before when you try

    to invoke server component, but there is a way to make this work. You can wrap it in a suspense.

    And now that actually does work. Now I got to be honest with you, this is a fun thing to see, but I wouldn't do it in

    my application because what's actually happening is that the client component is getting run on the client. That's invoking the server action as part of that suspense. That's sending back some virtual DOM from that server function, and then we're rendering it, which is super cool, but there's going to be a delay in the client and I really wouldn't

    do this unless I absolutely had to. All right, so let's back that out. Now one last thing I want to cover here is the whole concept of a client component, because as it turns out, you don't actually always have to say, "Use client to make client components." Let me show you.

    So I'm going to make a new component called container, and notice it's not specifically marked as a client component. Now let's import it into our client component, and now we'll wrap the content in it.

    And now we can see over in the browser, we have our dotted container component around our server component, and we're getting renders on the client. So even though we haven't said that container is a client component, it gets promoted to being a client component because it was invoked by a client component.

    Now you might be like, "Hmm, well, what about adding a hook? If we add a hook to it, would that mean that it has to be marked as a client component?" Well, let's give it a try. So let's bring in new state, and then we'll have a toggle for showing and hiding.

    Then we'll wrap this whole thing in a div, we'll put a button in here to flip the toggle, and then we'll only show or hide the children based on that toggle. All right, so far so good, let's give it a try.

    So now we've got our toggle show, we can show and hide, super cool, but we haven't marked it as a client component. So what's important to understand here is that not all components that use hooks need to be marked specifically as client components using the use client directive.

    So we start off with the page component, and inside that we have our client component, and inside that we have our container, and then inside that we have the server component. That's the tree in our application. Now only client component is marked using use client, but what's really happening is

    that that use client marker is creating essentially a zone inside of client component where whatever client component is invoking, assuming it can invoke it, that becomes a client component. That's how container gets upscaled into being a client component.

    But server component remains a server component because server component was actually invoked by page and then given to the client component as a child or content. Now let's say that we want to extend page to go and have it use container and within container its own server component, well, that's not going to work because container

    isn't specifically tagged as a client component and it uses hooks. So that's going to blow up. Well, I certainly hope that going through this exercise helps you understand a lot more about the relationship between client and server components, what server components can do well, what client components can do well, because we are going to be using them extensively through this course.