Income Section and State Design
The income section is the entry point for ZeroBudget’s data. It is also where you will build the useBudget hook and establish the month-keyed persistence pattern that everything else depends on.
Building useLocalStorage
Section titled “Building useLocalStorage”Start with the foundation. Create src/hooks/useLocalStorage.js:
import { useState } from 'react';
export function useLocalStorage(key, initialValue) { const [storedValue, setStoredValue] = useState(() => { try { const item = localStorage.getItem(key); return item ? JSON.parse(item) : initialValue; } catch { return initialValue; } });
const setValue = (value) => { try { const valueToStore = value instanceof Function ? value(storedValue) : value; setStoredValue(valueToStore); localStorage.setItem(key, JSON.stringify(valueToStore)); } catch (error) { console.error('useLocalStorage error:', error); } };
return [storedValue, setValue];}Building useBudget — month structure
Section titled “Building useBudget — month structure”Create src/hooks/useBudget.js. Start with the month navigation and the month data structure:
import { useMemo, useCallback } from 'react';import { useLocalStorage } from './useLocalStorage';import { DEFAULT_CATEGORIES } from '../data/defaults';
function monthKey(year, month) { return `zb-${year}-${String(month + 1).padStart(2, '0')}`;}
function emptyMonth() { return { incomeSources: [], categories: DEFAULT_CATEGORIES.map(c => ({ ...c })), transactions: [], };}
export function useBudget() { const today = new Date(); const [currentYear, setCurrentYear] = useLocalStorage('zb-current-year', today.getFullYear()); const [currentMonth, setCurrentMonth] = useLocalStorage('zb-current-month', today.getMonth());
const key = monthKey(currentYear, currentMonth); const [monthData, setMonthData] = useLocalStorage(key, emptyMonth());
const { incomeSources, categories, transactions } = monthData;monthData is a single object per month. Updating any of the three arrays goes through setMonthData with a spread: d => ({ ...d, incomeSources: [...] }).
Income actions
Section titled “Income actions”Add the income CRUD functions inside useBudget:
const addIncome = useCallback((source) => { setMonthData(d => ({ ...d, incomeSources: [...d.incomeSources, { ...source, id: crypto.randomUUID() }], })); }, [setMonthData]);
const updateIncome = useCallback((id, updates) => { setMonthData(d => ({ ...d, incomeSources: d.incomeSources.map(s => s.id === id ? { ...s, ...updates } : s), })); }, [setMonthData]);
const deleteIncome = useCallback((id) => { setMonthData(d => ({ ...d, incomeSources: d.incomeSources.filter(s => s.id !== id), })); }, [setMonthData]);Building IncomeSection
Section titled “Building IncomeSection”Create src/components/IncomeSection/IncomeSection.jsx:
import { useState } from 'react';import { useBudgetContext } from '../../context/BudgetContext';
export default function IncomeSection() { const { incomeSources, totalIncome, addIncome, updateIncome, deleteIncome } = useBudgetContext(); const [name, setName] = useState(''); const [amount, setAmount] = useState('');
function handleAdd(e) { e.preventDefault(); if (!name.trim() || !amount) return; addIncome({ name: name.trim(), amount: parseFloat(amount) }); setName(''); setAmount(''); }
return ( <section className="income-section card"> <h2>Income</h2> <ul className="income-list"> {incomeSources.map(src => ( <IncomeRow key={src.id} source={src} onUpdate={updateIncome} onDelete={deleteIncome} /> ))} {incomeSources.length === 0 && <li className="empty">No income sources yet.</li>} </ul> <form className="income-form" onSubmit={handleAdd}> <input value={name} onChange={e => setName(e.target.value)} placeholder="Source name" /> <input type="number" value={amount} onChange={e => setAmount(e.target.value)} placeholder="Amount" min="0" step="0.01" /> <button type="submit" className="btn-primary">Add</button> </form> <div className="income-total"> Total: <strong>{totalIncome.toLocaleString('en-US', { style: 'currency', currency: 'USD' })}</strong> </div> </section> );}Exercise
Section titled “Exercise”- Implement
useLocalStorageexactly as shown above. - Implement the
useBudgetmonth structure and income actions. - Build
IncomeSectionwith the add form and delete button. - Set up
BudgetContext(use the pattern from Module 06, Lesson 04) and wrapAppwithBudgetProvider. - Add a few income sources and reload the page — confirm they persist in localStorage.
useBudgetstores the entire month as one object in localStorage, keyed byzb-YYYY-MM.emptyMonth()returns the default structure — called when no data exists for a month yet.- Income actions use
useCallbackwith the functional setter form to avoid stale dependencies. IncomeSectionreads from context and manages only its local form state.