Hook Composition: Building Libraries from Hooks
The first time I tried composing hooks, I thought I was being clever. I nested a hook inside another, passed config all over the place, and felt like a wizard. Until I had to debug it. Yikes. Suddenly, I had no idea what triggered what, or why some state wasn't syncing. Turns out, building a hook library is a lot like building a Lego spaceship—you need a plan. In this lesson, we’ll talk about composing React hooks properly, avoiding common traps, and creating utilities that are actually fun (and safe) to reuse.
Composing Hooks Like Functions (Because That’s What They Are)
Here’s the golden rule: Hooks are just functions.
That means you can—and should—compose them like you'd compose any utility. Build small, focused hooks, then combine them into something more powerful.
Let’s say you have these two:
function useAuth() {
const [user, setUser] = useState(null);
// pretend there's auth logic
return { user, login: () => {}, logout: () => {} };
}
function useTheme() {
const [theme, setTheme] = useState('light');
return { theme, toggleTheme: () => setTheme(t => t === 'light' ? 'dark' : 'light') };
}
Now let’s compose them:
function useAppContext() {
const auth = useAuth();
const theme = useTheme();
return { ...auth, ...theme };
}
Inside your component:
const { user, login, logout, theme, toggleTheme } = useAppContext();
Clean. Reusable. Centralized.
It’s like building tiny Lego bricks and snapping them together into a spaceship that doesn’t explode on launch.
Creating Higher-Order Hooks (They’re Not as Scary as They Sound)
A higher-order hook is just a function that returns a hook—kind of like how a higher-order component wraps a component.
Let’s say you have a hook that needs to behave differently based on config:
function withLogging(useHook, name) {
return function(...args) {
const result = useHook(...args);
useEffect(() => {
console.log(`[${name}] mounted`);
return () => console.log(`[${name}] unmounted`);
}, []);
return result;
};
}
Use it like this:
const useLoggedInput = withLogging(useInput, 'useInput');
function Form() {
const input = useLoggedInput('');
return <input {...input} />;
}
It’s kinda meta, but incredibly useful for adding common behavior to a family of hooks—logging, analytics, performance tracing, etc.
Just don’t go overboard. You’re composing logic, not creating inception.
Sharing Config and State Across Hooks (Without Losing Your Mind)
Sometimes your hooks need to talk to each other. Maybe usePagination
and useSorting
both rely on the same table config. You don’t want to duplicate that.
Here’s what I usually do: create a wrapper hook to hold the shared state, and pass it down.
function useTableConfig({ defaultSort = 'name' } = {}) {
const [sort, setSort] = useState(defaultSort);
const [page, setPage] = useState(1);
return { sort, setSort, page, setPage };
}
function useSorting(config) {
const { sort, setSort } = config;
// sorting logic
return { sort, setSort };
}
function usePagination(config) {
const { page, setPage } = config;
// pagination logic
return { page, setPage };
}
function useTable() {
const config = useTableConfig();
const sorting = useSorting(config);
const pagination = usePagination(config);
return { ...sorting, ...pagination };
}
Everything shares the same source of truth. No drift. No duplication.
This pattern saved me from serious headaches when I was building a filterable, sortable table for a client’s admin panel. Before I used this approach, I had two separate pieces of state competing like divorced parents. Ugly.
The Hook Library Pattern (And When to Drop the Fancy Stuff)
Some teams go full-on and build a hook library—a folder full of well-tested, documented, and shareable logic hooks.
This is awesome when:
You work on multiple projects or micro-frontends. You want to abstract common UI patterns (pagination, modals, fetchers). * You’re collaborating with other developers who don’t want to reinvent the wheel.
But here’s my hot take: don’t reach for a hook library unless you actually need one.
I've seen teams spend days building an “amazing” shared hook for modals… used once. A basic useState
would've done. Don’t make your logic “generic” until it has to be.
Keep it stupid simple (KISS), and only abstract once you hit the “ugh I’ve written this three times” point.
Mistakes to Avoid When Composing Hooks (Yes, I Made Them All)
1. Conditionally calling hooks inside other hooks. Nope. Never. Don’t do it. React will scream, and your app will break in mysterious ways.
2. Trying to over-optimize too early. Premature abstraction is like premature celebration. You’ll probably regret it.
3. Creating black boxes. If your composed hook does five things and hides them all, you’re making debugging a nightmare. Return named values, add logging, or expose internal state as needed.
4. Forgetting about dependencies. If one hook relies on another’s value, be careful with memoization and effects. Things can desync fast.
And the worst one?
5. Writing a “library” that’s just a pile of helper code with no structure. If you’re gonna do it, do it right. Separate concerns, write docs, write tests.
Next up: what happens when you throw fetch
into your custom hook soup?
Welcome to the world of race conditions, stale data, and loading state juggling.
Jump to Handling Async in Hooks (Without Going Mad)