Skip to content
ExponaBLOG
REACT & NEXT.JS · DAY 024 / 100

Where to put 'use client' for React & Next.js developers

Draw the boundary as low as you can.

Expona Admin7 min read

One of the most common gotchas when adopting the App Router in Next.js 13+ is where to put 'use client'. You draw a boundary, and suddenly everything imported into that file becomes client code—including dependencies you never intended to bundle on the browser. In this article, we'll walk through how to place the directive strategically so you keep server rendering where it belongs and only opt in to the client where you actually need interactivity.

The directive marks a boundary — Everything imported into a client component becomes client code

When you add 'use client' at the top of a file, you're telling Next.js: "Render this component and everything it imports on the client." That includes utility functions, constants, third-party libraries—everything. This is powerful but also dangerous if you're not intentional.

Consider a dashboard page that imports a heavy data-processing library just to format one column of a table. If you mark the entire page as a client component, that library ships to the browser even though it could run once on the server.

tsx
// pages/dashboard.tsx
// ❌ The whole tree is client code now
'use client'

import { expensiveDataLibrary } from 'heavy-lib' // 500KB
import { formatMetrics } from './utils' // Uses the library
import React from 'react'

export default function Dashboard() {
  const data = formatMetrics(/* ... */)
  return (
    <>
      <Header />
      <MetricsTable data={data} />
      <InteractiveChart />
    </>
  )
}

Now expensiveDataLibrary is part of your client bundle. You've crossed the boundary too high.

Push it down — Make only the interactive button a client component

The real strategy is to identify the smallest component that actually needs interactivity, then place 'use client' there. Everything else stays on the server.

In the dashboard example above, only the InteractiveChart component has click handlers and state. So let's push 'use client' down to just that component:

tsx
// app/dashboard/page.tsx
// ✅ Server component (default in App Router)
import { expensiveDataLibrary } from 'heavy-lib'
import { formatMetrics } from './utils'
import InteractiveChart from './InteractiveChart'
import MetricsTable from './MetricsTable'

export default async function Dashboard() {
  const data = formatMetrics(/* ... */)
  
  return (
    <>
      <Header />
      <MetricsTable data={data} />
      <InteractiveChart initialData={data} />
    </>
  )
}

// app/dashboard/InteractiveChart.tsx
'use client'

import React, { useState } from 'react'

export default function InteractiveChart({ initialData }) {
  const [filter, setFilter] = useState('')
  
  return (
    <div>
      <input
        value={filter}
        onChange={(e) => setFilter(e.target.value)}
        placeholder="Filter..."
      />
      {/* Render chart based on filter */}
    </div>
  )
}

Now the heavy library stays on the server. Only the chart component—the part with the useState hook—is client code. Your bundle is smaller, and the server does what it's good at.

Pass Server Components as children — A client wrapper can render server-rendered children

Here's a pattern many developers miss: a client component can accept server components as children via props. This is powerful because it lets you keep layout or wrapper logic on the client while delegating expensive rendering back to the server.

Imagine a tabbed interface where the tab switcher is interactive (client), but each tab panel displays a large dataset that you want fetched and rendered on the server:

tsx
// app/products/ClientTabs.tsx
'use client'

import React, { useState } from 'react'

export default function ClientTabs({ children }) {
  const [activeTab, setActiveTab] = useState(0)
  
  return (
    <div>
      <div className="tabs">
        <button onClick={() => setActiveTab(0)}>All</button>
        <button onClick={() => setActiveTab(1)}>Featured</button>
        <button onClick={() => setActiveTab(2)}>Sale</button>
      </div>
      <div className="tab-content">
        {/* Render the active child */}
        {Array.isArray(children) ? children[activeTab] : children}
      </div>
    </>
  )
}

// app/products/page.tsx
// Server component
import ClientTabs from './ClientTabs'
import AllProductsList from './AllProductsList'
import FeaturedProductsList from './FeaturedProductsList'
import SaleProductsList from './SaleProductsList'

export default function ProductsPage() {
  return (
    <ClientTabs>
      <AllProductsList />
      <FeaturedProductsList />
      <SaleProductsList />
    </ClientTabs>
  )
}

// app/products/AllProductsList.tsx
// Server component — can use async/await, databases, secrets
import { db } from '@/lib/db'

export default async function AllProductsList() {
  const products = await db.products.findAll()
  
  return (
    <ul>
      {products.map((p) => (
        <li key={p.id}>{p.name}</li>
      ))}
    </ul>
  )
}

Notice: ClientTabs is client code (it manages state and event handlers), but AllProductsList, FeaturedProductsList, and SaleProductsList are server components. They can fetch from the database, read environment variables, and render HTML. When you switch tabs on the client, the UI updates, but the actual product data was fetched and rendered on the server where it's safe.

This pattern is often better than fetching all tabs' data upfront or passing serialized data as props.

Props must be serializable — No functions or class instances from server to client (except Server Actions)

When you pass props from a server component to a client component, those props must be serializable to JSON. This makes sense: they have to cross the boundary from the server bundle to the browser.

You cannot pass:

  • Functions (unless they're Server Actions)
  • Class instances
  • Symbols
  • Closures

Here's what breaks:

tsx
// app/example/page.tsx
import ClientButton from './ClientButton'

export default function Page() {
  const handleClick = () => {
    console.log('clicked')
  }
  
  // ❌ Cannot pass a function like this
  return <ClientButton onClick={handleClick} />
}

If your client component truly needs to run code in response to user interaction, use a Server Action:

tsx
// app/example/page.tsx
import ClientButton from './ClientButton'

async function handleClick() {
  'use server'
  // This runs on the server
  console.log('clicked on server')
  // Can access databases, secrets, etc.
}

export default function Page() {
  // ✅ Server Actions are serializable and can be passed to client components
  return <ClientButton onClick={handleClick} />
}

// app/example/ClientButton.tsx
'use client'

export default function ClientButton({ onClick }) {
  return <button onClick={onClick}>Click me</button>
}

Server Actions are a special case: they're serialized as references, not as function code. When the client calls them, it makes a network request to run the action on the server.

Common mistakes

  • Adding 'use client' at the page level. This defeats the entire purpose of server rendering. Only use it where you need interactivity.
  • Wrapping children in the client component's logic. If you pass children, they're still server components—they don't gain access to client state. If a child needs client state, it must become a client component itself.
  • Trying to pass database connections or functions to client components. These can't be serialized. Fetch on the server, pass the data.
  • Importing heavy libraries in client components unnecessarily. Before you mark a component as client, check what you're actually importing. Move utility functions that don't need interactivity to separate files that stay on the server.
  • Forgetting that the boundary is one-way. A client component cannot import a server component. If you need server rendering inside a client wrapper, use the children pattern.

Key takeaways

  • Push 'use client' down as far as possible—only the component that needs React interactivity (state, hooks, event handlers) should be marked as a client component.
  • Use the children pattern to keep heavy rendering on the server while keeping a lightweight interactive shell on the client.
  • Remember that crossing the boundary from server to client requires serializable props; functions and class instances won't work (except Server Actions).
  • Marking a file as 'use client' also marks everything it imports as client code, so be deliberate about what lives in that file.
  • When in doubt, start with a server component and opt in to the client only when you hit a React hook you can't live without.
ShareXLinkedInReddit

Found this useful? Support the writing.

Buy me a coffee
Written by
Expona Admin
Site administrator

Comments

Loading comments…

Leave a comment

Comments are reviewed before they appear. Be kind and stay on topic.

Related articles