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, oronSubmit? -
State: Does it need React hooks like
useState,useReducer, oruseTransition? -
Effects: Does it need lifecycle hooks like
useEffectoruseLayoutEffect? -
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>
);
}
✅ 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>
);
}
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>
);
}
💡 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 (likepage.tsx) can read thesesearchParamsfrom 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 ownuseState. - 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
asyncfunction with"use server". A Client Component can then import and call this function directly from anonClickoronSubmit. -
Minimal Boilerplate: You use the
useTransitionhook to get aisPendingstate. No moreuseStatefor 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 theawait 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 theuseStore()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)