skip to content

You import a UI component from an npm package into a Next.js App Router page and the build fails saying the component needs useState and only works in a Client Component — but the package ships no 'use client' directive of its own. How do you fix it without turning the page into a Client Component?

level: middleimportance: should knowfreq 48%

answer

  1. who is allowed to declare the boundary
  2. the package cannot, but you can
  3. a thin module you own
  4. re-export under 'use client'
  5. wrap the export, never the barrel

basics

~20 s

Create a thin module of your own that starts with 'use client' and re-exports the package's component, then import that module from the page. Your wrapper becomes the boundary, the package's code sits below it, and the page stays a Server Component.

solid answer

~50 s

The package assumes a world where all React code is client code, so nobody in the import chain has opened the boundary — and you cannot edit `node_modules`. The fix is to open it yourself: add a small module you own whose first line is `'use client'` and which re-exports the component, then import from your module instead of from the package. Because the directive marks a module as an entry point into the client graph, the package's code is now pulled in below your wrapper while the page above it stays a Server Component and can keep awaiting data. Two habits keep this from rotting: wrap the specific export you need rather than a barrel of the whole library, and keep the wrapper thin so it does not accumulate logic. If the package later ships its own directive, delete the wrapper.

code

tsx · 4 lines
tsx
// app/components/carousel.tsx
'use client'

export { Carousel } from 'some-carousel-lib'

go deeper

for a junior

Know that a package can be missing the directive and that the fix is a small file of your own starting with 'use client' that re-exports the component; import that file instead of the package.

for a middle

Explain the mechanism: the directive marks a module as an entry point, so the package's code joins the client graph beneath your wrapper while the page above keeps its server abilities.

for a senior

Show the review instinct — check for a server-safe entry point first, wrap one export rather than a barrel, keep the wrapper thin, and treat it as a shim to delete when the package ships its own directive.

for a principal

Be ready to talk about dependency policy: how you evaluate whether a library is App-Router-ready before it lands, and when a heavy client-only dependency is worth wrapping at all versus replacing.

## What the error is actually telling you Next walks the import graph starting from your route's entry — the page or layout — and finds a module that uses a client-only hook with no `'use client'` declared anywhere above it in that chain. The message names the hook and points at the offending module. When that module lives inside `node_modules`, the package was written for a bundler where every React module was client code: true for a plain SPA, true for the Pages Router, not true under the App Router where the default flipped. ## Why adding the directive to the page is the wrong fix It does make the error go away, and that is what makes it tempting. But the directive applies to the module it is written in and to everything that module imports, transitively — so a directive on `page.tsx` pulls the page's whole import subtree into the client graph. You also lose the page's server abilities in one step: it can no longer be an `async` component awaiting data, it can no longer import `next/headers`, and any server-side client or SDK it imported now has no business being in that graph. You traded a two-line fix for a page that ships far more JavaScript than it needs to. ## The fix: a boundary module you own ```tsx // app/components/carousel.tsx 'use client' export { Carousel } from 'some-carousel-lib' ``` Then the page imports `./components/carousel` rather than the package. The directive marks this file as an entry point into the client graph, so the re-exported component and everything the package pulls in with it are bundled for the browser, and the page above it is untouched. A wrapper that adds a default configuration works the same way — declare the directive, import the package inside that module, and export your configured component. The important property is not the shape of the wrapper but that the directive sits in a file you control, as close to the leaf as possible. ## Details that decide whether this holds up **Wrap the export, not the barrel.** If you write the directive on a module that re-exports the package's entire index, every import from your wrapper drags the whole library across the boundary even when you only wanted one component. One wrapper per component you actually use keeps the boundary tight. **Props still have to survive the trip.** Whatever the page passes into the wrapped component is serialized on its way to the client, which is a separate constraint from the boundary itself — a callback prop will not cross just because the wrapper exists. **Check for a server-safe entry first.** Some packages publish separate subpath exports, or ship the directive on the interactive parts and leave pure helpers importable from the server. Reading the package's exports before wrapping saves you a wrapper you would later delete. **`next/dynamic` is not a substitute.** Loading the component lazily is about *when* the code arrives, not about *which side* it belongs to, and disabling SSR for a dynamic import is itself only allowed from within a Client Component. It is a bundle-timing tool, not a boundary tool. ## What the wrapper does and does not buy you It does not make the library cheaper. The same code ships to the browser either way; what changed is that the boundary now starts at a file you chose instead of swallowing the page. If the library is heavy and you only need it on one interaction, the honest answer is to reconsider the dependency or defer its loading — the wrapper is about correctness and boundary placement, not about size. ## The upstream fix The permanent fix belongs in the package: a `'use client'` directive at the top of its client-only entry points. Many libraries added exactly that as the App Router became common. Opening an issue or a small pull request is a reasonable move, and until it lands your wrapper is the local shim — one you can delete in a single commit once the package ships it.

  • Does the wrapper make the library any cheaper to ship?
    No. Exactly the same package code ends up in the browser bundle; only the position of the boundary changed. If size is the concern, the answer is deferring the load, importing a lighter entry point, or dropping the dependency — not the wrapper.
  • Why not put 'use client' on a barrel file that re-exports your whole component library?
    Because the directive applies to that module and everything it imports. A barrel imports every component, so one import from it pulls the entire library into the client graph — even the purely presentational parts that would have rendered fine on the server.
  • How do you find which import actually forced the boundary?
    The error names the module and the hook, and prints the import chain that reached it. Reading that chain from your page downward shows the first module without a directive — that is where the wrapper belongs, and it is often a transitive dependency rather than the one you imported.

saying these in an interview costs you the question

  • Puts 'use client' at the top of the page to silence the error
  • Tries to edit the file inside node_modules
  • Thinks the wrapper makes the library render on the server
  • Reaches for next/dynamic as if it moved the boundary
  • Wraps a barrel export of the whole library

context