Fix: React Server Components Error — useState, Event Handlers, and Client Boundary Issues
Part of: React & Frontend Errors
Quick Answer
How to fix React Server Components errors — useState and hooks in server components, missing 'use client' directive, async component patterns, serialization errors, and client/server boundary mistakes.
The Error
A React Server Component throws a build or runtime error:
Error: useState only works in Client Components. Add the "use client" directive at the top of the file to use it.Or an event handler error:
Error: Event handlers cannot be passed to Client Component props.
<Component onClick={function} children={...}>
^^^^^^^^^^^^^^^^
If you need interactivity, consider converting part of this to a Client Component.Or a serialization error when passing data from server to client:
Error: Only plain objects can be passed to Client Components from Server Components.
Date objects are not supported.Or an async component error on the client side:
Error: async/await is not yet supported in Client Components, only Server Components.Why This Happens
React Server Components (RSC) introduced a strict server/client split that breaks patterns that worked fine in traditional React. Every file in the Next.js App Router is a Server Component by default. There is no visual indicator in the code that distinguishes server from client — the only signal is the presence or absence of the 'use client' directive at the top of the file. This implicit default catches developers who are used to Create React App or Pages Router, where every component runs on the client.
The boundary between server and client is also a serialization boundary. Props that cross from a Server Component parent to a Client Component child must be JSON-serializable. Functions, class instances, Date objects, Map, Set, and RegExp cannot survive that crossing. This is fundamentally different from traditional React, where any JavaScript value can be passed as a prop because everything runs in the same runtime.
Here are the specific patterns that trigger the error:
- Hooks in Server Components —
useState,useEffect,useRef, and all other React hooks only work in Client Components. Server Components render once on the server with no lifecycle or state. - Missing
'use client'directive — any component that uses hooks, event handlers, or browser APIs must have'use client'at the top of its file. Without it, React treats it as a Server Component and rejects hook usage. - Event handlers from server to client — functions cannot cross the server/client boundary as props because they cannot be serialized to JSON. Server Components cannot pass
onClick,onChange, or any function prop to Client Components directly. - Non-serializable data — Server Components can pass data to Client Components only if it is serializable: strings, numbers, arrays, plain objects, and
null.Dateobjects,Map,Set,classinstances, andundefinedrequire conversion. - Async Client Components — async/await is only supported in Server Components. Client Components must use
useEffector React Query for async data fetching. - Context in Server Components — React context (
createContext,useContext) only works on the client. Server Components cannot consume or provide context.
Diagnostic Timeline
Here is the step-by-step process an experienced engineer follows when one of these errors hits.
Minute 0 — Read the error message literally. The RSC error messages are specific. “useState only works in Client Components” tells you the exact hook and the exact component type mismatch. “Event handlers cannot be passed to Client Component props” tells you a function is crossing the boundary. Do not guess yet. Note which file and which line the error points to.
Minute 1 — First instinct: add 'use client' to the file. This is the most common first reaction, and it works for simple cases. But slapping 'use client' on every file that errors is the wrong approach — it defeats the purpose of Server Components and bloats the client bundle. Resist the urge to apply it broadly. Ask: does this component actually need client-side interactivity, or is it importing something that does?
Minute 2 — Check what the component actually uses. Open the file the error points to. Scan for hooks (useState, useEffect, useRef), event handlers (onClick, onChange), and browser APIs (window, document, localStorage). If none of these appear in the component itself, the problem is deeper — likely a transitive import.
Minute 3 — Trace the import chain. The error often points to a component that does not directly use hooks but imports a library that does. For example, a component importing react-chartjs-2 or framer-motion will fail because those libraries use hooks internally. Check whether the imported library is client-only. If it is, the component that imports it needs 'use client', or you need a wrapper component.
Minute 4 — Check prop serialization. If the error mentions serialization (“Only plain objects can be passed”), inspect the props being passed from the Server Component to the Client Component. Look for Date objects from database queries (Prisma, Drizzle), class instances, function references, or Map/Set. The fix is conversion, not adding 'use client' — this is a data problem, not a directive problem.
Minute 5 — Identify the real boundary. The 'use client' directive should be placed as deep in the component tree as possible. If a page component fetches data on the server and only a small interactive widget needs client-side state, extract just that widget into a separate file with 'use client'. The page stays as a Server Component and benefits from server-side data fetching, zero-bundle-size rendering, and direct database access.
Minute 6 — Verify the fix. Run next build (not just the dev server). The dev server is more lenient with RSC boundaries than a production build. A passing dev server does not guarantee the build will succeed.
Fix 1: Add the 'use client' Directive
Any component that uses hooks, browser APIs, or event handlers needs 'use client' at the very top of the file — before imports:
// WRONG — using hooks without 'use client'
import { useState } from 'react';
export function Counter() {
const [count, setCount] = useState(0); // Error
return <button onClick={() => setCount(count + 1)}>{count}</button>;
}// CORRECT — 'use client' at the top, before imports
'use client';
import { useState } from 'react';
export function Counter() {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(count + 1)}>{count}</button>;
}'use client' marks the boundary, not individual components. Everything imported by a 'use client' file becomes part of the client bundle — including all its transitive imports.
Common hooks that require 'use client':
'use client';
import {
useState, // Local state
useEffect, // Side effects / lifecycle
useRef, // Mutable refs / DOM access
useContext, // Context consumption
useReducer, // Complex state
useCallback, // Memoized callbacks
useMemo, // Memoized values
useTransition, // Concurrent mode transitions
useOptimistic, // Optimistic updates
} from 'react';Fix 2: Keep Server Components as High as Possible
The goal is to push 'use client' as deep into the component tree as possible, keeping data fetching and rendering on the server:
// WRONG — marking the whole page as client component
// (loses all server-side benefits: data fetching, direct DB access, etc.)
'use client';
import { useState } from 'react';
import { db } from '@/lib/db';
export default async function ProductPage({ params }) {
const product = await db.product.findUnique({ where: { id: params.id } });
const [quantity, setQuantity] = useState(1); // forces 'use client'
return (
<div>
<h1>{product.name}</h1>
<input value={quantity} onChange={e => setQuantity(+e.target.value)} />
</div>
);
}// CORRECT — Server Component fetches data, Client Component handles interaction
// app/products/[id]/page.tsx (Server Component — no 'use client')
import { db } from '@/lib/db';
import { QuantitySelector } from './QuantitySelector';
export default async function ProductPage({ params }) {
const product = await db.product.findUnique({ where: { id: params.id } });
return (
<div>
<h1>{product.name}</h1>
<p>{product.description}</p>
{/* Only the interactive part is a Client Component */}
<QuantitySelector productId={product.id} price={product.price} />
</div>
);
}// components/QuantitySelector.tsx
'use client'; // Only this small component is a Client Component
import { useState } from 'react';
export function QuantitySelector({ productId, price }: { productId: string; price: number }) {
const [quantity, setQuantity] = useState(1);
return (
<div>
<input
type="number"
value={quantity}
onChange={e => setQuantity(+e.target.value)}
min={1}
/>
<p>Total: ${(price * quantity).toFixed(2)}</p>
</div>
);
}Fix 3: Fix Event Handler Prop Errors
Server Components cannot pass function props (event handlers) to Client Components because functions are not serializable. The pattern is to define functions inside Client Components:
// WRONG — Server Component trying to pass a function prop
// app/page.tsx (Server Component)
export default function Page() {
const handleClick = () => console.log('clicked'); // Function defined in server
return <Button onClick={handleClick} />; // Error: function can't cross boundary
}// CORRECT — Client Component defines its own handlers
// app/page.tsx (Server Component)
import { SubmitButton } from '@/components/SubmitButton';
export default function Page() {
return <SubmitButton label="Submit Order" />; // Pass only serializable props
}// components/SubmitButton.tsx
'use client';
export function SubmitButton({ label }: { label: string }) {
// Event handler defined inside the Client Component
const handleClick = () => {
console.log('clicked');
};
return <button onClick={handleClick}>{label}</button>;
}For Server Actions — pass functions using the action prop or Server Action pattern:
// Server Actions are a special exception — they can be passed from server to client
// app/actions.ts
'use server';
export async function submitOrder(formData: FormData) {
// This runs on the server
await db.order.create({ data: { ... } });
}// app/page.tsx (Server Component)
import { submitOrder } from './actions';
import { OrderForm } from '@/components/OrderForm';
export default function Page() {
// Server Actions CAN be passed as props — they're serialized as references
return <OrderForm action={submitOrder} />;
}// components/OrderForm.tsx
'use client';
export function OrderForm({ action }: { action: (formData: FormData) => Promise<void> }) {
return (
<form action={action}>
<input name="product" />
<button type="submit">Order</button>
</form>
);
}Fix 4: Fix Serialization Errors
Only these types can cross the server/client boundary as props:
- Primitives:
string,number,boolean,null,undefined - Plain objects and arrays (recursively containing only the above)
BigInt,Date(as of React 19 / Next.js 15)- React elements (JSX)
- Server Actions
// WRONG — passing a Date object (not serializable in older versions)
// app/page.tsx
export default async function Page() {
const post = await db.post.findFirst();
return <PostCard post={post} />; // post.createdAt is a Date object — Error
}// CORRECT — serialize Date to string before passing
export default async function Page() {
const post = await db.post.findFirst();
return (
<PostCard
title={post.title}
createdAt={post.createdAt.toISOString()} // Serialize to string
/>
);
}Or use a serializable DTO pattern:
// Serialize the entire object
export default async function Page() {
const post = await db.post.findFirst();
return (
<PostCard
post={{
id: post.id,
title: post.title,
createdAt: post.createdAt.toISOString(), // Convert Date to string
tags: post.tags, // Plain array — OK
}}
/>
);
}Class instances and Maps/Sets — convert to plain objects:
// WRONG — Map can't be serialized
const tagMap = new Map([['react', 10], ['nextjs', 5]]);
return <TagCloud tags={tagMap} />; // Error
// CORRECT — convert to plain object
const tagMap = Object.fromEntries([['react', 10], ['nextjs', 5]]);
return <TagCloud tags={tagMap} />; // OKFix 5: Fetch Data in Server Components
Server Components support async/await natively. Client Components need useEffect or a data-fetching library:
// WRONG — async Client Component (not supported)
'use client';
export default async function Page() {
const data = await fetch('/api/posts').then(r => r.json()); // Error
return <PostList posts={data} />;
}// CORRECT option 1 — fetch in Server Component (recommended)
// No 'use client' needed — this is a Server Component
export default async function Page() {
const data = await fetch('https://api.example.com/posts').then(r => r.json());
return <PostList posts={data} />; // PostList is a Server Component too
}// CORRECT option 2 — fetch in Client Component with useEffect
'use client';
import { useState, useEffect } from 'react';
export default function Page() {
const [posts, setPosts] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetch('/api/posts')
.then(r => r.json())
.then(data => {
setPosts(data);
setLoading(false);
});
}, []);
if (loading) return <div>Loading...</div>;
return <PostList posts={posts} />;
}// CORRECT option 3 — use React Query in Client Component
'use client';
import { useQuery } from '@tanstack/react-query';
export default function Page() {
const { data: posts, isLoading } = useQuery({
queryKey: ['posts'],
queryFn: () => fetch('/api/posts').then(r => r.json()),
});
if (isLoading) return <div>Loading...</div>;
return <PostList posts={posts} />;
}Pro Tip: Prefer fetching data in Server Components whenever possible. You get direct database access (no API layer needed), automatic deduplication with React’s cache, and zero client-side waterfall loading.
Fix 6: Context in Server Components
React context does not work in Server Components. For server-side data sharing, use function parameters or module-level caching:
// WRONG — using context in a Server Component
import { useContext } from 'react';
import { ThemeContext } from '@/context/theme';
export function ServerComponent() {
const theme = useContext(ThemeContext); // Error: hooks not allowed in Server Components
return <div className={theme.primary}>Content</div>;
}// CORRECT option 1 — pass data as props from parent Server Component
export default async function Page() {
const theme = await getThemeFromDB();
return <ServerComponent theme={theme} />;
}
export function ServerComponent({ theme }: { theme: Theme }) {
return <div className={theme.primary}>Content</div>;
}// CORRECT option 2 — use React's cache() for server-side data sharing
import { cache } from 'react';
// cache() deduplicates calls within a single render
export const getTheme = cache(async () => {
return db.settings.findFirst({ where: { key: 'theme' } });
});
// Any Server Component can call this without prop drilling
export async function Sidebar() {
const theme = await getTheme(); // Cached — only one DB call per request
return <nav className={theme.primary}>...</nav>;
}
export async function Header() {
const theme = await getTheme(); // Uses the cached result
return <header className={theme.primary}>...</header>;
}For client-side context, wrap only the parts that need it:
// providers.tsx
'use client';
import { ThemeProvider } from '@/context/theme';
export function Providers({ children }: { children: React.ReactNode }) {
return <ThemeProvider>{children}</ThemeProvider>;
}// app/layout.tsx (Server Component)
import { Providers } from './providers';
export default function RootLayout({ children }) {
return (
<html>
<body>
<Providers>
{children} {/* Server Components can be children of Client Component providers */}
</Providers>
</body>
</html>
);
}Fix 7: Third-Party Components Without ‘use client’
Many npm packages have not added 'use client' directives. Wrapping them in a local Client Component resolves the error:
// WRONG — importing a third-party component that uses hooks
// but doesn't have 'use client'
import { Tooltip } from 'some-ui-library'; // Uses useState internally — Error
export default function Page() {
return <Tooltip content="Hello">Hover me</Tooltip>;
}// CORRECT — wrap it in a local file with 'use client'
// components/TooltipWrapper.tsx
'use client';
export { Tooltip } from 'some-ui-library';
// or
import { Tooltip as _Tooltip } from 'some-ui-library';
export function Tooltip(props: React.ComponentProps<typeof _Tooltip>) {
return <_Tooltip {...props} />;
}// Now import from the wrapper
import { Tooltip } from '@/components/TooltipWrapper';
export default function Page() {
return <Tooltip content="Hello">Hover me</Tooltip>; // No error
}Still Not Working?
Check which component is actually the Server Component. Next.js App Router defaults every component to a Server Component. If a component two levels deep needs a hook, the 'use client' must be on that component’s file specifically — not the parent.
Verify the 'use client' directive is on the correct file. Adding 'use client' to a barrel export file (index.ts) does not automatically apply to all exported components. Each file that uses hooks needs its own directive.
// WRONG — 'use client' in barrel doesn't propagate
// components/index.ts
'use client';
export { Counter } from './Counter'; // Counter.tsx still needs its own 'use client'
// CORRECT — each file declares 'use client' independently
// components/Counter.tsx
'use client';
export function Counter() { ... }Use React DevTools to visualize the component tree and verify which components are server vs. client rendered. Server Components appear with a different indicator in the React DevTools component tree.
Check for circular imports between server and client modules. If a Server Component imports a Client Component which imports back into a server-only module (like db), it can cause unexpected errors. Server-only modules should use the server-only package:
npm install server-only// lib/db.ts
import 'server-only'; // Throws at build time if imported in a Client Component
import { PrismaClient } from '@prisma/client';
export const db = new PrismaClient();Check the React and Next.js versions. React 19 and Next.js 15 expanded the set of serializable types across the boundary (including Date and BigInt). If you are on React 18 / Next.js 13 or 14, some data types that work in newer versions will throw serialization errors. Upgrade or convert the data before passing.
Watch for 'use client' placement after a comment block. The directive must be the very first statement in the file. A JSDoc comment, // @ts-check, or a license header before 'use client' makes React ignore the directive and treat the file as a Server Component:
// WRONG — comment before 'use client' breaks it
// This component handles user interactions
'use client'; // React ignores this because it's not the first statement
// CORRECT
'use client';
// This component handles user interactionsUse the server-only and client-only packages as guardrails. Import server-only in any module that must never run on the client (database access, secret keys) and client-only in modules that depend on browser APIs. This catches boundary violations at build time instead of runtime:
npm install server-only client-onlyFor related Next.js and React issues, see Fix: Next.js Hydration Failed, Fix: React Hooks Called Conditionally, Fix: Next.js Server Action Not Working, and Fix: React Context Not Updating.
Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.
Was this article helpful?
Related Articles
Fix: nuqs Not Working — URL State Not Syncing, Type Errors, or Server Component Issues
How to fix nuqs URL search params state management — useQueryState and useQueryStates setup, parsers, server-side access, shallow routing, history mode, and Next.js App Router integration.
Fix: React Hydration Error — Text Content Does Not Match
How to fix React hydration errors — server/client HTML mismatches, useEffect for client-only code, suppressHydrationWarning, dynamic content, and Next.js specific hydration issues.
Fix: Clerk Not Working — Auth Not Loading, Middleware Blocking, or User Data Missing
How to fix Clerk authentication issues — ClerkProvider setup, middleware configuration, useUser and useAuth hooks, server-side auth, webhook handling, and organization features.
Fix: cmdk Not Working — Command Palette Not Opening, Items Not Filtering, or Keyboard Navigation Broken
How to fix cmdk command palette issues — Dialog setup, custom filtering, groups and separators, keyboard shortcuts, async search, nested pages, and integration with shadcn/ui and Tailwind.