vM.

Fixing use client Boundary and Provider Errors in the Next.js App Router

Author
Vishal Maurya
Published on
Reading time
5 min read

Overview

You add useState to a component in the App Router and get an error saying the hook only works in a Client Component. Or you wrap your app in a context provider and createContext fails. Or you pass a function as a prop and see a serialization error. All three come from the same model: components are Server Components by default, and "use client" marks a boundary into client-side code.

1. Understand What the Boundary Means

In the App Router, every component in app/ is a Server Component unless it is imported by a file that starts with "use client". The directive marks a module as an entry point to the client bundle. Everything that module imports becomes part of the client graph too, so you do not need the directive in each child file.

Server Components cannot use state, effects, event handlers, or browser APIs. Client Components can use them, but cannot import server-only code.

2. Hooks and Event Handlers in the Wrong Place

Symptom: an error stating that useState, useEffect, or onClick requires a Client Component.

The fix is to isolate the interactive piece rather than marking a whole page as client:

// components/like-button.tsx
'use client';

import { useState } from 'react';

export function LikeButton({ initial }: { initial: number }) {
  const [count, setCount] = useState(initial);
  return <button onClick={() => setCount((c) => c + 1)}>Likes: {count}</button>;
}
// app/posts/[id]/page.tsx
import { LikeButton } from '@/components/like-button';

export default async function Post({ params }: { params: Promise<{ id: string }> }) {
  const { id } = await params;
  // fetch post on the server
  return (
    <article>
      <h1>Post {id}</h1>
      <LikeButton initial={0} />
    </article>
  );
}

Note that params is a Promise in newer Next.js versions; in Next.js 14 it is a plain object. Match your version.

3. Context Providers

createContext and useContext need a Client Component, and a layout is a Server Component. Put the provider in its own client file and render it from the layout:

// app/providers.tsx
'use client';

import { createContext, useContext, useState, type ReactNode } from 'react';

type Theme = 'light' | 'dark';
const ThemeContext = createContext<{ theme: Theme; setTheme: (t: Theme) => void } | null>(null);

export function Providers({ children }: { children: ReactNode }) {
  const [theme, setTheme] = useState<Theme>('light');
  return <ThemeContext.Provider value={{ theme, setTheme }}>{children}</ThemeContext.Provider>;
}

export function useTheme() {
  const ctx = useContext(ThemeContext);
  if (!ctx) throw new Error('useTheme must be used inside Providers');
  return ctx;
}
// app/layout.tsx
import { Providers } from './providers';

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <body>
        <Providers>{children}</Providers>
      </body>
    </html>
  );
}

Passing children through the provider is important. Server Components passed as children stay server-rendered; they are not pulled into the client bundle just because they sit inside a client provider.

The "must be used inside Providers" error is a good guard. If you see a null context at runtime, a component is rendering outside the provider, often in a different root layout or a portal.

4. Props Must Be Serializable

Props passed from a Server Component to a Client Component cross the network boundary. Plain objects, arrays, strings, numbers, booleans, dates, and similar serializable values work. Functions, class instances, and many library objects do not. The only functions allowed are Server Actions.

If you need to pass behavior, either define the handler inside the client component, or pass a Server Action marked with "use server".

5. Importing Server-Only Code Into the Client Graph

If a client file imports a module that reads environment secrets or uses fs, it can leak or break the build. Mark such modules explicitly:

// lib/db.ts
import 'server-only';

export async function getUser(id: string) {
  // database access here
}

The server-only package makes the build fail if a client module imports the file, which turns a silent leak into an early error.

6. Third-Party Components Without the Directive

Some libraries use hooks but do not declare "use client". Importing them from a Server Component fails. Wrap them in your own client file:

// components/chart-wrapper.tsx
'use client';

export { Chart } from 'some-chart-library';

Check the library's current release first; many have added the directive.

7. Keep the Boundary Low

Placing "use client" at the top of a page makes everything beneath it client code, increasing bundle size. Push the boundary down to the smallest interactive leaf: a button, a form, a dropdown. Fetch data in the server parent and pass it down as props.

Conclusion

Most boundary errors resolve once you ask two questions: does this component need interactivity, and does everything I pass to it serialize? Keep providers in small client files, keep the directive on leaves, and mark server-only modules explicitly.

If your App Router migration is producing confusing boundary errors or large client bundles, I can help review the component structure. Contact me.

Additional Resources

  • Server and Client Components
  • The use client directive
  • Composition patterns