Software_development_department senior-frontend

Next.js App Router specific patterns — Server Components, Client Components boundary, parallel fetching, bundle analysis, a11y. Use ONLY for Next.js 13+ App Router projects. For generic React/Vue patterns, use `frontend-patterns` instead.

install
source · Clone the upstream repo
git clone https://github.com/tranhieutt/software_development_department
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/tranhieutt/software_development_department "$T" && mkdir -p ~/.claude/skills && cp -r "$T/.claude/skills/senior-frontend" ~/.claude/skills/tranhieutt-software-development-department-senior-frontend && rm -rf "$T"
manifest: .claude/skills/senior-frontend/SKILL.md
source content

Senior Frontend

Critical rules (non-obvious)

  • Always
    return
    in server components before client boundary
    — mixing async server + client state without boundaries causes hydration mismatches
  • priority
    on LCP images only
    — adding
    priority
    everywhere defeats preload budgets
  • use client
    at the leaf, not the root
    — push client boundary as deep as possible to maximize RSC tree
  • Parallel data fetching in Server Components: use
    Promise.all([...])
    at the page level, not sequential awaits
  • Bundle heavy deps:
    moment
    (290KB) →
    dayjs
    (2KB);
    lodash
    lodash-es
    with tree-shaking;
    axios
    → native
    fetch

Next.js: server vs client boundary

// Server Component (default) — fetch directly, no hooks
async function ProductPage({ params }: { params: { id: string } }) {
  const [product, reviews] = await Promise.all([  // parallel fetch
    getProduct(params.id),
    getReviews(params.id),
  ]);
  return (
    <div>
      <h1>{product.name}</h1>
      <Suspense fallback={<ReviewsSkeleton />}>
        <Reviews productId={params.id} />  {/* can defer slow queries */}
      </Suspense>
      <AddToCartButton productId={product.id} />  {/* client boundary at leaf */}
    </div>
  );
}

// Client Component — only where interactivity needed
"use client";
function AddToCartButton({ productId }: { productId: string }) {
  const [adding, setAdding] = useState(false);
  return <button onClick={() => addToCart(productId)}>Add to Cart</button>;
}

Next.js: config essentials

// next.config.js
const nextConfig = {
  images: {
    remotePatterns: [{ hostname: "cdn.example.com" }],
    formats: ["image/avif", "image/webp"],
  },
  experimental: {
    optimizePackageImports: ["lucide-react", "@heroicons/react"],  // tree-shake icon libs
  },
};

Component: TypeScript patterns

// Generic list component
function List<T extends { id: string }>({ items, renderItem }: {
  items: T[];
  renderItem: (item: T) => React.ReactNode;
}) {
  return <ul>{items.map(item => <li key={item.id}>{renderItem(item)}</li>)}</ul>;
}

// Props extending HTML element
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
  variant?: "primary" | "ghost" | "danger";
  isLoading?: boolean;
}

export function Button({ variant = "primary", isLoading, children, ...props }: ButtonProps) {
  return (
    <button {...props} disabled={props.disabled || isLoading} aria-busy={isLoading}
      className={cn("px-4 py-2 rounded font-medium focus-visible:ring-2",
        variant === "primary" && "bg-blue-600 text-white hover:bg-blue-700",
        variant === "danger" && "bg-red-600 text-white",
        (props.disabled || isLoading) && "opacity-50 cursor-not-allowed"
      )}>
      {isLoading && <Spinner aria-hidden />}
      {children}
    </button>
  );
}

Performance: bundle analysis

Common heavy deps to replace:

PackageSizeAlternative
moment290KB
dayjs
(2KB) or
date-fns
(12KB)
lodash71KB
lodash-es
(tree-shakeable)
axios14KBnative
fetch
or
ky
(3KB)
@mui/materialLargeshadcn/ui or Radix UI
# Analyze bundle
npx @next/bundle-analyzer  # or
npx vite-bundle-visualizer

Accessibility essentials

// Skip link — place before main nav
<a href="#main-content" className="sr-only focus:not-sr-only focus:absolute focus:top-4 focus:left-4">
  Skip to main content
</a>

// Icon button — always label
<button type="button" aria-label="Close dialog" className="focus-visible:ring-2">
  <XIcon aria-hidden="true" />
</button>

// Minimum contrast: 4.5:1 for text, 3:1 for UI components

Project structure (Next.js App Router)

app/
├── layout.tsx          # Root layout: fonts, providers, metadata
├── page.tsx
├── (auth)/             # Route group — no URL segment
│   ├── login/page.tsx
│   └── register/page.tsx
└── api/
    └── [route]/route.ts
components/
├── ui/                 # Button, Input, Card (reusable primitives)
└── features/           # Domain-specific composites
hooks/                  # useDebounce, useLocalStorage, useMediaQuery
lib/
├── utils.ts            # cn(), formatDate()
└── api.ts              # API client
types/                  # Shared TypeScript types