Headless UI Pattern Overview
Parent: Headless UI Architecture
Last Updated: 2024-12-01
Core Principle
ALL LOGIC IN HOOKS. COMPONENTS JUST RENDER.
This single principle drives the entire architecture and ensures maximum flexibility, testability, and reusability.
Architecture Layers
┌─────────────────────────────┐
│ Components (Thin Wrappers) │
│ - Just rendering │
│ - No logic │
└──────────┬──────────────────┘
│ useHook()
├─ data
├─ states
├─ computed
├─ actions
┌──────────┴──────────────────┐
│ Hooks (ALL Logic) │
│ - Business logic │
│ - State management │
│ - Data fetching │
│ - Computed values │
│ - Event handlers │
└──────────┬──────────────────┘
│ API calls
┌──────────┴──────────────────┐
│ API Layer (tRPC/REST) │
└─────────────────────────────┘
Hook Contract Pattern
Every hook follows a consistent interface pattern:
// 1. Options (input configuration)
export interface UseFeatureOptions {
id: string;
mode?: 'compact' | 'full';
onComplete?: () => void;
}
// 2. Return value (exposed API)
export interface UseFeatureReturn {
data: Data | undefined;
isLoading: boolean;
error: Error | null;
computedValue: string;
actions: {
refresh: () => void;
update: (data: Data) => Promise<void>;
};
}
// 3. Implementation (ALL logic here)
export function useFeature(
options: UseFeatureOptions
): UseFeatureReturn {
// Business logic
// State management
// Data fetching
// Computed values
// Event handlers
return {
data,
isLoading,
error,
computedValue,
actions: { refresh, update }
};
}
Component Pattern
Components are thin wrappers that just render:
export function FeatureComponent({ id, mode }: Props) {
// 1. Get EVERYTHING from hook
const { data, isLoading, computedValue, actions } =
useFeature({ id, mode });
// 2. Just render (conditional rendering OK)
if (isLoading) return <Skeleton />;
if (!data) return <EmptyState />;
return (
<Card title={computedValue}>
<Content data={data} />
<Button onClick={actions.refresh}>Refresh</Button>
</Card>
);
}
What Components CAN Do
- ✅ Call hooks
- ✅ Conditional rendering
- ✅ Map data to JSX
- ✅ Pass hook actions to event handlers
- ✅ Compose other components
What Components CANNOT Do
- ❌ Business logic
- ❌ State management
- ❌ Data fetching
- ❌ Computed values
- ❌ Complex event handlers
Comparison
❌ Monolithic (Anti-Pattern)
function FeatureComponent({ id }: Props) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
// Fetch logic mixed with component
fetchData(id).then(setData).catch(setError).finally(() => setLoading(false));
}, [id]);
const handleSave = async () => {
// Business logic in component
try {
await saveData(data);
toast.success('Saved');
} catch (e) {
toast.error('Failed');
}
};
if (loading) return <Loading />;
return <Card>...</Card>;
}
Problems:
- Logic tied to React
- Can't test without rendering
- Can't reuse logic
- Hard to maintain
- Mixed concerns
✅ Headless (Correct Pattern)
// Hook: ALL logic (reusable, testable)
function useFeature({ id }: Options) {
const { data, isLoading, error } = useQuery(['feature', id]);
const handleSave = useCallback(async () => {
try {
await saveData(data);
toast.success('Saved');
} catch (e) {
toast.error('Failed');
}
}, [data]);
return { data, isLoading, error, handleSave };
}
// Component: Just rendering
function FeatureComponent({ id }: Props) {
const { data, isLoading, handleSave } = useFeature({ id });
if (isLoading) return <Loading />;
return <Card><Button onClick={handleSave}>Save</Button></Card>;
}
Benefits:
- Logic extracted and reusable
- Easy to test separately
- Clear separation of concerns
- Framework agnostic
- Maintainable
File Organization
src/
├── hooks/
│ ├── useFeature.ts # Business logic
│ ├── useDataFetch.ts # Data fetching
│ └── useValidation.ts # Validation logic
├── components/
│ ├── Feature.tsx # UI component
│ ├── FeatureCard.tsx # Sub-component
│ └── FeatureList.tsx # List component
└── tests/
├── hooks/
│ ├── useFeature.test.ts # Hook tests
│ └── useValidation.test.ts
└── components/
└── Feature.test.tsx # Component tests
Principles:
- Hooks in
hooks/directory - Components in
components/directory - Tests mirror structure
- Clear naming convention
Related Documentation
- Pattern Benefits - Why this pattern works
- Testing Strategy - How to test hooks and components
- Best Practices - Do's and don'ts
- Complete Examples - Real-world implementations
Status: Core Pattern
Last Updated: 2024-12-01