Skip to content

Extracting Custom Hooks

A custom hook is a JavaScript function whose name starts with use and that calls one or more React hooks. It is not a special API — just a convention that lets you share stateful logic between components without sharing UI.

App.jsx in ZeroBudget will accumulate a lot of state: income sources, categories, transactions, the current month and year. If it all lives in App, the component becomes hard to read — 200+ lines of state declarations, derived values, and handlers before any JSX.

Custom hooks let you move that logic out:

App.jsx (before) App.jsx (after)
───────────────────────────── ─────────────────────────────────
const [incomeSources, set... const budget = useBudget();
const [categories, setCat... return <IncomeSection {...budget} />;
const [transactions, set...
const [year, setYear] = ...
const [month, setMonth] = ...
const totalIncome = useMemo...
const totalBudgeted = useMemo...
... 15 more state declarations ...

App becomes a layout component. useBudget owns the data.

A custom hook is just a function:

import { useState } from 'react';
import { useLocalStorage } from './useLocalStorage';
export function useBudget() {
const [incomeSources, setIncomeSources] = useLocalStorage('zb-income', []);
function addIncome(source) {
setIncomeSources(prev => [...prev, { ...source, id: crypto.randomUUID() }]);
}
function deleteIncome(id) {
setIncomeSources(prev => prev.filter(s => s.id !== id));
}
return { incomeSources, addIncome, deleteIncome };
}

The hook uses useLocalStorage (which uses useState internally). The rules of hooks apply: useBudget must be called at the top of a component or another hook — not inside loops, conditions, or event handlers.

Custom hooks follow the same rules as built-in hooks:

  • Only call them at the top level of a component or another hook.
  • They cannot be called conditionally.
  • Each component that calls a custom hook gets its own isolated state.

Custom hooks share logic, not state. Two components both calling useBudget() would get two separate budget state instances. To share state, the hook needs to use Context (covered in Lesson 03).

Start with income and expand:

export function useBudget() {
const today = new Date();
const [currentYear, setCurrentYear] = useLocalStorage('zb-year', today.getFullYear());
const [currentMonth, setCurrentMonth] = useLocalStorage('zb-month', today.getMonth());
const key = `zb-${currentYear}-${String(currentMonth + 1).padStart(2, '0')}`;
const [monthData, setMonthData] = useLocalStorage(key, emptyMonth());
const { incomeSources, categories, transactions } = monthData;
// ... derived values, actions ...
return { incomeSources, categories, transactions, /* ... */ };
}

Move one piece at a time. Test after each move. When App.jsx reads cleanly and useBudget owns all the state, the refactor is done.

  1. Create src/hooks/useBudget.js.
  2. Move incomeSources state and the addIncome/deleteIncome functions from App.jsx into useBudget.
  3. Return { incomeSources, addIncome, deleteIncome } from the hook.
  4. In App.jsx, replace the moved state with const { incomeSources, addIncome, deleteIncome } = useBudget().
  5. Confirm the app still works.
  • A custom hook is a function starting with use that calls React hooks internally.
  • Extract stateful logic into custom hooks when a component accumulates too much state and handler code.
  • Custom hooks share logic, not state — each component calling the hook gets its own instance.
  • To share state between components, the hook needs to expose it through Context.