Skip to content
Felipe Reducino
All posts

Stop putting "use client" in your page.tsx

Why your route entry points should stay Server Components, and how to move interactivity into small *-client.tsx files instead.

Felipe Reducino

Did you know?

OK, maybe you already know this. In Next.js, some files are special. By special I mean that their names are reserved by the framework for specific features, like layout.tsx and page.tsx, among others. Today we'll stick to just those two. By default, both files are Server Components. Yet the anti-pattern I've run into most in production projects is layouts and pages marked with "use client". And here's the key point: not everything that needs interactivity has to turn the route's entry point into a Client Component.

The problem

When you add "use client" to a page.tsx or layout.tsx, you're basically saying: "Everything in here, and everything it imports, is now part of the client bundle." In practice, this can lead to:

  1. More JavaScript shipped to the browser

  2. Higher hydration cost

  3. Mixed responsibilities (server and client code in the same place)

  4. Data fetching on the client when it doesn't need to be there

  5. Code that's harder to maintain and evolve

And the most important part: it usually happens for a simple reason. A small piece of the screen needs state or an event handler.

The right (and more scalable) approach

Instead of turning the whole page.tsx into a Client Component, you can do something much simpler and more efficient: separate responsibilities. Keep page.tsx on the server, where it fetches data and composes the route:

// app/posts/[slug]/page.tsx
import { getPost } from "@/lib/posts";
import { LikeButton } from "./like-button-client";

export default async function Page({ params }: { params: Promise<{ slug: string }> }) {
  const { slug } = await params;
  const post = await getPost(slug);

  return (
    <article>
      <h1>{post.title}</h1>
      <p>{post.body}</p>
      <LikeButton postId={post.id} initialLikes={post.likes} />
    </article>
  );
}

And move only the interactive piece into its own Client Component:

// app/posts/[slug]/like-button-client.tsx
"use client";

import { useState } from "react";

export function LikeButton({ postId, initialLikes }: { postId: string; initialLikes: number }) {
  const [likes, setLikes] = useState(initialLikes);

  return (
    <button onClick={() => setLikes((n) => n + 1)}>
      ♥ {likes}
    </button>
  );
}

Now the title and body render on the server and ship zero JavaScript. Only the button is hydrated. I like this approach because it's very easy to apply when refactoring a large codebase that's already running in production, and the gains can be significant!


Summary

  • page.tsx → route orchestration (server component by default)

  • *-client.tsx → interactivity

  • layout.tsx → shared structure (server component by default)

  • providers.tsx, AuthProvider.tsx, etc. → global state managers that need to run on the client