DEV Community

TJ Coding
TJ Coding

Posted on

How to Decide When to Use "use client"

By default, every component in the App Router is a Server Component (RSC). This is the new "RSC-first" approach. You should only opt into "use client" when you absolutely have to.

Think of "use client" as a boundary. It tells React, "Everything in this file and everything it imports will be a Client Component, with its JavaScript sent to the browser."

Here is the simple litmus test. Ask yourself if your component needs:

  • Interactivity: Does it use event handlers like onClick, onChange, or onSubmit?
  • State: Does it need React hooks like useState, useReducer, or useTransition?
  • Effects: Does it need lifecycle hooks like useEffect or useLayoutEffect?
  • Browser-Only APIs: Does it need to access window, localStorage, or other browser APIs?
  • Context: Does it need to use (consume) Context with useContext? (Note: Providing context also requires "use client").

If you answer "yes" to any of these, it must be a Client Component.


The Golden Rule: Push "use client" to the Leaves

The most important best practice is to push your Client Components as far down the component tree as possible.

Don't put "use client" at the top of your main page. Instead, keep your page as a Server Component and import smaller, interactive "island" components into it.

❌ Bad Example (Making the whole page a Client Component)

// app/page.tsx
"use client"; // <-- Unnecessary! Now the whole page is client-side.

import { useState } from "react";
import { MyData } from "./get-data";

export default function Page() {
  const [count, setCount] = useState(0);

  // This data fetching is now running on the client!
  const data = MyData.get(); 

  return (
    <div>
      <h1>My Page</h1>
      <p>Data: {data.info}</p>
      {/* This is the only part that actually needed state */}
      <button onClick={() => setCount(count + 1)}>
        Count: {count}
      </button>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

✅ Good Example (Isolating the Client Component)

Step 1: Your page remains a Server Component (the default). It can do fast, server-side data fetching.

// app/page.tsx
// NO "use client" directive

import { MyData } from "./get-data";
import { Clicker } from "./ui/clicker"; // Import the client part

// This function runs ON THE SERVER
async function getData() {
  const data = await MyData.get();
  return data;
}

export default async function Page() {
  // Data is fetched on the server before the page is sent
  const data = await getData();

  return (
    <div>
      <h1>My Page (from the server)</h1>
      <p>Data: {data.info} (from the server)</p>

      {/* We import just the one interactive piece */}
      <Clicker />
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Step 2: You create a tiny "island" component that handles only the interactivity.

// app/ui/clicker.tsx
"use client"; // <-- The boundary is as small as possible

import { useState } from "react";

export function Clicker() {
  const [count, setCount] = useState(0);

  return (
    <button onClick={() => setCount(count + 1)}>
      Count: {count} (from the client)
    </button>
  );
}
Enter fullscreen mode Exit fullscreen mode

💡 How to Manage State with Minimal Boilerplate (RSC-First)

Your instinct is correct: the old way of wrapping your entire app in global state providers (<ReduxProvider>, <AppContext.Provider>) is now an anti-pattern. Doing so would force your entire application to be a Client Component.

Here is the new "RSC-first" state management hierarchy, from most-preferred to least-preferred.

1. For Global State (Filters, Modals, Tabs): Use the URL

This is the number one, minimal-boilerplate way to manage global state. The URL is the state manager.

  • How: Use React's built-in useSearchParams() hook (in a Client Component) to update the URL. Your Server Components (like page.tsx) can read these searchParams from their props and re-fetch or re-render with the new state.
  • Best for: Search queries, filters, pagination, "is this modal open" (?modal=login), active tabs.
  • Why it's great: It's shareable, refresh-proof, and requires zero external libraries.

2. For UI State (Toggles, Dropdowns): Keep it Local

  • How: Just like the <Clicker /> example above. Create a tiny Client Component with its own useState.
  • Best for: "Is this dropdown open?", "What's the value of this input field?"
  • Why it's great: It's simple, encapsulated, and has zero impact on the rest of your app.

3. For Data Mutations (Changing Data): Use Server Actions

This is the new way to handle form submissions or any action that changes data, and it dramatically reduces boilerplate.

  • How: You define an async function with "use server". A Client Component can then import and call this function directly from an onClick or onSubmit.
  • Minimal Boilerplate: You use the useTransition hook to get a isPending state. No more useState for loading, error, and data. React handles the pending state for you.
  • Best for: Form submissions, "Add to Cart" buttons, "Delete Post" buttons.

4. For Passing Data Down: Fetch in RSC, Pass as Props

  • How: Your top-level Server Component (like page.tsx) does the await fetch(...). It then passes the resulting data as props down to any Client Components that need to display it.
  • Why it's great: This completely eliminates all client-side data-fetching state (isLoading, error, data). The data is just there when the component renders.

5. For True Global Client State (e.g., Theme, Cart): Use Zustand/Jotai

Sometimes you just need a global, client-side-only state that isn't in the URL (like a theme toggle or a shopping cart).

  • How: Use a minimal library like Zustand. It doesn't require a <Provider> at the root of your app. You can just create a store and call the useStore() hook in any Client Component that needs it.
  • Why it's great: It's minimal, avoids the "wrap the whole app" anti-pattern, and works perfectly alongside Server Components.

Summary: State Management Cheat Sheet

State Type ✅ Recommended Solution (Minimal Boilerplate)
Global State (Filters, search, tabs) Use the URL (useSearchParams or nuqs)
Local UI State (Toggles, inputs) Local useState (in a small Client Component)
Data Mutations (Form submits) Server Actions (with useTransition)
Server Data (Displaying data) Fetch in RSC (and pass down as props)
Global UI State (Theme, cart) Zustand (or Jotai)

Would you like a specific code example for one of these patterns, like using Server Actions or managing URL state?

Top comments (0)