Integrating Client and Server Components in Next.js

Published on
Integrating Client and Server Components in Next.js

Integrating Server Components within Client Components

Introduction

Client Components in Next.js enable interactive UIs that are server-pre-rendered but run client-side JavaScript in the browser. They offer interactivity through state, effects, and event listeners, allowing dynamic UI updates and immediate user feedback.

To utilize Client Components, the React "use client" directiveExternal Link is placed at the top of a file, establishing a boundary between server and client module components.

Imported modules, including child components, are treated as part of the client bundle (client components).

Note: Once "use client" has been used in the parent component, it doesn't need to be defined in every child component that needs to be rendered on the client.

Understanding the Challenge

Server Components are always rendered first during a request, so directly importing Server Components into Client Components is unsupported because it would require additional server requests.

However there is a method we can integrate server components within client components. We can instead, pass Server Components as props to Client Components.

Let us see an example:

// Tip: This will be a child client component and does not need the "use client" directive at the top level
import { useState } from "react";

export default function Counter() {
  const [count, setCount] = useState(0);

  return <button onClick={() => setCount((prev) => prev + 1)}>Count: {count}</button>;
}
app/components/counter.tsx

We use the React children as props to create a "slot" in our Client Component. This will allow us to render a child that is a server component.

Below, <ClientComponent> accepts a children prop:

"use client";

import Counter from "./counter";

export default function ClientComponent({ children }: { children: React.ReactNode }) {
  return (
    <>
      <h1>Client Component</h1>
      <Counter />
      {children}
    </>
  );
}
app/components/client-component.tsx

Next we can have a server component like this one:

import React from "react";

function ServerTitle() {
  return <div>Behold!, I am a title rendered in the server!</div>;
}

export default ServerTitle;
app/components/server-title.tsx

<ClientComponent> does not know that children will eventually be filled in by the result of a Server Component. The only responsibility <ClientComponent> has is to decide where children will eventually be placed.

Everything put together will look like this:

import ClientComponent from "./components/client-component";
import ServerTitle from "./components/server-title"; // Server component

export default async function Home() {
  return (
    <ClientComponent>
      <ServerTitle />
    </ClientComponent>
  );
}
app/page.tsx

This pattern can be usefull if we have components that we know can be rendered on the server.

For example wrapping the application in context provider or if we want to include something common like themes in our application:

"use client";

import { ThemeProvider as NextThemesProvider } from "next-themes";

function ThemeProvider({ children }: { children: React.ReactNode }) {
  return (
    <NextThemesProvider attribute="class" defaultTheme="system" enableSystem>
      {children}
    </NextThemesProvider>
  );
}

export default ThemeProvider;
app/components/theme-provider.tsx
import ThemeProvider from "./components/theme-provider";

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <html lang="en" suppressHydrationWarning>
      <body>
        <ThemeProvider>{children}</ThemeProvider>
      </body>
    </html>
  );
}
app/layout.tsx

Coming Soon: themes guide

Conclusion

Not all files require the "use client" directive. Everything imported after setting that boundary will be considered a client component. However, it is still possible to pre-render children on the server.