Fix: component needs useState, no Client parent

Client-only hooks need a client boundary. Add "use client" at the entry point and keep server-only imports outside its client dependency graph.

Mahdi Ben RhoumaMahdi Ben Rhouma · Updated

You're importing a component that needs useState. It only works in a Client Component, but none of its parents are marked with "use client" appears when a module uses a client-only hook without belonging to a "use client" import graph. The message names useState, but the same error also concerns client-only hooks such as useEffect and useReducer. It does not mean that every React hook is browser-only, or that Client Components cannot be prerendered on the server.

The one-line fix — and why it's often in the wrong file#

The fix is adding "use client" as the very first line of the file that calls the hook:

tsx
// components/Counter.tsx
'use client';
 
import { useState } from 'react';
 
export function Counter() {
  const [count, setCount] = useState(0);
  return <button onClick={() => setCount((c) => c + 1)}>{count}</button>;
}

Adding the directive to the file that imports Counter also works, because the import joins that module's client dependency graph:

tsx
// app/page.tsx
'use client'; // Valid boundary: imported Counter is also client code.
 
import { Counter } from '@/components/Counter';
 
export default function Page() {
  return <Counter />;
}

This is a valid fix, not a missing-directive check being bypassed. The official directive reference explains that every client file does not need its own directive. The tradeoff is the size of the client import graph: placing the boundary on a small interactive component keeps unrelated imports out of that graph. It does not convert Server Components passed as children by a server parent into client imports. Server-only code must remain outside the client dependency graph; marking a page as client does not make its database access or async server-component code browser-compatible.

The barrel-file version of this bug#

A plain barrel re-export does not remove a component's existing client boundary:

tsx
// components/index.ts — barrel file
export { Counter } from './Counter';
export { Header } from './Header';
tsx
// app/page.tsx — Server Component
import { Counter } from '@/components'; // through the barrel

If Counter.tsx itself has "use client" this works — the directive lives with the component, and re-exporting does not strip it. The failure mode to watch for is a barrel file that imports the hook and forwards a wrapped version, which must itself be inside a client boundary if it calls client-only hooks. When a Server Component imports that wrapper directly, mark the wrapper's entry file:

tsx
// components/index.tsx
'use client';
import { useState } from 'react';
import { Counter } from './Counter';
 
export function CounterWithLabel() {
  const [visible, setVisible] = useState(true);
  return (
    <div>
      <button onClick={() => setVisible(!visible)}>Toggle counter</button>
      {visible && <Counter />}
    </div>
  );
}

When a third-party component triggers it#

If the hook lives inside a library you cannot edit (a UI kit, a chart package), wrap it in your own Client Component instead of trying to patch the library:

tsx
// components/ChartClient.tsx
'use client';
 
import { InteractiveChart } from 'some-chart-lib';
 
export function ChartClient(props: React.ComponentProps<typeof InteractiveChart>) {
  return <InteractiveChart {...props} />;
}
tsx
// app/page.tsx — Server Component
import { ChartClient } from '@/components/ChartClient';
 
export default async function Page() {
  const data = await getChartData();
  return <ChartClient data={data} />;
}

Data fetched on the server passes through as a plain prop; only the rendering that needs the hook lives on the client.

Verifying the fix#

  1. grep -rln "useState\|useEffect\|useReducer\|useContext" src components app and check whether each module belongs to a client import graph. Add "use client" before imports at the intended entry point when no boundary exists.
  2. Confirm you added the directive to the smallest possible component, not to layout.tsx or page.tsx — check with next build that unrelated Server Components in the same file tree still run without a browser bundle.
  3. If a barrel defines a wrapper that uses client-only hooks, verify that wrapper belongs to a client import graph. Plain re-exports preserve existing boundaries; adding ordinary JSX without a client-only hook does not itself require a new one.

Keeping the fetch on the server: pass children instead of converting the tree#

Adding "use client" to the whole page is inappropriate when that page must retain server-only imports or server-side data loading. A Client Component can still render Server Components, as long as it receives them through props.children (or any other prop) rather than importing and rendering them directly. React only requires that the component instantiating the hook is a Client Component — it does not require every component nested inside it to be one too, if that nesting happens through composition instead of a direct import:

tsx
// components/ClientShell.tsx
'use client';
import { useState } from 'react';
 
export function ClientShell({ children }: { children: React.ReactNode }) {
  const [open, setOpen] = useState(false);
  return <div onClick={() => setOpen(!open)}>{open ? children : null}</div>;
}
tsx
// app/page.tsx — stays a Server Component
import { ClientShell } from '@/components/ClientShell';
 
export default async function Page() {
  const data = await getData(); // runs on the server, never ships to the browser
  return (
    <ClientShell>
      <ExpensiveServerRenderedList data={data} />
    </ClientShell>
  );
}

ExpensiveServerRenderedList is never imported by ClientShell.tsx, so its module graph — and anything it imports, including database clients — never enters the client bundle. page.tsx composes both, but composing is not importing: React resolves children before it ever crosses the client boundary. This distinction — what "use client" actually marks versus what people assume it does — is covered end to end in use client vs use server in Next.js: The Real Difference, including why "use server" is not its opposite.

The context-provider file: one small client file, not the whole layout#

A related version of this error shows up when a team introduces a React Context (a theme provider, a query-client provider, an auth-state provider) directly inside app/layout.tsx and puts "use client" at the top of the layout to make the createContext/useContext calls work. This moves the layout module and its imports into the client graph; pages supplied through children can remain Server Components. A dedicated providers.tsx keeps that client graph smaller while the layout itself can retain server-only work:

tsx
// app/providers.tsx
'use client';
 
import { createContext, useState } from 'react';
import type { Dispatch, SetStateAction } from 'react';
 
export const ThemeContext = createContext<{
  theme: string;
  setTheme: Dispatch<SetStateAction<string>>;
} | null>(null);
 
export function Providers({ children }: { children: React.ReactNode }) {
  const [theme, setTheme] = useState('light');
  return (
    <ThemeContext.Provider value={{ theme, setTheme }}>{children}</ThemeContext.Provider>
  );
}
tsx
// app/layout.tsx — stays a Server Component
import { Providers } from './providers';
 
export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html>
      <body>
        <Providers>{children}</Providers>
      </body>
    </html>
  );
}

layout.tsx never gets the directive; only providers.tsx does. The children prop passed into Providers is resolved by React before crossing into the client module, the same mechanism as the composition pattern above, which is why pages nested under this layout keep doing server-side data fetching without any hook-error surprises. The same "smallest possible client boundary" principle applies to route-level state, not just context — see how it plays out for URL-driven state in use client vs use server in Next.js: The Real Difference.

Further reading: Fix: Can't Perform a State Update on an Unmounted Component, Fix localStorage is not defined after 'use client' (Next.js).

Still stuck? Paste your error.

Paste the error message or stack trace — we check it against every fix and tool on the site.

Matching happens in your browser. When you search, a cleaned error signature is sent so we can spot errors we do not cover yet — never the raw text, your IP address or your e-mail. Do not paste passwords or keys. Details

Related fixes & guides