How to Fix: Client Component is rendering at server side on app dir even with “use client”

6 min read

Your Client Component is not actually “running as a Server Component” just because you see its HTML in the page source or initial response. In the Next.js app directory, even components marked with “use client” can still participate in the server-rendered HTML output so the browser gets a fast first paint, while their interactive logic hydrates on the client afterward.

Understanding the Root Cause

This issue is usually caused by a misunderstanding of how Next.js App Router handles rendering. In the app directory, the “use client” directive does not mean “never rendered on the server.” It means the component is treated as a Client Component boundary, allowing browser-only features such as useState, useEffect, event handlers, and DOM APIs.

However, Next.js still uses server-side pre-rendering to generate the initial HTML whenever possible. That means:

  • The server can output HTML for a Client Component subtree.
  • The browser receives that HTML immediately.
  • React then hydrates the component on the client and attaches interactivity.

So if you inspect the response and see markup from a Client Component, that is expected behavior, not a bug.

The confusion becomes more obvious in files like app/layout.tsx. Layouts are part of the server rendering pipeline, and any nested Client Components may still contribute HTML to the initial render. The key distinction is:

  • Server Component: executes only on the server and cannot use client-only hooks.
  • Client Component: can use client-only React features, but may still be pre-rendered into HTML on the server before hydration.

If your component accesses browser-only globals such as window, document, or localStorage during render, it may fail or behave unexpectedly because that render path can still be evaluated during pre-rendering.

In short, the root cause is this: “use client” controls component capability and bundling, not whether initial HTML can be generated on the server.

Step-by-Step Solution

The fix depends on what you actually want.

1. Keep using “use client” for interactive components

If your component needs hooks or event handlers, keep the directive at the top of the file.

'use client'

import { useEffect, useState } from 'react'

export default function ThemeToggle() {
  const [mounted, setMounted] = useState(false)

  useEffect(() => {
    setMounted(true)
  }, [])

  if (!mounted) return null

  return <button>Toggle Theme</button>
}

This ensures the component hydrates correctly and avoids browser-only logic running too early.

2. Move browser-only APIs into useEffect

If the component reads from window, document, or localStorage, do not access them during render.

'use client'

import { useEffect, useState } from 'react'

export default function ClientOnlyValue() {
  const [value, setValue] = useState('')

  useEffect(() => {
    const saved = window.localStorage.getItem('demo') || ''
    setValue(saved)
  }, [])

  return <div>Saved value: {value}</div>
}

This is the most common correction when developers think a Client Component is “wrongly rendered on the server.”

3. If you want rendering to happen only in the browser, disable SSR for that component

Use a dynamic import with ssr: false. This is the correct solution when a component must never be pre-rendered on the server.

import dynamic from 'next/dynamic'

const BrowserOnlyWidget = dynamic(() => import('./BrowserOnlyWidget'), {
  ssr: false,
})

export default function Page() {
  return <BrowserOnlyWidget />
}

Then in the component itself:

'use client'

export default function BrowserOnlyWidget() {
  return <div>This renders only in the browser.</div>
}

This prevents the component from contributing HTML during server pre-rendering.

4. Keep app/layout.tsx as a Server Component unless client behavior is truly required

In most cases, layout.tsx should stay server-side and wrap smaller Client Components where needed.

import './globals.css'
import ThemeToggle from './ThemeToggle'

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <body>
        <header>
          <ThemeToggle />
        </header>
        {children}
      </body>
    </html>
  )
}

This pattern keeps the layout efficient while still enabling client interactivity where necessary.

5. Verify behavior correctly

Do not use only “HTML exists in view source” as proof that the component rendered incorrectly. Instead, verify:

  • Whether hooks like useEffect run in the browser.
  • Whether browser APIs are accessed safely.
  • Whether hydration completes without mismatch warnings.
  • Whether the component should be pre-rendered or truly browser-only.

If needed, compare the behavior with the official Next.js Client Components documentation.

6. Use a mounted check when content depends on client-only state

If the rendered output depends on values only available in the browser, delay the UI until the component mounts.

'use client'

import { useEffect, useState } from 'react'

export default function UserTimezone() {
  const [mounted, setMounted] = useState(false)

  useEffect(() => {
    setMounted(true)
  }, [])

  if (!mounted) {
    return <span>Loading...</span>
  }

  return <span>{Intl.DateTimeFormat().resolvedOptions().timeZone}</span>
}

This avoids hydration inconsistencies when server and client produce different output.

Common Edge Cases

Hydration mismatch errors

If the server renders one value and the browser renders another immediately, React may report a hydration mismatch. This often happens with:

  • Random values like Math.random()
  • Time-dependent values like Date.now()
  • Locale or timezone-sensitive formatting
  • Direct reads from browser storage during render

Fix these by moving client-specific logic into useEffect or by rendering a stable fallback first.

Using window or document inside render

Even with “use client”, accessing browser globals too early can break pre-rendering assumptions. Always guard them inside effects or event handlers.

useEffect(() => {
  console.log(window.location.href)
}, [])

Making the entire layout a Client Component unnecessarily

Marking large files such as layout.tsx with “use client” can increase the client bundle and reduce the benefits of Server Components. Prefer small, isolated client boundaries.

Third-party libraries that assume the browser exists

Some UI packages touch the DOM at import time. In those cases, use dynamic() with ssr: false if the library is not SSR-safe.

Confusing pre-rendering with execution location

Seeing HTML output does not mean all logic executed in the browser only. Next.js can pre-render markup on the server and still hydrate it later as a Client Component.

FAQ

Does “use client” mean the component should never appear in server HTML?

No. It means the component is a Client Component and can use client-side React features. Next.js may still pre-render its initial HTML on the server for performance.

How do I force a component to render only in the browser?

Use next/dynamic with ssr: false. That is the correct approach when server pre-rendering must be fully disabled.

Why does my Client Component break when reading localStorage?

Because browser APIs are not safely available during pre-rendering. Read them inside useEffect or after mount, not during the initial render.

The practical takeaway is simple: this behavior is expected in Next.js App Router. A component marked with “use client” is still allowed to be part of the server-generated HTML response. If you need true browser-only rendering, use dynamic import with SSR disabled; if you only need interactivity, keep the component as a normal Client Component and move browser-specific logic into safe client-only lifecycle paths.

Leave a Reply

Your email address will not be published. Required fields are marked *