Build headless UI hooks that encapsulate data fetching, state management, routing, and mutations without UI rendering...
Build headless UI hooks that encapsulate all application concerns (data fetching, state, routing, mutations) without any UI rendering logic. The hook is "headless" because it provides behavior without prescribing UI implementation.
A headless UI hook is a custom React hook that returns a structured object containing data, state, actions, and pending states. Pages consume these hooks and compose pure view components.
// The pattern: hook provides behavior, component provides UI
const { todos, handleCreate, isPending } = useTodosPage();
return <TodoList todos={todos} onCreate={handleCreate} isCreating={isPending.create} />;
// src/lib/hooks/use-todos-page.ts
export function useTodosPage() {
const { data: todos, isLoading } = useFetchTodos();
const createMutation = useCreateTodo();
const handleCreate = useCallback(async (title: string) => {
await createMutation.mutateAsync({ title });
}, [createMutation]);
return useMemo(() => ({
todos: todos ?? [],
isLoading,
handleCreate,
isPending: { create: createMutation.isPending },
}), [todos, isLoading, handleCreate, createMutation.isPending]);
}
// src/app/todos/page.tsx
export default function TodosPage() {
const { todos, isLoading, handleCreate, isPending } = useTodosPage();
return (
<TodoList
todos={todos}
isLoading={isLoading}
onCreate={handleCreate}
isCreating={isPending.create}
/>
);
}
Organize returns into clear sections:
| Section | Contents | Example |
|---|---|---|
| Data | Fetched data, computed values | todos, filteredTodos, selectedTodo |
| State | Local/URL state, loading/error | searchQuery, isEditing, isLoading |
| Actions | Async callbacks for mutations | handleCreate, handleSave, handleDelete |
| Pending | Loading states per mutation | isPending.create, isPending.update |
Domain-specific hooks handling a single domain's concerns:
useTodos() - todo data and mutationsuseNotes() - note data and mutationsCompose multiple atomic hooks into unified APIs:
useApp({ enabled: { todos: true, notes: true } })Full presenter hooks for specific pages:
useTodosPage() - everything needed for /todosuseNotesPage() - everything needed for /notesFor deeper understanding, explore these focused topics:
topics/hooks.mdPresenter Hooks
The foundational pattern for creating headless UI hooks. Covers hook structure, return object design, responsibilities, and API principles.
Start here if: You're new to headless UI or building your first presenter hook.
topics/composition.mdHook Composition
Compose multiple atomic hooks into aggregate hooks with opt-in enabled pattern. Covers atomic vs aggregate hooks, namespaced returns, and the enabled options object.
Start here if: You need to combine data from multiple domains on one page.
topics/conditional-fetching.mdConditional Fetching
Control when hooks fetch data using the enabled parameter. Covers React Query integration, lazy loading, and preventing over-fetching.
Start here if: You need to defer or conditionally load data.
topics/memoization.mdHook Memoization
Properly memoize return values and callbacks for stable references. Covers useMemo, useCallback, dependency management, and when NOT to memoize.
Start here if: You're seeing unnecessary re-renders or need to optimize hook performance.
See templates/ for starter code:
presenter-hook.ts - Full presenter hook template