Custom Hook and Context
With income, categories, and transactions working, this lesson finishes the useBudget hook with month navigation, wires the full BudgetContext, and cleans App.jsx down to a layout shell.
Month navigation in useBudget
Section titled “Month navigation in useBudget”Add prev/next navigation and the utility actions:
const goToPrevMonth = useCallback(() => { setCurrentMonth(m => { if (m === 0) { setCurrentYear(y => y - 1); return 11; } return m - 1; });}, [setCurrentMonth, setCurrentYear]);
const goToNextMonth = useCallback(() => { setCurrentMonth(m => { if (m === 11) { setCurrentYear(y => y + 1); return 0; } return m + 1; });}, [setCurrentMonth, setCurrentYear]);
const copyFromLastMonth = useCallback(() => { const prevMonth = currentMonth === 0 ? 11 : currentMonth - 1; const prevYear = currentMonth === 0 ? currentYear - 1 : currentYear; const prevKey = monthKey(prevYear, prevMonth); try { const prev = JSON.parse(localStorage.getItem(prevKey)); if (!prev) return; setMonthData(d => ({ ...d, categories: prev.categories.map(c => ({ ...c })) })); } catch { /* nothing stored */ }}, [currentMonth, currentYear, setMonthData]);
const resetMonth = useCallback(() => { setMonthData(emptyMonth());}, [setMonthData]);Building MonthNav
Section titled “Building MonthNav”const MONTHS = ['January','February','March','April','May','June','July','August','September','October','November','December'];
export default function MonthNav() { const { currentYear, currentMonth, goToPrevMonth, goToNextMonth, copyFromLastMonth, resetMonth } = useBudgetContext();
return ( <div className="month-nav"> <div className="month-nav__controls"> <button onClick={goToPrevMonth}>←</button> <span className="month-nav__label">{MONTHS[currentMonth]} {currentYear}</span> <button onClick={goToNextMonth}>→</button> </div> <div className="month-nav__actions"> <button className="btn-secondary" onClick={copyFromLastMonth}>Copy from last month</button> <button className="btn-danger" onClick={() => { if (confirm('Reset this month?')) resetMonth(); }}>Reset month</button> </div> </div> );}The complete BudgetContext
Section titled “The complete BudgetContext”import { createContext, useContext } from 'react';import { useBudget } from '../hooks/useBudget';
const BudgetContext = createContext(null);
export function BudgetProvider({ children }) { const budget = useBudget(); return <BudgetContext.Provider value={budget}>{children}</BudgetContext.Provider>;}
export function useBudgetContext() { const ctx = useContext(BudgetContext); if (!ctx) throw new Error('useBudgetContext must be used inside BudgetProvider'); return ctx;}The final App.jsx
Section titled “The final App.jsx”With context handling all data, App.jsx becomes a pure layout component:
import { useState } from 'react';import { BudgetProvider, useBudgetContext } from './context/BudgetContext';import MonthNav from './components/MonthNav/MonthNav';import IncomeSection from './components/IncomeSection/IncomeSection';import LeftToAssign from './components/LeftToAssign/LeftToAssign';import CategoryCard from './components/CategoryCard/CategoryCard';import TransactionForm from './components/TransactionForm/TransactionForm';import './App.css';
function AppContent() { const { categories, addCategory } = useBudgetContext(); const [newCatName, setNewCatName] = useState('');
function handleAddCategory(e) { e.preventDefault(); if (!newCatName.trim()) return; addCategory(newCatName.trim()); setNewCatName(''); }
return ( <div className="app"> <header className="app-header"> <h1>ZeroBudget</h1> <p>Give every dollar a job.</p> </header> <main className="app-main"> <MonthNav /> <div className="app-top"> <IncomeSection /> <LeftToAssign /> </div> <section className="categories-section"> <div className="categories-header"> <h2>Budget Categories</h2> <form onSubmit={handleAddCategory}> <input value={newCatName} onChange={e => setNewCatName(e.target.value)} placeholder="New category name" /> <button type="submit" className="btn-secondary">Add Category</button> </form> </div> <div className="categories-grid"> {categories.map(cat => <CategoryCard key={cat.id} category={cat} />)} </div> </section> <TransactionForm /> </main> </div> );}
export default function App() { return ( <BudgetProvider> <AppContent /> </BudgetProvider> );}App.jsx has one piece of local state: newCatName for the add-category form. Everything else is in useBudget and accessed through context.
Exercise
Section titled “Exercise”- Add
goToPrevMonth,goToNextMonth,copyFromLastMonth, andresetMonthtouseBudget. - Build
MonthNavand add it toApp.jsx. - Test month navigation: add income in June, navigate to July, confirm July starts empty. Navigate back to June — income should still be there.
- Test copy from last month: navigate to July, click “Copy from last month”, confirm the categories and their budgeted amounts match June.
- Test reset: click “Reset month”, confirm all three arrays go back to defaults.
- Month navigation updates
currentYearandcurrentMonthstored in localStorage — thekeychanges and the correct month’s data loads automatically. copyFromLastMonthreads the previous month’s localStorage entry directly — no React state involved.BudgetContextwrapsuseBudget’s return value. Components calluseBudgetContext()for everything they need.App.jsxends up with exactly oneuseState— the new-category form input. Everything else lives in the hook.