4.6 KiB
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') DateobjectsMap,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 | - |