The situation
Migrated a project to Next.js 14 App Router, or started fresh and moved components over. Then this hit:
Error: 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".
โญโ[/app/dashboard/page.tsx:1:1]
ยท
1 โ import { SearchBar } from "@/components/SearchBar";
ยท โโโโโโโโโโ
โฐโ
Maybe one of these is what you want:
- /components/SearchBar.tsx
The component itself was fine โ worked in the old pages/ router. App Router just plays by different rules.
Why this happens
In Next.js App Router, every component is a Server Component by default. Server Components run on the server โ great for fetching data, reading from a database, keeping secrets off the client. But they have zero access to browser APIs or React's interactive hooks: useState, useEffect, useRef, useCallback, and so on.
Hook-dependent code can't run on the server. When Next.js sees a Server Component importing something that calls hooks โ without a "use client" boundary in between โ it throws this error rather than letting broken code silently ship.
Debug: trace the import chain
The error points to the import line, but the actual hook is sometimes buried two or three components deep. Check the error stack โ it usually spells out the full chain:
Trace:
DashboardPage (Server Component)
โโ SearchBar โ imports useCallback
โโ FilterDropdown โ imports useState โ actual offender
Walk the chain until you find the component that actually calls the hook. That's where the fix goes โ not necessarily the file listed at the top of the error.
Solution 1: Add "use client" to the component file
Fastest fix โ add "use client" as the very first line of the file that uses the hook:
// components/SearchBar.tsx
"use client";
import { useState, useCallback } from "react";
export function SearchBar() {
const [query, setQuery] = useState("");
const handleChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
setQuery(e.target.value);
}, []);
return (
<input
value={query}
onChange={handleChange}
placeholder="Search..."
/>
);
}
Once a file has "use client", all components imported into it are also treated as client-side. The parent Server Component can import this freely โ it just can't use hooks itself.
Solution 2: Split the component (better for performance)
Better approach: keep data-fetching and static rendering on the server, push only the interactive pieces into a Client Component. One hook in the wrong place can pull an entire page into the client bundle.
Before (broken โ one hook contaminates the whole page):
// app/dashboard/page.tsx
import { useState } from "react"; // โ kills it
export default function DashboardPage() {
const [filter, setFilter] = useState("all");
const data = fetchDataFromDB(); // can't mix server data fetch with hooks
return (
<div>
<h1>Dashboard</h1>
<select value={filter} onChange={e => setFilter(e.target.value)}>
...
</select>
</div>
);
}
After (split correctly):
// components/FilterSelect.tsx
"use client";
import { useState } from "react";
export function FilterSelect() {
const [filter, setFilter] = useState("all");
return (
<select value={filter} onChange={e => setFilter(e.target.value)}>
<option value="all">All</option>
<option value="active">Active</option>
</select>
);
}
// app/dashboard/page.tsx โ stays a Server Component
import { FilterSelect } from "@/components/FilterSelect";
export default async function DashboardPage() {
const data = await fetchDataFromDB(); // runs on server, no "use client" needed
return (
<div>
<h1>Dashboard</h1>
<FilterSelect />
<DataTable data={data} />
</div>
);
}
Only the dropdown gets bundled into client JS. The rest stays server-rendered.
Common gotchas
"use client" placement matters
"use client" must be the absolute first line โ before imports, before comments. Anything above it and Next.js ignores the directive entirely.
// WRONG โ comment before directive
// This is a search bar component
"use client";
// CORRECT
"use client";
// This is a search bar component
Third-party libraries that use hooks internally
Chart libraries, date pickers, UI component kits โ many of them call hooks under the hood. Your file doesn't need to call useState directly; if the library does, you still need a wrapper:
// components/ChartWrapper.tsx
"use client";
import { LineChart } from "recharts"; // uses hooks internally
export function ChartWrapper({ data }: { data: ChartData[] }) {
return <LineChart data={data} width={600} height={300} />;
}
Context providers in root layout
This one catches almost everyone. Drop a ThemeProvider or any Context.Provider into app/layout.tsx and suddenly the whole layout needs "use client" โ which defeats the point. Extract the provider into its own file instead:
// components/Providers.tsx
"use client";
import { ThemeProvider } from "next-themes";
export function Providers({ children }: { children: React.ReactNode }) {
return <ThemeProvider attribute="class">{children}</ThemeProvider>;
}
// app/layout.tsx โ Server Component, no "use client" needed here
import { Providers } from "@/components/Providers";
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
<Providers>{children}</Providers>
</body>
</html>
);
}
Verify the fix
- Run
pnpm devโ error should be gone from both the terminal and browser console. - Production check:
pnpm buildshould complete with no errors. - Open DevTools โ Network โ filter JS. A properly split component tree often cuts the initial JS bundle by 30โ50% compared to marking the whole page as a Client Component.
pnpm build
# โ Compiled successfully
# โ Generating static pages (12/12)
# โ Finalizing page optimization
The mental model
App Router flips the default. In the old pages/ router, every component was implicitly client-side. Now it's the opposite: server-first, and you opt into client rendering only when you need it.
Keep "use client" as deep in the component tree as possible. Each level you push it down is less JavaScript shipped to the browser. A page-level "use client" on a data-heavy dashboard is essentially the same as abandoning App Router entirely.
Eight hooks trigger this error: useState, useEffect, useCallback, useMemo, useRef, useContext, useReducer, and useLayoutEffect. Any hook that touches browser state or component lifecycle needs "use client".

