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.
Why extract logic into a hook
Section titled “Why extract logic into a hook”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.
Writing a custom hook
Section titled “Writing a custom hook”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.
The rules of hooks apply to custom hooks
Section titled “The rules of hooks apply to custom hooks”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).
Building useBudget progressively
Section titled “Building useBudget progressively”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.
Exercise
Section titled “Exercise”- Create
src/hooks/useBudget.js. - Move
incomeSourcesstate and theaddIncome/deleteIncomefunctions fromApp.jsxintouseBudget. - Return
{ incomeSources, addIncome, deleteIncome }from the hook. - In
App.jsx, replace the moved state withconst { incomeSources, addIncome, deleteIncome } = useBudget(). - Confirm the app still works.
- A custom hook is a function starting with
usethat 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.