Mortgage calculators are one of those "simple" features that turn out to be tricky — rounding matters, and users expect the numbers to match their bank's quote exactly.
This tutorial skips the math and uses a financial API to build a polished React mortgage calculator with an amortization breakdown.
What We're Building
A mortgage calculator that shows:
- Monthly payment
- Total cost of the loan
- Total interest paid
- Full month-by-month payment breakdown
Live example (you'll have this running in 10 minutes):
Loan amount: $350,000
Rate: 6.75%
Term: 30 years
Monthly payment: $2,270.17
Total paid: $817,261.20
Total interest: $467,261.20
Setup
npx create-react-app mortgage-calc
cd mortgage-calc
npm start
Get a free API key at RapidAPI.
Add to .env:
REACT_APP_RAPIDAPI_KEY=your_key_here
The API Hook
Create src/hooks/useMortgage.js:
import { useState, useCallback } from 'react';
const API_HOST = 'fincalcapi.p.rapidapi.com';
const API_KEY = process.env.REACT_APP_RAPIDAPI_KEY;
export function useMortgage() {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const calculate = useCallback(async ({ principal, annualRate, termYears }) => {
setLoading(true);
setError(null);
try {
// Fetch mortgage summary and amortization schedule in parallel
const [mortgageRes, amortizeRes] = await Promise.all([
fetch(
`https://${API_HOST}/mortgage?` +
new URLSearchParams({ principal, annual_rate: annualRate, term_years: termYears }),
{ headers: { 'X-RapidAPI-Key': API_KEY, 'X-RapidAPI-Host': API_HOST } }
),
fetch(
`https://${API_HOST}/amortize?` +
new URLSearchParams({
principal,
annual_rate: annualRate,
term_months: termYears * 12,
}),
{ headers: { 'X-RapidAPI-Key': API_KEY, 'X-RapidAPI-Host': API_HOST } }
),
]);
const [mortgage, amortize] = await Promise.all([
mortgageRes.json(),
amortizeRes.json(),
]);
setData({ ...mortgage, schedule: amortize.schedule });
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
}, []);
return { data, loading, error, calculate };
}
★ Insight ─────────────────────────────────────
Using Promise.all() here fires both API requests simultaneously instead of sequentially — this halves the perceived latency when both calls take ~50ms each.
─────────────────────────────────────────────────
The Calculator Form
Create src/components/MortgageForm.jsx:
import { useState } from 'react';
export function MortgageForm({ onCalculate, loading }) {
const [values, setValues] = useState({
principal: 350000,
annualRate: 6.75,
termYears: 30,
});
const handle = (e) => {
setValues({ ...values, [e.target.name]: Number(e.target.value) });
};
return (
<form
onSubmit={(e) => { e.preventDefault(); onCalculate(values); }}
style={{ display: 'grid', gap: '12px', maxWidth: '400px' }}
>
<label>
Loan Amount ($)
<input
name="principal"
type="number"
value={values.principal}
onChange={handle}
min="1000"
step="1000"
/>
</label>
<label>
Annual Interest Rate (%)
<input
name="annualRate"
type="number"
value={values.annualRate}
onChange={handle}
min="0.1"
max="30"
step="0.05"
/>
</label>
<label>
Term (years)
<select name="termYears" value={values.termYears} onChange={handle}>
<option value={10}>10 years</option>
<option value={15}>15 years</option>
<option value={20}>20 years</option>
<option value={25}>25 years</option>
<option value={30}>30 years</option>
</select>
</label>
<button type="submit" disabled={loading}>
{loading ? 'Calculating...' : 'Calculate'}
</button>
</form>
);
}
The Results Component
Create src/components/MortgageResults.jsx:
const fmt = (n) => new Intl.NumberFormat('en-US', {
style: 'currency', currency: 'USD'
}).format(n);
export function MortgageResults({ data }) {
const [showAll, setShowAll] = useState(false);
const schedule = showAll ? data.schedule : data.schedule.slice(0, 12);
return (
<div>
{/* Summary cards */}
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: '16px', margin: '24px 0' }}>
<SummaryCard label="Monthly Payment" value={fmt(data.monthly_payment)} highlight />
<SummaryCard label="Total Paid" value={fmt(data.total_paid)} />
<SummaryCard label="Total Interest" value={fmt(data.total_interest)} />
</div>
{/* Interest vs principal pie-chart (CSS only) */}
<InterestBreakdown
principal={data.principal || (data.total_paid - data.total_interest)}
interest={data.total_interest}
/>
{/* Amortization table */}
<h3>Payment Schedule</h3>
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
<thead>
<tr style={{ background: '#f5f5f5' }}>
<th>Month</th><th>Payment</th><th>Principal</th><th>Interest</th><th>Balance</th>
</tr>
</thead>
<tbody>
{schedule.map(row => (
<tr key={row.period} style={{ borderBottom: '1px solid #eee' }}>
<td>{row.period}</td>
<td>{fmt(row.payment)}</td>
<td>{fmt(row.principal)}</td>
<td>{fmt(row.interest)}</td>
<td>{fmt(row.balance)}</td>
</tr>
))}
</tbody>
</table>
<button onClick={() => setShowAll(!showAll)}>
{showAll ? 'Show less' : `Show all ${data.schedule.length} payments`}
</button>
</div>
);
}
function SummaryCard({ label, value, highlight }) {
return (
<div style={{
padding: '16px',
border: `2px solid ${highlight ? '#2563eb' : '#e5e7eb'}`,
borderRadius: '8px',
textAlign: 'center',
}}>
<div style={{ fontSize: '12px', color: '#6b7280' }}>{label}</div>
<div style={{ fontSize: '24px', fontWeight: 'bold', color: highlight ? '#2563eb' : '#111' }}>
{value}
</div>
</div>
);
}
Wire It Together
Update src/App.js:
import { useMortgage } from './hooks/useMortgage';
import { MortgageForm } from './components/MortgageForm';
import { MortgageResults } from './components/MortgageResults';
export default function App() {
const { data, loading, error, calculate } = useMortgage();
return (
<div style={{ maxWidth: '900px', margin: '40px auto', padding: '0 20px' }}>
<h1>Mortgage Calculator</h1>
<MortgageForm onCalculate={calculate} loading={loading} />
{error && (
<div style={{ color: 'red', margin: '16px 0' }}>Error: {error}</div>
)}
{data && <MortgageResults data={data} />}
</div>
);
}
The Result
After 10 minutes you have:
✅ Live mortgage calculations
✅ Full amortization schedule
✅ Zero financial math in your codebase
✅ Automatically handles rounding correctly
Extending This
The same API pattern works for:
Refinancing comparison — Call /mortgage twice with different rates and diff the results.
Rent vs. buy analysis — Combine /mortgage with /compound-interest (for investment returns on the down payment alternative).
Extra payment impact — Call /amortize with a reduced term to show how much interest extra payments save.
Deploy It
npm run build
# Deploy to Vercel, Netlify, or GitHub Pages — it's just static files
Your API key is a client-side env variable which is fine for prototypes. For production, proxy the API calls through your own backend to keep the key server-side.
👉 Get your free API key — 50 calls/day on the free tier, no credit card required.
Questions? Drop them in the comments.
Top comments (0)