DEV Community

Vishal Yadav
Vishal Yadav

Posted on

Mastering Client and Server Components in Next.js: A Comprehensive Guide

Deploying interactivity in a Next.js application can be straightforward, but it often comes with pitfalls. This guide will walk you through the common mistakes and best practices for handling client and server components effectively.

Introduction

Next.js is a powerful framework for building server-rendered React applications, but it introduces a concept that can be tricky for newcomers: the distinction between client and server components. Understanding when and how to use these components is crucial for optimizing performance and user experience.

Client vs. Server Components

The Basics

  • Client Components: Handle client-side interactivity and are rendered in the browser.
  • Server Components: Render on the server and come with several benefits, including better performance and security.

Common Mistake: Converting Entire Pages to Client Components

When you add client-side interactivity to a component, such as an onClick event on a button, you might be tempted to convert the entire page into a client component. This approach works but negates the benefits of server components.

Example Scenario

Let's consider a simple example: a page with an H1 element and a Button component.

// pages/index.js
export default function HomePage() {
  return (
    <div>
      <h1>Hello, World!</h1>
      <Button />
    </div>
  );
}

// components/Button.js
export default function Button() {
  return (
    <button onClick={() => console.log('Hello, World!')}>Click Me</button>
  );
}
Enter fullscreen mode Exit fullscreen mode

By default, everything in the app directory in Next.js is a server component. Attempting to add client-side interactivity directly will result in an error.

The Wrong Approach

A common mistake is to add the use client directive at the top of the page component, converting the entire page into a client component.

// pages/index.js
'use client';

export default function HomePage() {
  return (
    <div>
      <h1>Hello, World!</h1>
      <Button />
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

While this removes the error, it also converts every component imported into this page into client components, including components that don't need client-side interactivity.

The Right Approach

Instead, add the use client directive only to the components that require client-side interactivity.

// components/Button.js
'use client';

export default function Button() {
  return (
    <button onClick={() => console.log('Hello, World!')}>Click Me</button>
  );
}
Enter fullscreen mode Exit fullscreen mode

This way, the HomePage component remains a server component, preserving the benefits of server-side rendering, while the Button component handles the client-side interactivity.

Benefits of Server Components

  1. Data Fetching: Server components can fetch data closer to the source, improving performance.
  2. Backend Access: Directly access backend resources like databases, keeping sensitive information secure.
  3. Dependency Management: Keep large dependencies on the server to avoid bloating client-side bundles.

Example: Third-Party Libraries

Consider a Post component that uses a third-party library like sanitize-html.

// components/Post.js
import sanitizeHtml from 'sanitize-html';

export default function Post({ content }) {
  const cleanContent = sanitizeHtml(content);
  return <div dangerouslySetInnerHTML={{ __html: cleanContent }} />;
}
Enter fullscreen mode Exit fullscreen mode

If Post is imported into a client component, the large sanitize-html library would be shipped to the client. By ensuring Post remains a server component, we keep the library on the server.

Structuring Your Application

Component Tree

Think of your React app as a tree of components, with the root component at the top.

  • Root Component: In Next.js, this is the layout component.
  • Pages: Various pages of your application, each potentially importing several components.

Client Components at the Edges

Only mark components as client components at the outer edges of the tree, i.e., the leaves. This minimizes the number of client components and maximizes the benefits of server components.

// components/Button.js
'use client';

export default function Button() {
  return (
    <button onClick={() => console.log('Hello, World!')}>Click Me</button>
  );
}

// pages/index.js
import Button from '../components/Button';

export default function HomePage() {
  return (
    <div>
      <h1>Hello, World!</h1>
      <Button />
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Handling Context and Providers

When using context providers or third-party libraries that wrap your application, such as a theme provider, it's crucial to understand their impact on client and server components.

// components/ThemeProvider.js
'use client';

export function ThemeProvider({ children }) {
  return <ThemeContext.Provider value={theme}>{children}</ThemeContext.Provider>;
}

// pages/_app.js
import { ThemeProvider } from '../components/ThemeProvider';

function MyApp({ Component, pageProps }) {
  return (
    <ThemeProvider>
      <Component {...pageProps} />
    </ThemeProvider>
  );
}

export default MyApp;
Enter fullscreen mode Exit fullscreen mode

Important Note

A provider marked as a client component does not convert its children to client components as long as it passes them through using the children pattern.

Conclusion

Mastering the use of client and server components in Next.js requires an understanding of their differences and benefits. By following best practices and avoiding common pitfalls, you can build highly efficient and performant applications.

Further Learning

If you're diving into React and Next.js, ensure you have a solid grasp of JavaScript and CSS fundamentals. These are the building blocks that will make your journey with these frameworks smoother.

Top comments (0)