Engineering Frontend Architecture
Markdown--- name: engineering-frontend-architecture description: Designs and implements Next.js/React/TypeScript frontend architectures including component trees, state management, routing flows, and performance optimizations. Use when building new frontend features, structuring a React/Next.js app, planning UI architecture, or reviewing frontend code for accessibility, responsiveness, and performance. ---
Given a feature request, produce these five artifacts in order:
- Component tree — hierarchy with responsibilities
- State management — what's local, what's global, what's server state
- Routing flow — Next.js App Router structure, layouts, loading/error states
- UI architecture — styling approach, design tokens, accessibility notes
- Performance optimization — rendering strategy, code splitting, caching
Example request: "Build a product listing page with filters and infinite scroll."
ProductListPage (Server Component)
├── FilterSidebar (Client Component - needs interactivity)
│ ├── CategoryFilter
│ ├── PriceRangeFilter
│ └── ClearFiltersButton
├── ProductGrid (Client Component - infinite scroll)
│ ├── ProductCard[] (memoized, pure)
│ └── LoadMoreSentinel (IntersectionObserver)
└── EmptyState (conditional)
State: filters in URL search params (shareable, back-button friendly), product data via
useInfiniteQuery (TanStack Query), UI toggle state (mobile filter drawer) local useState.
Progress:
- Clarify requirements: rendering target (SSR/SSG/CSR), data source, interactivity needs
- Design component tree — mark each node Server or Client Component
- Define state ownership — local vs URL vs global store vs server cache
- Map routing — file structure, dynamic segments, layouts, parallel/intercepting routes if needed
- Specify UI architecture — styling system, design tokens, a11y requirements
- Identify performance levers — streaming, lazy loading, memoization, image/font optimization
- Flag risks — bundle size, waterfalls, hydration mismatches, accessibility gaps
1. Component Tree
- Default to Server Components; promote to Client Component only when needed (hooks, events, browser APIs)
- Keep client boundaries as low/small as possible ("client islands")
- Name components by responsibility, not implementation (
ProductGridnotDiv1)
2. State Management
Decide category for each piece of state:
- Local UI state →
useState/useReducer - Shared client state (theme, modals, cart drawer) → Context or lightweight store (Zustand)
- Server/remote data → TanStack Query or Next.js server components +
fetchcache - Shareable/bookmarkable state (filters, pagination, tabs) → URL search params
- Form state → React Hook Form + schema validation (Zod)
Avoid putting server data into global client stores — causes staleness bugs.
3. Routing Flow (App Router)
app/
products/
page.tsx # list page (Server Component)
layout.tsx # shared shell, nav
loading.tsx # streaming skeleton
error.tsx # error boundary
[id]/
page.tsx # detail page
not-found.tsx
- Use
loading.tsx+ Suspense for streaming instead of full-page spinners - Use route groups
(marketing)for layout-only segregation without affecting URL - Prefer
generateStaticParamsfor known dynamic routes (SSG); fallback to on-demand for the rest
4. UI Architecture
- Styling: Tailwind CSS for utility-first speed, or CSS Modules for scoped component styles — pick one per project, don't mix
- Design tokens (spacing, color, typography) centralized in
tailwind.config.tsor atokens.ts - Accessibility baseline for every component:
- Semantic HTML first (
button,nav,dialog) before ARIA - Visible focus states, keyboard navigable (Tab/Enter/Escape)
- Labels/
aria-labelon icon-only controls - Color contrast ≥ 4.5:1 for text
- Semantic HTML first (
- Responsive: mobile-first breakpoints, avoid fixed pixel widths, test at 375px/768px/1440px
5. Performance Optimization
- Default to Server Components to cut client JS
next/dynamicfor below-the-fold or heavy client components (charts, editors)next/imagewith explicitsizes,next/fontfor zero layout-shift fonts- Memoize expensive list renders (
React.memo, stable keys, virtualization for 100+ items) - Avoid client-side waterfalls — fetch in parallel with
Promise.allor parallel routes - Measure: Lighthouse/Web Vitals (LCP, INP, CLS) before/after
Example 1: Input: "Dashboard with real-time notifications, sidebar nav, and a data table with sorting/pagination." Output:
DashboardLayout (Server Component, layout.tsx)
├── Sidebar (Server Component, static nav links)
├── NotificationBell (Client Component — WebSocket subscription, local unread-count state)
└── DashboardPage (Server Component)
└── DataTable (Client Component — sort/pagination interactivity)
├── TableHeader (sortable columns → state in URL search params: ?sort=name&dir=asc)
├── TableRow[] (memoized)
└── Pagination (page number synced to URL)
State: notifications via WebSocket + local reducer; table sort/page in URL (shareable, refresh-safe); initial table data fetched server-side and hydrated, subsequent pages via client fetch. Performance: virtualize table if >200 rows; debounce sort-triggered refetch.
Example 2: Input: "Multi-step checkout form with validation." Output:
CheckoutPage (Client Component boundary starts here — form needs interactivity)
├── StepIndicator
├── ShippingForm
├── PaymentForm
└── ReviewStep
State: React Hook Form with Zod schema, one form instance spanning all steps (avoid state loss
between steps); current step in local useState (not URL, to avoid users bookmarking mid-payment);
submit triggers server action.
Accessibility: each step is a fieldset with legend; announce step changes via aria-live region.
- Server Components by default; justify every
"use client"directive - One source of truth per piece of state — never duplicate server data into local state
- Co-locate components with the route that uses them; only lift to
components/when reused - Design for keyboard-first, then verify with screen reader (VoiceOver/NVDA)
- Optimize after measuring — don't micro-optimize without a Lighthouse/profiler baseline
- Marking entire pages
"use client"just because one child needs interactivity — extract that child instead - Storing server data (fetched lists, API responses) in Zustand/Context — leads to stale/duplicated state
- Using
div onClickinstead ofbutton— breaks keyboard access and screen readers - Forgetting
loading.tsx/Suspense boundaries — causes blank-screen waits instead of streaming - Over-fetching in client components when data could be fetched server-side and passed as props
- Ignoring CLS from images/fonts — always set dimensions and use
next/font