Skip to content

Fix: React Server Components Error — useState, Event Handlers, and Client Boundary Issues

FixDevs ·

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 ComponentsuseState, 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. Date objects, Map, Set, class instances, and undefined require special handling.
  • Async Client Components — async/await is only supported in Server Components. Client Components must use useEffect or 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} />;  // OK

Fix 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.

F

FixDevs

Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.

Was this article helpful?

Related Articles