Skip to content

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.

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.

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.

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,
};

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.

  1. Complete useBudget to include all state: incomeSources, categories, transactions, and month navigation.
  2. Add all action functions: addIncome, deleteIncome, updateCategory, addTransaction, deleteTransaction, goToPrevMonth, goToNextMonth, copyFromLastMonth, resetMonth.
  3. Add derived values: totalIncome, totalBudgeted, leftToAssign, spentByCategory.
  4. Return everything from the hook and confirm App.jsx can 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 — useBudget uses useLocalStorage uses useState.
  • Return only what callers need. Group related items for clarity.
  • Custom hooks make stateful logic independently testable.