Back to all notes
Custom Hook Composition: Build Bigger Features from Small Hooks
Combine focused hooks to keep complex logic clean, testable, and reusable.
Introduction
Custom hook composition is about building complex behavior by combining smaller, focused hooks. Each hook solves a specific problem, and together they form a complete feature.
Why this matters
Big hooks become hard to read and reuse. Splitting logic into focused hooks and composing them makes features modular and easier to test.
The problem
Filtering logic and input state are interleaved inside the same component, leading to duplication when reused elsewhere.
Inefficient approach
Inline state and filtering with no reuse:
import { useState } from "react";
const ITEMS = ["apple","banana","cherry","date","elderberry","fig","grape"];
function App() {
const [query, setQuery] = useState("");
const filteredItems = ITEMS.filter(item =>
item.toLowerCase().includes(query.toLowerCase())
);
return (
<div>
<h1>Custom Hook Composition</h1>
<input
type="text"
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Search..."
/>
<ul>{filteredItems.map(i => <li key={i}>{i}</li>)}</ul>
</div>
);
}
export default App;
The solution
Create useInput for state, useFilter for memoized filtering, then compose them in useSearch.
import { useState, useMemo } from "react";
const ITEMS = ["apple","banana","cherry","date","elderberry","fig","grape"];
function useInput() {
const [query, setQuery] = useState("");
return { query, setQuery };
}
function useFilter(items, query) {
return useMemo(
() => items.filter((item) => item.toLowerCase().includes(query.toLowerCase())),
[items, query]
);
}
function useSearch(items) {
const { query, setQuery } = useInput();
const filteredItems = useFilter(items, query);
return { query, setQuery, filteredItems };
}
function App() {
const { query, setQuery, filteredItems } = useSearch(ITEMS);
return (
<div>
<h1>Custom Hook Composition</h1>
<input
type="text"
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Search..."
/>
<ul>{filteredItems.map(i => <li key={i}>{i}</li>)}</ul>
</div>
);
}
export default App;
Step-by-step
- Extract input state into
useInputreturning{ query, setQuery }. - Extract filtering into
useFilter(items, query)usinguseMemo. - Compose both in
useSearch(items)and return{ query, setQuery, filteredItems }. - Replace inline logic with the composed hook in components.
Tips
- Keep each hook responsible for a single concern.
- Return a stable, minimal API from each hook.
- Don’t fear small hooks—they compose into powerful features.
Knowledge base
Problem snippet:
const filteredItems = ITEMS.filter(item =>
item.toLowerCase().includes(query.toLowerCase())
);
Solution snippet:
const { query, setQuery, filteredItems } = useSearch(ITEMS);