Rendering Lists
In Module 02 you learned the basics of rendering lists with map and key. This lesson goes deeper — dynamic lists that come from state, filtering, sorting, and the empty state pattern.
Lists from state
Section titled “Lists from state”When the list comes from state, every change to state re-renders the list automatically:
const [transactions, setTransactions] = useState([]);
function addTransaction(tx) { setTransactions(prev => [...prev, { ...tx, id: crypto.randomUUID() }]);}
function deleteTransaction(id) { setTransactions(prev => prev.filter(t => t.id !== id));}[...prev, newItem] creates a new array with the item appended — never mutating the existing array. prev.filter(...) returns a new array without the removed item.
The empty state pattern
Section titled “The empty state pattern”Always handle the case where the list is empty:
export default function TransactionList({ transactions, onDelete }) { if (transactions.length === 0) { return <p className="empty">No transactions yet.</p>; }
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 || 'No description'}</span> <span className="tx-amount">${tx.amount.toFixed(2)}</span> <button onClick={() => onDelete(tx.id)}>×</button> </li> ))} </ul> );}The early return shows the empty message when there is nothing to display. The list only renders when there is something to show.
Filtering before rendering
Section titled “Filtering before rendering”In ZeroBudget, transactions belong to categories. Each CategoryCard shows only the transactions for its category:
export default function CategoryCard({ category, transactions, onAddTransaction, onDeleteTransaction }) { const categoryTransactions = transactions.filter(tx => tx.categoryId === category.id);
return ( <div className="category-card card"> <h3>{category.name}</h3> <TransactionList transactions={categoryTransactions} onDelete={onDeleteTransaction} /> </div> );}Filter the array before passing it to the list component. The filtering is a computation — it does not need to be stored in state. This is derived state: a value computed from existing state that does not need its own useState.
Sorting
Section titled “Sorting”Sort with Array.prototype.sort before rendering. Always sort a copy — sort mutates the original array:
const sorted = [...transactions] .sort((a, b) => new Date(b.date) - new Date(a.date));The spread [...transactions] creates a copy. Then sort descending by date. The original transactions array in state is unchanged.
Exercise
Section titled “Exercise”- Add
transactionsstate toApp.jsxwithuseState([]). WriteaddTransactionanddeleteTransactionfunctions. - Create
TransactionListthat accepts atransactionsarray and anonDeletecallback. Show an empty message when the list is empty. - In
CategoryCard, filtertransactionsbycategoryIdand pass the result toTransactionList. - Test by adding a transaction through the form and confirming it appears in the correct category card.
- Dynamic lists come from state — updates automatically trigger re-renders.
- Use
[...prev, newItem]to add andprev.filter(...)to remove — never mutate the array in state. - Always handle the empty state with a message or fallback UI.
- Filter and sort arrays before passing them to rendering components — these are derived computations, not separate state.