← Back

When to Use Client Components

Server vs Client Components, CSS :has() for parent styling, data-pending attribute pattern.

PublishedMarch 5, 2026200 words
Edit

React Server Components

Server Components render on the server, can be async, and fetch data directly.

Example: BlogList

From app/page.tsx:

async function BlogList() { const posts = await getPublishedPosts(); return posts.map(post => <Card key={post.slug}>{post.title}</Card>); }
async function BlogList() { const posts = await getPublishedPosts(); return posts.map(post => <Card key={post.slug}

When to Use Client Components

Add 'use client' when you need interactivity—event handlers or React hooks.

From app/dashboard/_components/ArchiveButton.tsx:

'use client'; export function ArchiveButton({ slug, archived }) { const [optimisticArchived, setOptimisticArchived] = useOptimistic(archived); const isPending = optimisticArchived !== archived

Example: Composition with CSS :has()

Server Components render Client Components. Use CSS :has() to style parent elements based on child state—no state lifting required:

// PostList.tsx (Server Component) export async function PostList({ searchParams }) { const posts = await getPosts(validFilter); return posts.map(post => (

The has-data-pending: variant (Tailwind's :has([data-pending])) lets the Card react to the button's pending state without becoming a Client Component.

Keep Client Components at the leaves to maximize server rendering.

>
{
post
.
title
}
</
Card
>
)
;
}
;
return
(
<
form
data-pending
=
{
isPending
||
undefined
}
action
=
{
async
(
)
=>
{
let
newValue
;
setOptimisticArchived
(
current
=>
{
newValue
=
!
current
;
return
newValue
;
}
)
;
await
toggleArchivePost
(
slug
,
newValue
)
;
}
}
>
<
button
>
{
optimisticArchived
?
'Unarchive'
:
'Archive'
}
</
button
>
</
form
>
)
;
}
'use client'; export function ArchiveButton({ slug, archived }) { const [optimisticArchived, setOptimisticArchived] = useOptimistic(archived); const isPending = optimisticArchived !== archived; return ( <form data-pending={isPending || undefined} action={async () => { let newValue; setOptimisticArchived(current => { newValue = !current; return newValue; }); await toggleArchivePost(slug, newValue); }} > <button>{optimisticArchived ? 'Unarchive' : 'Archive'}</button> </form> ); }
<
Card
className
=
"
has-data-pending:animate-pulse has-data-pending:bg-muted/70
"
>
<
ArchiveButton
slug
=
{
post
.
slug
}
archived
=
{
post
.
archived
}
/>
</
Card
>
)
)
;
}
// PostList.tsx (Server Component) export async function PostList({ searchParams }) { const posts = await getPosts(validFilter); return posts.map(post => ( <Card className="has-data-pending:animate-pulse has-data-pending:bg-muted/70"> <ArchiveButton slug={post.slug} archived={post.archived} /> </Card> )); }