Misplacing `'use client'` Can Double Your Next.js Bundle — 4 Common Mistakes in App Router
When you first adopt the Next.js App Router, almost everyone runs into a similar situation. You try to use useState, get an error saying "you can't do that in a Server Component," and in a hurry you just slap 'use client' at the top of the file and move on. I did exactly that in the beginning.
The problem is that the outcome changes dramatically depending on where that one line is placed. Declaring 'use client' incorrectly in layout.tsx and then properly fixing the boundary can shrink the client bundle from 340kB down to 67kB — that kind of change actually happens. It can also plant hydration errors or even become a ticking time bomb that exposes server-only code to the client.
This post is for those who are just getting started with the Next.js App Router, or who are using it but still feel a bit uncertain about exactly how 'use client' works. We'll look at the 4 most common mistake patterns encountered in real-world projects, what results they actually produce, and why each one leads to that outcome.
Core Concepts
'use client' is a boundary declaration, not a "client component marker"
Many people understand 'use client' as roughly meaning "this component runs on the client." In reality, it's a bit different. This directive declares the boundary between the server component module graph and the client component module graph. The moment you declare it in a file, every module imported from that file gets pulled into the client bundle wholesale.
Key principle: You don't need to put
'use client'on every client component. You only need to declare it at the entry point where you cross from the server component tree into a client component. Components imported below that point automatically become client components.
Two more rules worth keeping in mind:
- Server components can import and render client components. The restriction goes the other way. A client component cannot directly import and render a server component. However, passing one via
childrenprop or other composition patterns is possible. - Props that cross the server→client boundary must be serializable. Plain functions, class instances,
Dateobjects, etc. cannot be passed across.
Where does a bad boundary hurt?
| Area | Result |
|---|---|
| Bundle size | Unnecessary static content gets included in client JS |
| Hydration | Errors from mismatches between server and client render output |
| Streaming | Progressive rendering — where the server streams HTML as sections become ready — is disabled |
| Security | Risk of DB access code, API keys, etc. being exposed to the client |
| SEO | Reduced server-rendering benefits lower crawler accessibility |
Practical Examples
Example 1: Declaring 'use client' at the top of layout.tsx
This is the highest-impact mistake. It usually happens something like this: you want to detect the current route and apply an active style to navigation, you want to use usePathname directly in the layout file, so you add 'use client'. The error goes away, it works — but the entire app gets included in the client bundle.
// ❌ Pattern you should never do
// app/layout.tsx
'use client'
import { usePathname } from 'next/navigation'
export default function RootLayout({ children }: { children: React.ReactNode }) {
const pathname = usePathname()
return (
<html>
<body>
<nav>
<a href="/" className={pathname === '/' ? 'active' : ''}>Home</a>
<a href="/about" className={pathname === '/about' ? 'active' : ''}>About</a>
</nav>
{children}
</body>
</html>
)
}// ✅ Correct pattern — extract only the component that needs interactivity
// components/NavLinks.tsx
'use client'
import { usePathname } from 'next/navigation'
export function NavLinks() {
const pathname = usePathname()
return (
<nav>
<a href="/" className={pathname === '/' ? 'active' : ''}>Home</a>
<a href="/about" className={pathname === '/about' ? 'active' : ''}>About</a>
</nav>
)
}
// app/layout.tsx — kept as a server component with no 'use client'
import { NavLinks } from '@/components/NavLinks'
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html>
<body>
<NavLinks />
{children}
</body>
</html>
)
}| Item | ❌ Declared in layout | ✅ Declared in leaf component |
|---|---|---|
| Client bundle size | Entire app included | Only that component included |
| Server rendering scope | None | Most of the tree |
| SEO | Significantly disadvantaged | Advantaged |
| TTFB (Time to First Byte) | Slow | Fast |
Example 2: A Context Provider causing hydration mismatches
This pattern "seems to work fine" and then blows up under specific conditions. A theme-based Context is a classic example.
// providers.tsx
'use client'
export function Providers({ children }: { children: React.ReactNode }) {
return (
<ThemeProvider> {/* Reads theme from localStorage */}
<AuthProvider>
{children}
</AuthProvider>
</ThemeProvider>
)
}
// app/layout.tsx
import { Providers } from './providers'
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html>
<body>
<Providers>{children}</Providers>
</body>
</html>
)
}Suppose ThemeProvider reads localStorage to determine the theme. The server has no access to localStorage, so it renders with the default value (light). When the browser reads localStorage and gets dark, the results differ and React throws a hydration error.
Hydration Mismatch: An error that occurs when the HTML generated by the server differs from what React renders on the client. It commonly appears when APIs inaccessible on the server — like
window,localStorage, ordocument— are used during the initial render.
// ✅ Apply theme after client mount using useEffect
'use client'
import { useEffect, useState } from 'react'
export function ThemeProvider({ children }: { children: React.ReactNode }) {
const [theme, setTheme] = useState('light') // Start with same default as server
useEffect(() => {
const saved = localStorage.getItem('theme') ?? 'light'
setTheme(saved)
}, [])
return <div data-theme={theme}>{children}</div>
}This resolves the hydration error. However, there is one tradeoff: if the server renders with light and then immediately switches to dark after mount, you may see a FOUC (Flash of Unstyled Content) where the screen flickers once. If you need to eliminate the flicker as well, it's worth looking into the suppressHydrationWarning attribute combined with direct HTML class manipulation patterns.
Example 3: Passing non-serializable props across the boundary
This is the situation where you try to pass a function as a prop from a server component to a client component and hit a runtime error. At first it's quite baffling — "why is there an error?" I spent a long time figuring out what this error message meant the first time I saw it.
// ❌ Runtime error
// app/page.tsx (server component)
import { ClientButton } from './ClientButton'
export default function ServerPage() {
const handleClick = () => console.log('clicked') // Functions are not serializable
return <ClientButton onClick={handleClick} />
}Props crossing the server→client boundary are serialized as JSON. Plain functions have no way to be serialized, so an error occurs. Date objects fail for the same reason — they are class instances, not plain objects, so information is lost during serialization. For Date, you can use the pattern of converting to a string before passing and restoring with new Date() on the client.
When you need to pass a function, use Server Actions ('use server'). A Server Action is transformed at build time into an HTTP endpoint reference rather than the actual function. From the client's perspective, it only receives "here's the address to call" — making it serializable.
// ✅ Pass functions safely with Server Actions
// app/actions.ts
'use server'
export async function handleClick() {
// This runs on the server — DB operations, external API calls, etc. are fine
console.log('running on server')
}
// app/page.tsx (server component)
import { ClientButton } from './ClientButton'
import { handleClick } from './actions'
export default function ServerPage() {
return <ClientButton onClick={handleClick} /> // Server Actions are serializable
}| Type | Can cross the boundary |
|---|---|
string, number, boolean |
✅ |
| Plain objects, arrays | ✅ |
Date (class instance) |
❌ → Convert to string first |
Map, Set |
❌ |
| Plain functions | ❌ |
Server Action ('use server') |
✅ |
| Class instances | ❌ |
Example 4: Declaring on one large component, turning the entire subtree into a client
This is the most frequently seen pattern in real projects and the hardest to spot. Honestly, it's natural to think "do I really need to split a file just for one button?" But the issue is that ProductInfo, ProductImage, and Reviews that come along with that button are larger than you'd expect.
// ❌ Entire page becomes client-side because of AddToCartButton
// app/product/[id]/page.tsx
'use client'
export default function ProductPage({ product }) {
return (
<div>
<ProductInfo product={product} /> {/* Static — doesn't need to be a client */}
<ProductImage src={product.image} /> {/* Static — doesn't need to be a client */}
<AddToCartButton id={product.id} /> {/* Only this needs to be a client */}
<Reviews reviews={product.reviews} /> {/* Static — doesn't need to be a client */}
</div>
)
}// ✅ Extract only the parts that need to be client-side
// components/AddToCartButton.tsx
'use client'
import { useState } from 'react'
export function AddToCartButton({ id }: { id: string }) {
const [added, setAdded] = useState(false)
return (
<button onClick={() => setAdded(true)}>
{added ? 'Added!' : 'Add to Cart'}
</button>
)
}
// app/product/[id]/page.tsx (server component — no 'use client')
import { AddToCartButton } from '@/components/AddToCartButton'
export default function ProductPage({ product }) {
return (
<div>
<ProductInfo product={product} />
<ProductImage src={product.image} />
<AddToCartButton id={product.id} /> {/* Client island */}
<Reviews reviews={product.reviews} />
</div>
)
}This pattern is sometimes called "Islands Architecture." The concept is to float only the parts that need interactivity as client islands on a static sea.
Pros and Cons Analysis
Advantages
| Item | Details |
|---|---|
| Minimized bundle size | Static content is delivered as HTML only, with no JS |
| Improved TTFB | The broader the server rendering scope, the faster the first byte response |
| Streaming enabled | Progressive rendering possible when combined with <Suspense> boundaries |
| SEO-friendly | Crawlers can access content without executing JS |
| Enhanced security | Server-only code does not leak to the client |
Disadvantages and Caveats
| Item | Details | Mitigation |
|---|---|---|
| Component splitting overhead | Each interactive element requires its own file | Define a *Client.tsx naming convention as a team standard |
| Initial design complexity | More thought required for tree structure design | List out interactive elements per page before designing |
| Serialization constraint learning curve | Takes time to get used to prop type restrictions | Use TypeScript plugin warnings as a guide |
| FOUC risk | Screen flicker when theme switches after client mount | Explore suppressHydrationWarning + direct HTML class manipulation pattern |
RSC Flight payload: The serialized data stream that server components deliver to the client. If boundary design is wrong, this payload becomes bloated and leads to hydration delays.
Most Common Real-World Mistakes
- Adding
'use client'to the top of a file when auseStateerror appears — the error is fixed, but the file becomes the boundary entry point and the entire subtree below becomes client-side. The correct approach is to extract the part that needs interactivity into a separate component. - Using client-side APIs in the initial render of a Context Provider — reading APIs inaccessible on the server like
localStorageorwindowoutside ofuseEffectcauses hydration mismatches. - Declaring
'use client'on a page component — even if only some elements are interactive, the entire page becomes client-side. It's better to extract only the components that need interactivity.
Closing Thoughts
Where you place the 'use client' boundary makes a much bigger difference than you might think. The closer you declare it to the smallest unit that actually needs interactivity, the more you gain on all three fronts simultaneously: bundle size, hydration, and streaming.
If you have a project in progress, here's a good sequence to follow:
- Visualize the current client bundle composition with
@next/bundle-analyzer. Afterpnpm add -D @next/bundle-analyzerand connecting it tonext.config.js, you can see at a glance which modules are included in the bundle. If you see an unexpectedly large area, the next steps become worthwhile. - List
'use client'declaration locations withgrep. Rungrep -r "'use client'" ./app --include="*.tsx"to check whether anylayout.tsxor page-level files have the declaration. If they do, they are candidates for splitting as shown in Examples 1 and 4. - Add the
server-onlypackage to server-only modules. Afterpnpm add server-only, addingimport 'server-only'to the top of files that handle DB access or environment variables will trigger a build-time error if that module gets imported on the client, preventing leakage proactively.
References
- Directives: use client | Next.js Official Docs
- Getting Started: Server and Client Components | Next.js
- 'use client' directive – React Official Docs
- 6 React Server Component performance pitfalls in Next.js – LogRocket Blog
- Server and Client Composition Patterns – Next.js Docs
- Props must be serializable for components in "use client" file – GitHub Discussion
- Intro to Performance of React Server Components – Web Performance Calendar
- Mastering 'use client' in Next.js – Medium