React Loan Calculator If you’re looking to build a fully functional calculator using React, you’re in luck! Today, we’re sharing a comprehensive guide and the source code for a React Loan Calculator application. This project is perfect for both beginners looking to learn React and experienced developers wanting to polish their skills.
Key Features for React Loan Calculator:
- Basic Arithmetic Operations: Perform addition, subtraction, multiplication, and division.
- Responsive Design: Works seamlessly on both mobile and desktop devices.
- Error Handling: Displays error messages for invalid operations (like division by zero).
- Clean and Simple UI: User-friendly interface that focuses on usability.
React Loan Calculator Getting Started
To get started, you’ll need to have Node.js and npm (or yarn) installed on your machine. If you don’t have them yet, you can download them from Node.js official website.
Step-by-Step Guide for React Loan Calculator
- Set Up Your Project Open your terminal and run the following commands to set up a new React project:
npx create-react-app react-calculator
cd react-calculator
- Install Dependencies Though Create React App comes with most of the necessary dependencies, you might want to add a few more for better styling and functionality:
npm install styled-components
npm install bootstrap
npm install react-bootstrap
npm install react-chartjs-2
npm install sass
npm install next
Create Calculator Component In the src
directory, create a new file named Calculator.js
and add the following code:
"use client"
import { useState, useEffect, useRef } from 'react';
import { Chart, ArcElement, Tooltip, Legend } from 'chart.js';
import { Doughnut } from 'react-chartjs-2';
import styles from './style.module.scss';
import { Col, Container, Row } from "react-bootstrap";
Chart.register(ArcElement, Tooltip, Legend);
const LoanCalculator = ({ labels, defaultTermUnit = 'months' }) => {
const [principal, setPrincipal] = useState(100000); // Initial principal amount in rupees
const [interestRate, setInterestRate] = useState(1.0); // Initial interest rate in percentage
const [term, setTerm] = useState(1); // Initial loan term
const [termUnit, setTermUnit] = useState(defaultTermUnit); // Initial loan term unit
const [emi, setEmi] = useState(0);
const [totalInterest, setTotalInterest] = useState(0);
const [totalAmount, setTotalAmount] = useState(0);
const chartRef = useRef(null);
// Function to calculate monthly EMI
const calculateEMI = (principal, annualRate, months) => {
const monthlyRate = annualRate / (12 * 100);
if (monthlyRate === 0) return principal / months; // If interest rate is 0
return (
(principal * monthlyRate * Math.pow(1 + monthlyRate, months)) /
(Math.pow(1 + monthlyRate, months) - 1)
);
};
useEffect(() => {
const months = termUnit === 'years' ? term * 12 : term;
const calculatedEMI = calculateEMI(principal, interestRate, months);
const calculatedTotalAmount = calculatedEMI * months;
const calculatedTotalInterest = calculatedTotalAmount - principal;
setEmi(calculatedEMI);
setTotalAmount(calculatedTotalAmount);
setTotalInterest(calculatedTotalInterest);
}, [principal, interestRate, term, termUnit]);
useEffect(() => {
const updateSliders = () => {
const principalInput = document.querySelector(`.${styles.slider}[name="principal"]`);
const interestRateInput = document.querySelector(`.${styles.slider}[name="interestRate"]`);
const termInput = document.querySelector(`.${styles.slider}[name="term"]`);
if (principalInput) updateSliderBackground({ target: principalInput }, 'principal');
if (interestRateInput) updateSliderBackground({ target: interestRateInput }, 'interestRate');
if (termInput) updateSliderBackground({ target: termInput }, 'term');
};
updateSliders();
}, [principal, interestRate, term]);
// Chart data
const data = {
labels: [labels.principalLabel, labels.interestLabel],
datasets: [
{
data: [principal, totalInterest],
backgroundColor: ['#24256A', '#F47D20'],
hoverBackgroundColor: ['#24256A', '#F47D20'],
borderWidth: 0,
align: 'end',
},
],
};
const options = {
plugins: {
legend: {
display: true,
position: 'bottom',
labels: {
usePointStyle: false,
boxWidth: 15,
fontSize: 15,
fontColor: '#FF5722',
padding: 10,
}
}
}
};
// Update background of sliders
const updateSliderBackground = (e, type) => {
if (!e || !e.target) return;
const value = e.target.value;
let percentage = 0;
if (type === 'principal') {
percentage = ((value - 100000) / (10000000 - 100000)) * 100;
} else if (type === 'interestRate') {
percentage = ((value - 1) / (30 - 1)) * 100;
} else if (type === 'term') {
const maxTerm = termUnit === 'years' ? 30 : 360;
percentage = ((value - 1) / (maxTerm - 1)) * 100;
}
if (e.target.style) {
e.target.style.background = `linear-gradient(to right, #24256A 0%, #24256A ${percentage}%, #D0D5DD ${percentage}%, #D0D5DD 100%)`;
}
};
// Format number to Indian currency style (e.g., 1,00,000)
const formatCurrency = (amount) => {
return new Intl.NumberFormat('en-IN').format(amount);
};
// Format number to Indian currency style for display
const formattedEMI = `₹${formatCurrency(emi.toFixed(2))}`;
const formattedTotalInterest = `₹${formatCurrency(totalInterest.toFixed(2))}`;
const formattedTotalAmount = `₹${formatCurrency(totalAmount.toFixed(2))}`;
return (
<Container>
<div className={styles.LoanCalculator}>
<Row className={styles.LoanRow}>
<Col xs={12} lg={4} className={styles.Loanchart}>
<div className={styles.sliderMobileBtn}>
<button className="primaryBtn">{labels.applyNowLabel}</button>
</div>
<div className={styles.chart}>
<Doughnut data={data} options={options} ref={chartRef} />
</div>
<div className={styles.result}>
<div className={styles.resultItem}>
<span className={styles.label}>{labels.monthlyEmiLabel}</span>
<span> {formattedEMI}</span>
</div>
<div className={styles.resultItem}>
<span className={styles.label}>{labels.totalInterestLabel}</span>
<span> {formattedTotalInterest} </span>
</div>
<div className={styles.resultItem}>
<span className={styles.label}>{labels.totalAmountLabel}</span>
<span> {formattedTotalAmount} </span>
</div>
</div>
</Col>
<Col xs={12} lg={8}>
<div className={styles.details}>
<div className={styles.inputGroup}>
<div className={styles.fieldGroup}>
<label>{labels.loanAmountLabel}</label>
<input
type="text" // Use text type for formatted input
value={`₹${formatCurrency(principal)}`}
onChange={(e) => {
const value = e.target.value.replace(/\D/g, ''); // Remove non-numeric characters
if (!isNaN(value) && value !== '') {
setPrincipal(Number(value));
updateSliderBackground({ target: { value } }, 'principal');
}
}}
/>
</div>
<div className={styles.sliderContainer}>
<input
type="range"
min="100000"
max="10000000"
step="100000"
value={principal}
name="principal"
onChange={(e) => {
setPrincipal(Number(e.target.value));
updateSliderBackground(e, 'principal');
}}
className={styles.slider} // Apply custom slider class
/>
</div>
</div>
<div className={styles.inputGroup}>
<div className={styles.fieldGroup}>
<label>{labels.interestRateLabel}</label>
<input
type="text" // Use text type for formatted input
value={`${interestRate.toFixed(2)}%`}
onChange={(e) => {
const value = e.target.value.replace(/[^\d.-]/g, ''); // Remove non-numeric characters except decimal
if (!isNaN(value) && value !== '') {
setInterestRate(Number(value));
updateSliderBackground({ target: { value } }, 'interestRate');
}
}}
/>
</div>
<div className={styles.sliderContainer}>
<input
type="range"
min="1"
max="30"
step="0.01" // Allow steps of 0.01
value={interestRate}
name="interestRate"
onChange={(e) => {
setInterestRate(Number(e.target.value));
updateSliderBackground(e, 'interestRate');
}}
className={styles.slider} // Apply custom slider class
/>
</div>
</div>
<div className={styles.inputGroup}>
<div className={styles.fieldGroup}>
<label>{labels.loanTenureLabel} ({termUnit})</label>
<input
type="text" // Use text type for formatted input
value={`${term} ${termUnit === 'years' ? (term > 1 ? 'Years' : 'Year') : (term > 1 ? 'Months' : 'Month')}`}
onChange={(e) => {
const value = e.target.value.replace(/\D/g, ''); // Remove non-numeric characters
if (!isNaN(value) && value !== '') {
setTerm(Number(value));
updateSliderBackground({ target: { value } }, 'term');
}
}}
/>
</div>
<div className={styles.sliderContainer}>
<input
type="range"
min="1"
max={termUnit === 'years' ? "30" : "360"}
step="1"
value={term}
name="term"
onChange={(e) => {
setTerm(Number(e.target.value));
updateSliderBackground(e, 'term');
}}
className={styles.slider} // Apply custom slider class
/>
</div>
</div>
<div className={styles.toggleUnit}>
<button
className={`primaryBtn ${termUnit === 'months' ? styles.active : ''}`}
onClick={() => setTermUnit('months')}
>
Months
</button>
<button
className={`primaryBtn ${termUnit === 'years' ? styles.active : ''}`}
onClick={() => setTermUnit('years')}
>
Years
</button>
</div>
</div>
</Col>
</Row>
</div>
</Container>
);
};
export default LoanCalculator;
styled-components
@import "/src/styles/mixins";
.LoanCalculator {
border: 1px solid #000000;
border-radius: 12px;
background-color: #fff;
overflow: hidden;
padding: 67px 0;
position: relative;
z-index: 1;
margin-top: 40px;
@media only screen and (max-width: 990px) {
padding: 27px 0;
}
.LoanRow {
max-width: 1040px;
margin: 0 auto;
display: flex;
align-items: center;
.Loanchart {
@media only screen and (max-width: 990px) {
flex-direction: column-reverse;
display: flex;
}
}
@media only screen and (max-width: 990px) {
flex-direction: column-reverse;
}
}
.chart {
position: relative;
width: 260px;
height: 260px;
margin: 0 auto 25px;
.pie {
--p: 0%;
width: 100%;
height: 100%;
background: conic-gradient(#4caf50 var(--p), #f44336 0);
border-radius: 50%;
}
.slice {
width: 100%;
height: 100%;
}
}
.details {
width: 100%;
display: flex;
flex-direction: column;
gap: 55px;
max-width: 630px;
margin-left: auto;
@media only screen and (max-width: 990px) {
margin-top: 15px;
max-width: 100%;
margin-right: auto;
margin-bottom: 40px;
}
}
.inputGroup {
display: flex;
flex-direction: column;
gap: 5px;
input {
width: 165px;
height: 50px;
border-radius: 5px;
border: 1px solid #2B3980;
text-align: center;
font-size: 16px;
font-weight: 700;
&:focus {
border: 1px solid #2B3980;
}
@media only screen and (max-width: 990px) {
height: 30px;
width: 115px;
font-size: 14px;
}
}
input[type="range"] {
width: 100%;
border: none;
height: 4px;
}
}
.result {
margin-top: 20px;
max-width: 300px;
margin: 0 auto;
@media only screen and (max-width: 990px) {
max-width: 100%;
margin: inherit;
}
}
.applyButton {
margin-top: 20px;
padding: 10px 20px;
background-color: #1e40af;
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
}
input[type="range"] {
width: 100%;
background-image: linear-gradient(to right, #2A3485 1%, #2A3485 1%, #D0D5DD 0%, #D0D5DD 100%);
}
}
// styles/customSlider.module.scss
/* customSlider.module.scss */
.slider::-webkit-slider-thumb {
width: 20px;
height: 20px;
border-radius: 50%;
-webkit-appearance: none;
cursor: ew-resize;
background-size: 15px !important;
background-color: #fff !important;
border: solid 2px #2A3485;
}
.sliderValue {
margin-top: 5px;
font-size: 16px;
}
.fieldGroup {
display: flex;
align-items: center;
justify-content: space-between;
label {
font-size: 20px;
font-weight: 500;
line-height: normal;
@media only screen and (max-width:990px) {
font-size: 14px;
}
}
}
.sliderBtn {
display: flex;
align-items: center;
justify-content: center;
margin-top: 55px;
button {
min-width: 292px;
}
@media only screen and (max-width:990px) {
margin-bottom: 40px;
display: none;
}
}
.resultItem {
display: flex;
justify-content: space-between;
margin-bottom: 40px;
gap: 20px;
flex-wrap: wrap;
@media only screen and (max-width:990px) {
text-align: center;
justify-content: space-between;
}
span {
font-family: Inter;
font-size: 16px;
font-weight: 700;
line-height: normal;
text-align: left;
@media only screen and (max-width: 990px) {
text-align: center;
}
&.label {
font-family: Inter;
font-size: 16px;
font-weight: 500;
line-height: normal;
text-align: right;
@media only screen and (max-width: 990px) {
text-align: center;
}
}
}
}
.toggleUnit {
display: none;
}
.sliderMobileBtn {
display: none;
width: 100%;
@media only screen and (max-width: 990px) {
display: flex;
}
button {
width: 100%;
}
}
Update App Component Replace the content of App.js
with the following to render your calculator:
import Image from "next/image";
import styles from "./page.module.css";
import LoanCalculator from "@component/components/LoanCalculator";
export default function Home() {
const labels = {
principalLabel: 'Principal Amount',
interestLabel: 'Interest Amount',
monthlyEmiLabel: 'Monthly EMI:',
totalInterestLabel: 'Total Interest:',
totalAmountLabel: 'Total Amount:',
loanAmountLabel: 'Loan Amount',
interestRateLabel: 'Rate of Interest (%)',
loanTenureLabel: 'Loan Tenure',
applyNowLabel: 'Apply Now'
};
return (
<main >
<LoanCalculator labels={labels} />
</main>
);
}
Run Your App Finally, run the app with:
npm start
React Loan Calculator Open http://localhost:3000 to view it in the browser.
Conclusion
You’ve successfully built a React calculator! This project showcases the power of React and how you can create a fully functional application with a clean, responsive design. Feel free to customize and expand the functionality to suit your needs.
You can find the complete source code on GitHub. Happy coding!