Fix: React Server Components Error — useState, Event Handlers, and Client Boundary Issues
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:
- 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 can’t cross the server/client boundary as props because they can’t be serialized to JSON. Server Components can’t 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’s serializable: strings, numbers, arrays, plain objects, and
null.Dateobjects,Map,Set,classinstances, andundefinedrequire special handling. - 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 can’t consume or provide context.
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 can’t pass function props (event handlers) to Client Components because functions aren’t 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 → 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 doesn’t 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 haven’t 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) doesn’t 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();For related Next.js issues, see Fix: Next.js App Router Fetch Not Caching or Always Stale, Fix: Next.js Hydration Failed, and Fix: Next.js Environment Variables Not Working.
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.