Skip to content

Next.js error

You're Importing a Component That Needs next/headers

The error

You're importing a component that needs next/headers. That only works in a Server Component but one of its parents is marked with "use client", so it's a Client Component.

A server-only API was pulled into a client tree, usually by a shared helper. The error names the file; the cause is the boundary above it.

cookies(), headers() and draftMode() from next/headers read the incoming request. There is no incoming request in the browser, so they exist only in Server Components, route handlers and Server Actions.

The error appears when one of them ends up beneath a 'use client' boundary. Note what it actually says: one of its parents is marked. The file that throws is often not the file that changed.

Move the read up, not the directive down

The instinct is to make the component that needs the header a Server Component. That does not work if its parent is a Client Component - a Client Component cannot render a Server Component as a child.

What it can do is receive one as a prop or as children, rendered by a server parent:

// app/page.tsx  - Server Component
import { cookies } from 'next/headers';
import { Shell } from './shell';         // 'use client'
 
export default async function Page() {
  const theme = (await cookies()).get('theme')?.value ?? 'light';
  return <Shell theme={theme}>{/* ... */}</Shell>;
}

The cookie is read where it can be read. Shell receives a string. Nothing crosses the boundary that cannot serialise.

This is the general shape: request data enters at the top of the tree and is passed down. Reaching for it in the middle is what produces this error.

The shared-helper case

The version that takes longer to find:

app/dashboard/layout.tsx     'use client'
  └─ <UserMenu />
       └─ lib/session.ts      → calls cookies()

lib/session.ts is imported by six server files and one client one. It was fine until layout.tsx gained a directive - maybe for an animation - and took its whole subtree with it.

Two useful moves here. Split the helper so the part that reads the request is separate from the part that is pure. And add import 'server-only' to the request-reading half, which turns this into a build error at the exact import rather than a runtime surprise in one route.

What looks like a fix and is not

Removing 'use client' from the parent. Sometimes correct, and worth checking - directives accumulate and some are no longer needed. But if the parent genuinely has state or handlers, removing it trades this error for a different one.

Reading the cookie in an effect and fetching it from an API route. This works and costs a round trip after hydration, plus a flash of the wrong state. For a theme, that flash is the thing users notice.

Passing the whole cookies() object down. It does not serialise, and you get a different error again. Pass the value, not the accessor.

The consequence nobody mentions

Reading request data is also what takes a route out of static rendering, so fixing this error by moving the read upward has a second effect worth being deliberate about: the route becomes dynamic, and everything under it too. Contain it with a Suspense boundary and the rest of the page stays static.

Related questions

Can I read a cookie in the browser instead?
For a non-sensitive preference, yes - `document.cookie` or a client library works. For anything the server needs to trust, no: an httpOnly session cookie is not readable from JavaScript, which is the point of it.
Why does the error name a component I did not change?
Because the boundary moved, not the component. Something above it gained a 'use client' directive, and everything below inherited it.