Loading...
Loading...
[Pragmatic DDD Architecture] Guide for Server and Client components in Next.js App Router. Use when creating any .tsx file under presentation/components/, pages, or layouts — also when deciding whether to add "use client" to an existing component, passing data from a Server Component to a Client Component, composing Server content inside a Client slot, handling the VO serialization boundary, creating Compound Components, separating logic for Mobile/Desktop screens, or styling with `cva` and `cn`. Covers: Server vs Client decision, async Server Component patterns, creating getSession callbacks for Use Cases, Client Component restrictions, toBranded() boundary pattern, children slot composition, and props interface rules. Depends on 'use-cases' and 'server-actions'.
npx skill4agent add leif-sync/pragmatic-ddd componentsIMPORTANT: This skill defines architecture, rules, structure, component capabilities, constraints, and Value Object usage. It DOES NOT explain or restrict visual design or styling logic (e.g. CSS layout choices). You MAY use component libraries likeor similar inside your components.shadcn/ui
"use client"| Capability | Server Component | Client Component |
|---|---|---|
| ✅ | ❌ |
Value Object instantiation ( | ✅ | ✅ |
| Receive Class Instances as parameters | ✅ | ❌ (MUST use |
| ✅ | ❌ |
| ✅ | ❌ — MUST use |
| ❌ | ✅ |
Server-only modules (drizzle, | ✅ | ❌ |
| ✅ | ❌ |
Default to Server Components. Push theboundary as low in the tree as possible."use client"
PascalCaseLinksTable.tsxLinksTable"use client"shadcncvacncvacnshadcn/uiTableTableRowTableBodyTableCellTable.Row<Drawer><Dialog>function ContentList()useIsMobile()function ContentListMobile()function ContentListDesktop(){isMobile ? <Mobile> : <Desktop>}toBranded().toBranded()numberstringtoBranded().from()Result// In the VO definition
declare const uniqueBrand: unique symbol;
export type BrandedFolderId = string & { readonly [uniqueBrand]: never };
export class FolderId {
// ...
toBranded(): BrandedFolderId {
return this.value as BrandedFolderId;
// Caller receives a plain string at runtime,
// but TypeScript knows it came from a valid FolderId.
}
}.toBranded()// Server Component
import { FolderItem } from "./FolderItem"; // client component
export async function FolderList() {
const folders = /* from use case */;
return folders.map((folder) => (
<FolderItem
key={folder.getId().toBranded()}
folderId={folder.getId().toBranded()} // ✅ branded primitive
name={folder.getName().toBranded()} // ✅ plain string
/>
));
}"use client";
import { type BrandedFolderId } from "@/folders/domain/value-objects/folderId";
interface FolderItemProps {
folderId: BrandedFolderId; // branded primitive — valid by construction, no need to re-validate
name: string;
}
export function FolderItem({ folderId, name }: FolderItemProps) {
// folderId is a string at runtime, branded for type safety.
// Pass it directly to a server action — no .from() or error handling needed.
// The server action receives it as a string and reconstructs the VO internally.
}| Type | Serializable | How |
|---|---|---|
| ✅ | Directly |
Plain objects | ✅ | Directly (no class methods) |
| Arrays of the above | ✅ | Directly |
| ✅ | Passed as-is (Next.js handles it) |
| VO class instance | ❌ | Use |
| Functions / callbacks | ❌ | Not serializable — use Server Actions instead |
| ⚠️ | Omit the prop or use |
// ServerPage.tsx (server)
import { ClientWidget } from "./ClientWidget";
export default async function ServerPage() {
const data = await fetchData();
return <ClientWidget items={data.map((d) => ({ id: d.getId().toBranded(), name: d.getName().toBranded() }))} />;
}childrenchildren// Layout.tsx (server)
import { Sidebar } from "./Sidebar"; // client component
import { UserProfile } from "./UserProfile"; // server component
export default async function Layout({ children }: { children: ReactNode }) {
return (
<Sidebar>
<UserProfile /> {/* server component passed as children — valid */}
{children}
</Sidebar>
);
}// Sidebar.tsx (client)
"use client";
export function Sidebar({ children }: { children: ReactNode }) {
const [open, setOpen] = useState(true);
return <aside>{children}</aside>; // renders server components correctly
}Rule: Client Components must neverServer-only modules. Pass server-rendered content viaimportor other slot props instead.children
"use client"// ✅ Only the button is a client component
export async function FolderCard({ folder }: Props) {
return (
<div>
<h2>{folder.getName().toBranded()}</h2>
<DeleteFolderButton folderId={folder.getId().toBranded()} /> {/* client */}
</div>
);
}
// ❌ The entire card becomes client just because of one button
"use client";
export function FolderCard({ folder }: Props) { ... }interfacetypeany// ✅ Correct — serializable props for data coming from a Server Component
interface LinksTableProps {
links: {
id: BrandedUUID;
name: BrandedLinkName;
shortLink: BrandedShortLink;
destination: BrandedDestination;
clicks: BrandedNonNegativeInteger;
}[];
}
// ❌ Wrong — VO class in props passed from a Server Component (not serializable)
interface LinksTableProps {
links: ShortUrl[]; // ShortUrl is a class — cannot cross the RSC boundary
}"use client"asyncawait determineLocale()serviceContainer.toBranded()"use client"asyncuseLocale()determineLocale()router.refresh()window.location.reload()load-envauth.tsinterface.toBranded()"use client"children.agents/skills/components/references/