A Next.js server component using `useState`. Build error. Subtle for newcomers to the App Router — the rules around server vs client components are not obvious.
The vulnerable diff
// app/dashboard/page.tsx — server component by default
import { useState } from "react"; // BUG
export default function Dashboard() {
// BUG: useState only works in client components
const [tab, setTab] = useState("overview");
return <div>{tab}</div>;
}What is wrong
Next.js App Router treats every file as a server component by default — no React hooks (useState, useEffect, useRef, useMemo, useCallback, etc), no browser APIs (window, document, localStorage). Client hooks throw a build error. The fix is to split the interactive piece into a separate file with `"use client"` at the top — that file can use any client hook, and the server component imports and renders it. Component boundaries become the client/server seam.
The attack
Build output:
Error: useState can not be used in a Server Component.
Mark the file with "use client" to use it in a client component
or move it to a Client Component.Or worse: a missing `"use client"` directive lets the file compile but state is silently lost on hydration.
Mesrai's review comment
mesraipilot · Bot · reviewed 30 sec ago
[mesrai] [code-review] [Language] [RSC] [high]
useState in a server component. Move the interactive piece into a
client component:
// app/dashboard/components/TabSwitcher.tsx
"use client";
import { useState } from "react";
export function TabSwitcher() {
const [tab, setTab] = useState("overview");
return <div>...</div>;
}
// app/dashboard/page.tsx (stays server component)
import { TabSwitcher } from "./components/TabSwitcher";
export default function Dashboard() {
// Server-fetch any data needed (DB, fetch, headers, cookies)
return <TabSwitcher />;
}
Keep server components for data fetching + composition. Push
interactivity to leaf client components.The fix
// app/dashboard/components/TabSwitcher.tsx
"use client";
import { useState } from "react";
export function TabSwitcher() {
const [tab, setTab] = useState("overview");
return <div onClick={() => setTab("billing")}>{tab}</div>;
}
// app/dashboard/page.tsx (stays a server component)
import { TabSwitcher } from "./components/TabSwitcher";
export default async function Dashboard() {
const data = await db.users.findUnique({ where: { id: "..." } });
return <TabSwitcher />;
}Server component does data fetching (which now runs on the server with direct DB access, no API roundtrip). Client component handles interactivity (with state + handlers). The boundary lives at the file with `"use client"`.
Why human review missed it
RSC rules are 18 months old as of 2026 and many teams have not internalized them. The error message is clear when it fires, but more subtle missing-directive cases (no error, silent broken behavior) are harder. Mesrai catches `"use client"` mismatches and any client-hook import in a server context.
Related rules + further reading
Mesrai rule pack: language/rsc-boundary — flags client hook usage in server component files.
Next.js docs: Server and Client Components.
React Server Components RFC.
Takeaway
Server components for data + composition. Client components for state + interactivity. Split at the file with `"use client"`. Mesrai catches the boundary every time.