| name | Component Architecture |
| description | SEPARATE containers (data/business logic) from presentational (UI only) components. Prevent god components, prop drilling, and unnecessary client bundles. Custom hooks bridge data and UI. TanStack Query v5+ for all client data fetching. Server Components by default in Next.js. Trigger: "create (a|a) component", "build (the|a) UI", "refactor (the|the) frontend", component file exceeds 250 lines.
|
| category | frontend |
| version | 3.0.0 |
| last_updated | 2026-06-28T00:00:00.000Z |
| stacks | ["React 19.2","Vue 3","Next.js 16","Nuxt","SvelteKit"] |
| related_skills | ["full-stack-file-tree-architecture","empty-loading-error-states","accessibility-audit-protocol","react-composition","react-best-practices"] |
Component Architecture Patterns
IDENTIFY: When to Activate
Activate when:
- Building a new page or complex UI feature
- A component file exceeds 250 lines
- User says "refactor the frontend" or "split this component"
- Adding data-fetching logic to a UI component
CORE PRINCIPLE
Every component is one of two types:
| Type | Domain State? | UI State Only? | Side Effects? | Example |
|---|
| Container | Fetches, manages, or mutates domain data | May also have UI state | Yes | UserProfilePage, DashboardLayout |
| Presentational | NEVER touches domain data | May have local UI state | No | Button, Card, Collapsible, Tabs |
CRITICAL NUANCE: UI state (isOpen, activeTab) is OK in Presentational components. Domain data (user, orders, projects) is NOT.
RULE: A Presentational component MUST NEVER import data-fetching functions, API clients, or domain context consumers. Domain data flows in through props ONLY.
DECIDE: Architecture Selection
IF building from scratch →
RUN Steps 1-5 in order
IF component exceeds 250 lines →
RUN Step 6 (splitting workflow)
IF existing component has 5+ props →
CONSIDER composition (react-composition/SKILL.md)
IF component is pure UI (Button, Card, Input) →
SKIP to Step 2 (create Presentational only)
IF full-stack (Next.js App Router) →
PREFER Server Components (Step 5)
Only use 'use client' for interactive leaves
EXECUTE: Instructions
Step 1: Define Data Requirements First
Before writing any JSX, list what data the feature needs:
Feature: [name]
Data needed:
- [data source 1]
- [data source 2]
- [data source 3]
Sources:
- GET [endpoint 1]
- GET [endpoint 2]
Step 2: Create Presentational Components
Pure UI components that receive data through props:
type StatCardProps = {
label: string;
value: number;
trend?: 'up' | 'down';
};
export function StatCard({ label, value, trend }: StatCardProps) {
const [isExpanded, setIsExpanded] = useState(false);
return (
<div className="rounded-lg border p-4">
<p className="text-sm text-muted-foreground">{label}</p>
<p className="text-2xl font-bold">{value.toLocaleString()}</p>
</div>
);
}
Step 3: Create Custom Hook as Data Bridge
The idiomatic 2026 pattern, custom hooks bridge containers and presentational:
export function useDashboardData() {
const { data: user, isLoading: userLoading, error: userError } = useQuery({
queryKey: ['currentUser'],
queryFn: getCurrentUser,
});
const { data: orders, isLoading: ordersLoading, error: ordersError } = useQuery({
queryKey: ['recentOrders', 5],
queryFn: () => getRecentOrders(5),
});
return {
user,
orders,
isLoading: userLoading || ordersLoading,
error: userError || ordersError,
};
}
RULES for custom hooks:
- ALWAYS use TanStack Query (
useQuery, useMutation), NEVER raw fetch() in hooks
- ALWAYS return loading and error states
- ALWAYS use
useMutation for writes (form submits, updates, deletes)
- NEVER render JSX, hooks return data only
Step 4: Create the Container
For React SPA / Client Component (Vite, CRA, or interactive page):
'use client';
import { StatCard } from '@/components/ui/StatCard';
import { useDashboardData } from './useDashboardData';
export default function DashboardPage() {
const { user, orders, isLoading, error } = useDashboardData();
if (isLoading) return <DashboardSkeleton />;
if (error) return <ErrorState error={error} />;
if (!user) return <EmptyState />;
return (
<div className="grid gap-4 md:grid-cols-3">
<StatCard label="Recent Orders" value={orders?.length ?? 0} />
</div>
);
}
For Next.js 16 Server Components (PREFERRED, zero client JS for data):
import { StatCard } from '@/components/ui/StatCard';
import { getCurrentUser, getRecentOrders } from '@/server/queries';
export default async function DashboardPage() {
const [user, orders] = await Promise.all([
getCurrentUser(),
getRecentOrders(5),
]);
return (
<div className="grid gap-4 md:grid-cols-3">
<StatCard label="Recent Orders" value={orders.length} />
<StatCard label="Credits" value={user.credits} />
</div>
);
}
Step 5: Push 'use client' to Leaves: Default to Server Components
'use client';
export default function Page() { }
import { InteractiveForm } from './InteractiveForm';
export default function Page() {
return (
<div>
<h1>Settings</h1> {/* Server-rendered */}
<InteractiveForm /> {/* Client-rendered leaf */}
</div>
);
}
Step 6: Splitting a 250+ Line Component
When component exceeds 250 lines, follow this workflow:
1. EXTRACT custom hooks FIRST
Move ALL state logic + data fetching into use[Feature] hook
→ Often reduces component 30-50%
2. EXTRACT presentational sub-components
Identify distinct UI regions (header, sidebar, list, detail panel)
→ Extract each into own Presentational component
3. EXTRACT utility functions
Pure computations, formatters, validators → utils/ or lib/
4. SPLIT by concern if still too large
UserProfile → UserProfileView + UserProfileEdit + UserProfileActivity
MEASURABLE RULES
| Metric | Limit | Action |
|---|
| Component file lines | 250 | Split following Step 6 |
| Custom props per component | 5 | Use composition or group into object |
useState calls per component | 5 | Extract custom hook or use useReducer |
VALIDATE: Quality Gates
ANTI-PATTERNS: ALWAYS Avoid
| Anti-Pattern | Detection | Fix |
|---|
fetch() inside Button component | Couples UI to specific API | Pass onClick handler from Container |
6+ useState in one component | State unpredictable, hard to debug | Extract custom hook or useReducer |
| Props passed 5+ levels deep | Fragile refactoring, unclear flow | Use composition, hook, or state management |
'use client' on page layout | Forces entire page JS to client | Move to specific interactive leaf only |
| Hardcoding context in Card component | Card unusable outside that context | Accept data via props, not context |
| Extracting ALL state from Presentational | Over-engineering: accordion needs isOpen | UI state stays; domain state goes up |