Skip to content

Transactions and Lists

Transactions are the moment-to-moment data in ZeroBudget. Users add them when they spend money — either through the global form at the bottom or the quick-add on each category card. Both paths call the same addTransaction action.

const addTransaction = useCallback((tx) => {
setMonthData(d => ({
...d,
transactions: [
...d.transactions,
{
...tx,
id: crypto.randomUUID(),
date: tx.date || new Date().toISOString().slice(0, 10),
},
],
}));
}, [setMonthData]);
const deleteTransaction = useCallback((id) => {
setMonthData(d => ({
...d,
transactions: d.transactions.filter(t => t.id !== id),
}));
}, [setMonthData]);

The global form lets users pick any category:

export default function TransactionForm() {
const { categories, addTransaction } = useBudgetContext();
const [categoryId, setCategoryId] = useState('');
const [amount, setAmount] = useState('');
const [description, setDescription] = useState('');
const [date, setDate] = useState(new Date().toISOString().slice(0, 10));
function handleSubmit(e) {
e.preventDefault();
if (!categoryId || !amount) return;
addTransaction({ categoryId, amount: parseFloat(amount), description: description.trim(), date });
setAmount('');
setDescription('');
}
return (
<section className="transaction-form card">
<h2>Add Transaction</h2>
<form onSubmit={handleSubmit}>
<select value={categoryId} onChange={e => setCategoryId(e.target.value)} required>
<option value="">Select category…</option>
{categories.map(c => <option key={c.id} value={c.id}>{c.name}</option>)}
</select>
<input type="number" value={amount} onChange={e => setAmount(e.target.value)} placeholder="0.00" min="0" step="0.01" required />
<input type="text" value={description} onChange={e => setDescription(e.target.value)} placeholder="What was this for?" />
<input type="date" value={date} onChange={e => setDate(e.target.value)} />
<button type="submit" className="btn-primary">Add Transaction</button>
</form>
</section>
);
}

Add a quick-add form directly to CategoryCard. It pre-fills categoryId:

// Inside CategoryCard, below the progress bar:
const [quickAmt, setQuickAmt] = useState('');
const [quickDesc, setQuickDesc] = useState('');
function handleQuickAdd(e) {
e.preventDefault();
if (!quickAmt) return;
addTransaction({ categoryId: category.id, amount: parseFloat(quickAmt), description: quickDesc.trim() });
setQuickAmt('');
setQuickDesc('');
}
// In the return:
<form className="quick-add" onSubmit={handleQuickAdd}>
<input type="number" value={quickAmt} onChange={e => setQuickAmt(e.target.value)} placeholder="Amount" min="0" step="0.01" />
<input type="text" value={quickDesc} onChange={e => setQuickDesc(e.target.value)} placeholder="Description" />
<button type="submit" className="btn-primary btn-sm">+ Add</button>
</form>
export default function TransactionList({ transactions, onDelete }) {
if (transactions.length === 0) return null;
return (
<ul className="transaction-list">
{transactions.map(tx => (
<li key={tx.id} className="transaction-row">
<span className="tx-date">{tx.date}</span>
<span className="tx-desc">{tx.description || ''}</span>
<span className="tx-amount">{Number(tx.amount).toLocaleString('en-US', { style: 'currency', currency: 'USD' })}</span>
<button className="btn-icon btn-icon--danger" onClick={() => onDelete(tx.id)}>×</button>
</li>
))}
</ul>
);
}

In CategoryCard, filter transactions and add the toggle:

const { transactions, deleteTransaction } = useBudgetContext();
const [showTxns, setShowTxns] = useState(false);
const catTransactions = transactions.filter(t => t.categoryId === category.id);
// In the return:
{catTransactions.length > 0 && (
<button className="btn-link" onClick={() => setShowTxns(v => !v)}>
{showTxns ? 'Hide' : 'Show'} {catTransactions.length} transaction{catTransactions.length !== 1 ? 's' : ''}
</button>
)}
{showTxns && <TransactionList transactions={catTransactions} onDelete={deleteTransaction} />}
  1. Add addTransaction and deleteTransaction to useBudget with useCallback.
  2. Build TransactionForm and render it at the bottom of App.jsx.
  3. Add the quick-add form to CategoryCard.
  4. Build TransactionList and add the collapsible toggle in CategoryCard.
  5. Add a transaction via both paths and confirm it appears in the correct category card, updates the spent amount, and updates the progress bar.
  • Both transaction paths — global form and quick-add — call the same addTransaction action from context.
  • TransactionList receives pre-filtered transactions from CategoryCard.
  • The collapsible toggle uses local showTxns state — nothing else needs to know whether the list is expanded.
  • Filtering transactions by categoryId is derived computation — it does not need its own state.