Skip to content

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.

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

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: [...] }).

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

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>
);
}
  1. Implement useLocalStorage exactly as shown above.
  2. Implement the useBudget month structure and income actions.
  3. Build IncomeSection with the add form and delete button.
  4. Set up BudgetContext (use the pattern from Module 06, Lesson 04) and wrap App with BudgetProvider.
  5. Add a few income sources and reload the page — confirm they persist in localStorage.
  • useBudget stores the entire month as one object in localStorage, keyed by zb-YYYY-MM.
  • emptyMonth() returns the default structure — called when no data exists for a month yet.
  • Income actions use useCallback with the functional setter form to avoid stale dependencies.
  • IncomeSection reads from context and manages only its local form state.