Files
stack/packages/mosaic/framework/skills/next-best-practices/rsc-boundaries.md
T
fargo 1a822493ba format: apply repo prettier (3.8.1) to the folded skills tree
963 markdown files reformatted with the repository's pinned prettier so
pnpm format:check covers the folded tree like every other repo file.

The formatter's embedded-language pass also normalized code fences
(TS semicolons, closed HTML tags in examples, lowercased CSS hex colors,
one renumbered list that skipped an index). Alphanumeric token deltas vs
the fold commit were audited file-by-file; all are formatter-equivalent
markup normalizations plus the four sanitized skills.
2026-08-19 14:37:17 -05:00

4.6 KiB

RSC Boundaries

Detect and prevent invalid patterns when crossing Server/Client component boundaries.

Detection Rules

1. Async Client Components Are Invalid

Client components cannot be async functions. Only Server Components can be async.

Detect: File has 'use client' AND component is async function or returns Promise

// Bad: async client component
'use client';
export default async function UserProfile() {
  const user = await getUser(); // Cannot await in client component
  return <div>{user.name}</div>;
}

// Good: Remove async, fetch data in parent server component
// page.tsx (server component - no 'use client')
export default async function Page() {
  const user = await getUser();
  return <UserProfile user={user} />;
}

// UserProfile.tsx (client component)
('use client');
export function UserProfile({ user }: { user: User }) {
  return <div>{user.name}</div>;
}
// Bad: async arrow function client component
'use client';
const Dashboard = async () => {
  const data = await fetchDashboard();
  return <div>{data}</div>;
};

// Good: Fetch in server component, pass data down

2. Non-Serializable Props to Client Components

Props passed from Server → Client must be JSON-serializable.

Detect: Server component passes these to a client component:

  • Functions (except Server Actions with 'use server')
  • Date objects
  • Map, Set, WeakMap, WeakSet
  • Class instances
  • Symbol (unless globally registered)
  • Circular references
// Bad: Function prop
// page.tsx (server)
export default function Page() {
  const handleClick = () => console.log('clicked');
  return <ClientButton onClick={handleClick} />;
}

// Good: Define function inside client component
// ClientButton.tsx
('use client');
export function ClientButton() {
  const handleClick = () => console.log('clicked');
  return <button onClick={handleClick}>Click</button>;
}
// Bad: Date object (silently becomes string, then crashes)
// page.tsx (server)
export default async function Page() {
  const post = await getPost();
  return <PostCard createdAt={post.createdAt} />; // Date object
}

// PostCard.tsx (client) - will crash on .getFullYear()
('use client');
export function PostCard({ createdAt }: { createdAt: Date }) {
  return <span>{createdAt.getFullYear()}</span>; // Runtime error!
}

// Good: Serialize to string on server
// page.tsx (server)
export default async function Page() {
  const post = await getPost();
  return <PostCard createdAt={post.createdAt.toISOString()} />;
}

// PostCard.tsx (client)
('use client');
export function PostCard({ createdAt }: { createdAt: string }) {
  const date = new Date(createdAt);
  return <span>{date.getFullYear()}</span>;
}
// Bad: Class instance
const user = new UserModel(data)
<ClientProfile user={user} /> // Methods will be stripped

// Good: Pass plain object
const user = await getUser()
<ClientProfile user={{ id: user.id, name: user.name }} />
// Bad: Map/Set
<ClientComponent items={new Map([['a', 1]])} />

// Good: Convert to array/object
<ClientComponent items={Object.fromEntries(map)} />
<ClientComponent items={Array.from(set)} />

3. Server Actions Are the Exception

Functions marked with 'use server' CAN be passed to client components.

// Valid: Server Action can be passed
// actions.ts
'use server';
export async function submitForm(formData: FormData) {
  // server-side logic
}

// page.tsx (server)
import { submitForm } from './actions';
export default function Page() {
  return <ClientForm onSubmit={submitForm} />; // OK!
}

// ClientForm.tsx (client)
('use client');
export function ClientForm({ onSubmit }: { onSubmit: (data: FormData) => Promise<void> }) {
  return <form action={onSubmit}>...</form>;
}

Quick Reference

Pattern Valid? Fix
'use client' + async function No Fetch in server parent, pass data
Pass () => {} to client No Define in client or use server action
Pass new Date() to client No Use .toISOString()
Pass new Map() to client No Convert to object/array
Pass class instance to client No Pass plain object
Pass server action to client Yes -
Pass string/number/boolean Yes -
Pass plain object/array Yes -