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.
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:
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>
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 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.