Skip to content

useContext and Providers

With the Provider in place, consuming components can drop prop threading entirely. This lesson completes ZeroBudget’s context layer and shows the custom context hook pattern.

Every consuming component calling useContext(BudgetContext) directly has two problems: it imports both useContext and BudgetContext, and it has no protection against being used outside the Provider. A custom hook solves both:

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

Consumers call useBudgetContext() — one import, built-in error message if misused, no raw useContext needed in component files.

With the context hook in place, components become much simpler. Before:

// CategoryCard.jsx — before context
export default function CategoryCard({ category, spentByCategory, onUpdate, onDelete, onAddTransaction, onDeleteTransaction }) {
const spent = spentByCategory[category.id] || 0;
// ...
}

After:

// CategoryCard.jsx — after context
export default function CategoryCard({ category }) {
const { spentByCategory, updateCategory, deleteCategory, addTransaction, deleteTransaction } = useBudgetContext();
const spent = spentByCategory[category.id] || 0;
// ...
}

CategoryCard now only receives category as a prop — the data about which card it is. Everything else comes from context. App.jsx no longer needs to thread seven props through the categories grid.

import { BudgetProvider, useBudgetContext } from './context/BudgetContext';
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">
<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" />
<button type="submit">Add</button>
</form>
</div>
<div className="categories-grid">
{categories.map(cat => <CategoryCard key={cat.id} category={cat} />)}
</div>
</section>
<TransactionForm />
</div>
);
}
export default function App() {
return (
<BudgetProvider>
<AppContent />
</BudgetProvider>
);
}

App is a layout component. No state, no handlers — just structure. Every component reads what it needs from context.

An app can have multiple contexts — auth, theme, notifications, budget. Each context has its own Provider. Nest them from outermost (app-wide) to innermost (feature-specific):

<AuthProvider>
<ThemeProvider>
<BudgetProvider>
<AppContent />
</BudgetProvider>
</ThemeProvider>
</AuthProvider>
  1. Update BudgetContext.jsx with the useBudgetContext hook that throws if used outside the Provider.
  2. Remove all budget-related props from IncomeSection, LeftToAssign, CategoryCard, TransactionForm, and MonthNav. Have each component call useBudgetContext() directly.
  3. Update App.jsx to wrap with <BudgetProvider> and remove all prop passing.
  4. Confirm the app still works end-to-end.
  • Create a useBudgetContext hook that calls useContext and throws if used outside the Provider.
  • Consumers import one hook — no raw useContext or context object in component files.
  • Components receive only the props that differentiate them (like category); shared data comes from context.
  • Multiple contexts nest — outermost is broadest scope.