# Next.js Component Standards

> **Scope:** frontend/nextjs/components
> **Layer:** 2 (on keyword)
> **Keywords:** component, shadcn, reusable, shared, ui, three-tier
> **Load When:** creating or editing React components

**Verified against:** Next.js 15 + shadcn/ui CLI + TanStack Table 8. Last-verified: 2026-05-20.

---

Three-tier component hierarchy. `components/ui/` is shadcn primitives (never edit). `components/` is composed shared (no business logic). `features/*/components/` is feature-scoped.

## Core Rules

- NEVER edit files in `components/ui/` — they are regenerated by shadcn CLI
- NEVER import from `features/` inside `components/` — components know nothing about domain
- ALWAYS compose shadcn primitives in `components/` instead of editing them
- ALWAYS add `'use client'` only if the component uses hooks, events, or browser APIs
- NEVER pass raw API data directly to a component — transform to props first

## Three-Tier Hierarchy

```
Tier 1: components/ui/         ← shadcn/ui CLI output (DO NOT EDIT)
           ↓ composed into
Tier 2: components/            ← shared project components (no business logic)
           ↓ used by
Tier 3: features/*/components/ ← feature-scoped (knows about users, billing, etc.)
```

## Tier 1 — shadcn/ui Primitives

```bash
# Add shadcn components via CLI — never write them manually
npx shadcn@latest add button card dialog form input table
```

If you need to change a shadcn component's behavior, wrap it — do not edit the source file.

## Tier 2 — Shared Composed Components

```tsx
// components/data-table.tsx — composes shadcn Table + TanStack Table
'use client';

import {
  Table, TableBody, TableCell, TableHead, TableHeader, TableRow
} from '@/components/ui/table';
import {
  type ColumnDef, flexRender, getCoreRowModel, useReactTable
} from '@tanstack/react-table';

interface DataTableProps<TData> {
  columns: ColumnDef<TData>[];
  data: TData[];
}

export function DataTable<TData>({ columns, data }: DataTableProps<TData>) {
  const table = useReactTable({ data, columns, getCoreRowModel: getCoreRowModel() });
  return (
    <Table>
      <TableHeader>
        {table.getHeaderGroups().map((hg) => (
          <TableRow key={hg.id}>
            {hg.headers.map((h) => (
              <TableHead key={h.id}>
                {flexRender(h.column.columnDef.header, h.getContext())}
              </TableHead>
            ))}
          </TableRow>
        ))}
      </TableHeader>
      <TableBody>
        {table.getRowModel().rows.map((row) => (
          <TableRow key={row.id}>
            {row.getVisibleCells().map((cell) => (
              <TableCell key={cell.id}>
                {flexRender(cell.column.columnDef.cell, cell.getContext())}
              </TableCell>
            ))}
          </TableRow>
        ))}
      </TableBody>
    </Table>
  );
}
```

## Tier 3 — Feature Components

```tsx
// features/users/components/user-list.tsx
'use client';

import { DataTable } from '@/components/data-table';
import { useUsers } from '@/features/users/hooks/use-users';
import type { ColumnDef } from '@tanstack/react-table';
import type { User } from '@/features/users/types/user.types';

const columns: ColumnDef<User>[] = [
  { accessorKey: 'name', header: 'Name' },
  { accessorKey: 'email', header: 'Email' },
];

export function UserList() {
  const { data: users = [], isLoading } = useUsers();
  if (isLoading) return <div>Loading...</div>;
  return <DataTable columns={columns} data={users} />;
}
```

## Component Props Conventions

```tsx
interface UserCardProps {
  user: User;
  onEdit?: (id: string) => void;
  className?: string; // Always allow className for Tailwind override
}

export function UserCard({ user, onEdit, className }: UserCardProps) {}
```

## Common Mistakes

| Wrong | Right | Why |
|-------|-------|-----|
| Edit `components/ui/button.tsx` | Wrap in `components/action-button.tsx` | shadcn CLI overwrites it |
| `import { useUsers } from '@/features/users'` in `components/` | Move to a feature component | Breaks tier isolation |
| `'use client'` on all components | Only on interactive ones | Unnecessary client JS |
| Pass raw fetch response as prop | Type and validate with Zod first | Runtime type safety |

---

*MORPH-SPEC by Polymorphism Tech*
