Sharing Logic with Hooks
Custom hooks shine when the same combination of state and effects appears in multiple components. Extracting it into a hook gives you a single, tested implementation instead of copy-pasted code.
Hooks vs utility functions
Section titled “Hooks vs utility functions”A plain utility function can share pure computation:
export function formatCurrency(n) { return Number(n || 0).toLocaleString('en-US', { style: 'currency', currency: 'USD' });}This has no state, no side effects — just a calculation. Use it as a regular function.
A custom hook shares stateful behavior — when you need useState, useEffect, useMemo, or another hook as part of the shared logic:
export function useLocalStorage(key, initialValue) { const [value, setValue] = useState(() => { try { return JSON.parse(localStorage.getItem(key)) ?? initialValue; } catch { return initialValue; } });
const set = useCallback((next) => { const v = next instanceof Function ? next(value) : next; setValue(v); localStorage.setItem(key, JSON.stringify(v)); }, [key, value]);
return [value, set];}useLocalStorage combines useState and a custom setter that writes to storage. This pattern repeats wherever you need persistence — put it in a hook once.
Composing hooks
Section titled “Composing hooks”Hooks can use other hooks. useBudget uses useLocalStorage, which uses useState:
export function useBudget() { const [currentYear, setCurrentYear] = useLocalStorage('zb-year', new Date().getFullYear()); const [currentMonth, setCurrentMonth] = useLocalStorage('zb-month', new Date().getMonth());
const key = `zb-${currentYear}-${String(currentMonth + 1).padStart(2, '0')}`; const [monthData, setMonthData] = useLocalStorage(key, emptyMonth());
// ... rest of budget logic}Each layer of abstraction handles one thing. useLocalStorage handles persistence. useBudget handles budget domain logic. App handles layout.
Returning the right interface
Section titled “Returning the right interface”Return only what callers need. A hook that returns 30 items forces callers to understand all of them. Group related items into objects when it improves clarity:
return { // Current state currentYear, currentMonth, incomeSources, categories, transactions, // Derived totalIncome, totalBudgeted, leftToAssign, spentByCategory, // Actions addIncome, updateIncome, deleteIncome, addCategory, updateCategory, deleteCategory, addTransaction, deleteTransaction, goToPrevMonth, goToNextMonth, copyFromLastMonth, resetMonth,};Testing custom hooks
Section titled “Testing custom hooks”Because hooks are just functions, you can test the logic without rendering any UI. Libraries like @testing-library/react-hooks or the renderHook utility in React Testing Library let you call hooks in isolation and assert on their return values.
This is one of the biggest advantages of custom hooks over keeping logic in components — the logic becomes independently testable.
Exercise
Section titled “Exercise”- Complete
useBudgetto include all state:incomeSources,categories,transactions, and month navigation. - Add all action functions:
addIncome,deleteIncome,updateCategory,addTransaction,deleteTransaction,goToPrevMonth,goToNextMonth,copyFromLastMonth,resetMonth. - Add derived values:
totalIncome,totalBudgeted,leftToAssign,spentByCategory. - Return everything from the hook and confirm
App.jsxcan destructure what it needs cleanly.
- Use regular functions for pure computations with no hooks.
- Use custom hooks when the shared logic involves
useState,useEffect, or other hooks. - Compose hooks —
useBudgetusesuseLocalStorageusesuseState. - Return only what callers need. Group related items for clarity.
- Custom hooks make stateful logic independently testable.