I built an amortization calculator in React. The user enters a loan amount, interest rate, and term length, and the app shows a payment summary, an area chart of principal vs. interest over time, and a scrollable payment schedule. Everything recalculates as the inputs change.
The Math
Standard amortization formula. Nothing exotic:
const monthlyRate = annualRate / 100 / 12;
const termMonths = termYears * 12;
const monthlyPayment = (principal * monthlyRate) /
(1 - Math.pow(1 + monthlyRate, -termMonths));
let remainingBalance = principal;
for (let month = 1; month <= termMonths; month++) {
const interestPayment = remainingBalance * monthlyRate;
const principalPayment = monthlyPayment - interestPayment;
remainingBalance -= principalPayment;
}The yearly view required summing 12 months at a time, with handling for partial final years. Rounding errors accumulate over 360 iterations, so the last payment needed adjustment to zero out the balance cleanly.
State and Recalculation
Three pieces of user input drive the entire app. A useEffect recalculates the schedule and summary whenever any of them change:
const [loanAmount, setLoanAmount] = useState(300000);
const [interestRate, setInterestRate] = useState(4.5);
const [loanTerm, setLoanTerm] = useState(30);
useEffect(() => {
const schedule = generateAmortizationSchedule(loanAmount, interestRate, loanTerm);
setAmortizationSchedule(schedule);
setPaymentSummary(calculatePaymentSummary(schedule, loanAmount));
}, [loanAmount, interestRate, loanTerm]);Charting with Recharts
First time using Recharts. The stacked area chart worked well for showing how the principal-to-interest ratio shifts over the life of the loan:
<ResponsiveContainer width="100%" height="100%">
<AreaChart data={chartData}>
<XAxis dataKey="paymentNumber" />
<YAxis />
<Tooltip />
<Area type="monotone" dataKey="interest" stackId="1" fill="#8884d8" />
<Area type="monotone" dataKey="principal" stackId="1" fill="#82ca9d" />
</AreaChart>
</ResponsiveContainer>ResponsiveContainer needs its parent to have explicit dimensions. That took some trial and error to get right.
The Layout Problem
The most instructive part of this project was the CSS. I wanted the amortization table to fill available vertical space and scroll internally, without making the whole page scroll. Flexbox alone didn't solve it. The fix was min-h-0:
<div className="flex flex-col min-h-0">
<div className="flex-shrink-0">
{/* Summary and chart */}
</div>
<div className="flex-1 min-h-0">
{/* AmortizationTable with internal scrolling */}
</div>
</div>Without min-h-0, flex items won't shrink below their content size. It's a default that makes sense when you read the spec, but it's not obvious when you're staring at a table that refuses to stay inside its container. I'll remember this one.
— Lo