Skip to content

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.

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]);
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>
);
}
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;
}

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.

  1. Add goToPrevMonth, goToNextMonth, copyFromLastMonth, and resetMonth to useBudget.
  2. Build MonthNav and add it to App.jsx.
  3. Test month navigation: add income in June, navigate to July, confirm July starts empty. Navigate back to June — income should still be there.
  4. Test copy from last month: navigate to July, click “Copy from last month”, confirm the categories and their budgeted amounts match June.
  5. Test reset: click “Reset month”, confirm all three arrays go back to defaults.
  • Month navigation updates currentYear and currentMonth stored in localStorage — the key changes and the correct month’s data loads automatically.
  • copyFromLastMonth reads the previous month’s localStorage entry directly — no React state involved.
  • BudgetContext wraps useBudget’s return value. Components call useBudgetContext() for everything they need.
  • App.jsx ends up with exactly one useState — the new-category form input. Everything else lives in the hook.