Build a React Currency Converter with Real-Time RBA Exchange Rates API
Building a currency converter is one of the best ways to learn API integration in React. Using the Reserve Bank of Australia's official exchange rate data, we'll create a production-ready converter with modern React patterns, responsive design, and real-time updates.
What We're Building
- Modern React app with hooks and functional components
- Real-time currency conversion with RBA exchange rates
- Historical rate comparison feature
- Responsive design that works on all devices
- Error handling and loading states
- Local storage for user preferences
Project Setup
Create a new React app and install dependencies:
npx create-react-app rba-currency-converter
cd rba-currency-converter
npm install axios date-fns
npm start
We'll use axios for API calls and date-fns for date formatting.
Prefer other technologies? We also have tutorials for Python backend integration and WordPress/PHP implementations.
API Integration Hook
First, let's create a custom hook for the RBA Exchange Rates API:
// hooks/useExchangeRates.js
import { useState, useEffect, useCallback } from 'react';
import axios from 'axios';
const API_BASE = 'https://api.exchangeratesapi.com.au';
export const useExchangeRates = (apiKey) => {
const [rates, setRates] = useState(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const [lastUpdated, setLastUpdated] = useState(null);
const headers = {
Authorization: `Bearer ${apiKey}`
};
const fetchLatestRates = useCallback(async () => {
setLoading(true);
setError(null);
try {
const response = await axios.get(`${API_BASE}/latest`, { headers });
if (response.data.success) {
setRates(response.data);
setLastUpdated(new Date());
} else {
setError(response.data.error?.info || 'Failed to fetch rates');
}
} catch (err) {
if (err.response?.status === 401) {
setError('Invalid API key. Please check your credentials.');
} else if (err.response?.status === 429) {
setError('Rate limit exceeded. Please try again later.');
} else {
setError('Network error. Please check your connection.');
}
} finally {
setLoading(false);
}
}, [apiKey, headers]);
const convertCurrency = useCallback(async (from, to, amount, date = null) => {
setLoading(true);
setError(null);
try {
const params = { from, to, amount };
if (date) params.date = date;
const response = await axios.get(`${API_BASE}/convert`, {
headers,
params
});
if (response.data.success) {
return response.data;
} else {
setError(response.data.error?.info || 'Conversion failed');
return null;
}
} catch (err) {
setError('Conversion request failed');
return null;
} finally {
setLoading(false);
}
}, [apiKey, headers]);
const getHistoricalRates = useCallback(async (date) => {
setLoading(true);
setError(null);
try {
const response = await axios.get(`${API_BASE}/${date}`, { headers });
if (response.data.success) {
return response.data;
} else {
setError(response.data.error?.info || 'Historical data not available');
return null;
}
} catch (err) {
setError('Failed to fetch historical data');
return null;
} finally {
setLoading(false);
}
}, [apiKey, headers]);
return {
rates,
loading,
error,
lastUpdated,
fetchLatestRates,
convertCurrency,
getHistoricalRates
};
};
Main Currency Converter Component
// components/CurrencyConverter.js
import React, { useState, useEffect } from 'react';
import { useExchangeRates } from '../hooks/useExchangeRates';
import { format } from 'date-fns';
import './CurrencyConverter.css';
const SUPPORTED_CURRENCIES = {
'AUD': 'Australian Dollar',
'USD': 'US Dollar',
'EUR': 'Euro',
'GBP': 'British Pound',
'JPY': 'Japanese Yen',
'CAD': 'Canadian Dollar',
'CHF': 'Swiss Franc',
'CNY': 'Chinese Renminbi',
'NZD': 'New Zealand Dollar',
'SGD': 'Singapore Dollar',
'HKD': 'Hong Kong Dollar',
'KRW': 'South Korean Won'
};
const CurrencyConverter = () => {
const [apiKey, setApiKey] = useState(
localStorage.getItem('rba-api-key') || ''
);
const [fromCurrency, setFromCurrency] = useState('AUD');
const [toCurrency, setToCurrency] = useState('USD');
const [amount, setAmount] = useState(100);
const [result, setResult] = useState(null);
const [historicalDate, setHistoricalDate] = useState('');
const [showHistorical, setShowHistorical] = useState(false);
const [historicalResult, setHistoricalResult] = useState(null);
const {
rates,
loading,
error,
lastUpdated,
fetchLatestRates,
convertCurrency,
getHistoricalRates
} = useExchangeRates(apiKey);
// Save API key to localStorage
useEffect(() => {
if (apiKey) {
localStorage.setItem('rba-api-key', apiKey);
}
}, [apiKey]);
// Load initial rates
useEffect(() => {
if (apiKey && apiKey.length > 20) {
fetchLatestRates();
}
}, [apiKey, fetchLatestRates]);
const handleConvert = async (e) => {
e.preventDefault();
if (!apiKey) {
alert('Please enter your API key');
return;
}
const conversionResult = await convertCurrency(fromCurrency, toCurrency, amount);
setResult(conversionResult);
};
const handleHistoricalCompare = async () => {
if (!historicalDate) return;
const historical = await convertCurrency(fromCurrency, toCurrency, amount, historicalDate);
setHistoricalResult(historical);
};
const swapCurrencies = () => {
setFromCurrency(toCurrency);
setToCurrency(fromCurrency);
setResult(null);
setHistoricalResult(null);
};
const formatCurrency = (value, currency) => {
return new Intl.NumberFormat('en-AU', {
style: 'currency',
currency: currency === 'AUD' ? 'AUD' : 'USD',
minimumFractionDigits: 2,
maximumFractionDigits: 6
}).format(value);
};
if (!apiKey) {
return (
<div className="converter-container">
<div className="api-key-setup">
<h2>RBA Currency Converter</h2>
<p>Enter your API key to get started:</p>
<input
type="password"
placeholder="Enter your API key"
value={apiKey}
onChange={(e) => setApiKey(e.target.value)}
className="api-key-input"
/>
<p className="api-help">
Get your free API key at{' '}
<a href="https://app.exchangeratesapi.com.au" target="_blank" rel="noopener noreferrer">
app.exchangeratesapi.com.au
</a>
</p>
</div>
</div>
);
}
return (
<div className="converter-container">
<header className="converter-header">
<h1>RBA Currency Converter</h1>
<p>Official Reserve Bank of Australia Exchange Rates</p>
{lastUpdated && (
<p className="last-updated">
Last updated: {format(lastUpdated, 'PPpp')}
</p>
)}
</header>
<form onSubmit={handleConvert} className="converter-form">
<div className="amount-input">
<label htmlFor="amount">Amount</label>
<input
id="amount"
type="number"
value={amount}
onChange={(e) => setAmount(parseFloat(e.target.value) || 0)}
min="0"
step="0.01"
/>
</div>
<div className="currency-selectors">
<div className="currency-input">
<label htmlFor="from-currency">From</label>
<select
id="from-currency"
value={fromCurrency}
onChange={(e) => setFromCurrency(e.target.value)}
>
{Object.entries(SUPPORTED_CURRENCIES).map(([code, name]) => (
<option key={code} value={code}>
{code} - {name}
</option>
))}
</select>
</div>
<button
type="button"
onClick={swapCurrencies}
className="swap-button"
title="Swap currencies"
>
⇄
</button>
<div className="currency-input">
<label htmlFor="to-currency">To</label>
<select
id="to-currency"
value={toCurrency}
onChange={(e) => setToCurrency(e.target.value)}
>
{Object.entries(SUPPORTED_CURRENCIES).map(([code, name]) => (
<option key={code} value={code}>
{code} - {name}
</option>
))}
</select>
</div>
</div>
<button type="submit" disabled={loading} className="convert-button">
{loading ? 'Converting...' : 'Convert'}
</button>
</form>
{error && (
<div className="error-message">
<strong>Error:</strong> {error}
</div>
)}
{result && (
<div className="result-container">
<div className="conversion-result">
<div className="result-amount">
{formatCurrency(amount, fromCurrency)} = {formatCurrency(result.result, toCurrency)}
</div>
<div className="exchange-rate">
Exchange Rate: 1 {fromCurrency} = {result.info?.rate.toFixed(6)} {toCurrency}
</div>
<div className="result-date">
Rate from: {result.date}
</div>
</div>
<div className="historical-section">
<button
onClick={() => setShowHistorical(!showHistorical)}
className="historical-toggle"
>
{showHistorical ? 'Hide' : 'Show'} Historical Comparison
</button>
{showHistorical && (
<div className="historical-compare">
<div className="date-input">
<label htmlFor="historical-date">Compare with date:</label>
<input
id="historical-date"
type="date"
value={historicalDate}
onChange={(e) => setHistoricalDate(e.target.value)}
min="2018-01-01"
max={new Date().toISOString().split('T')[0]}
/>
<button
type="button"
onClick={handleHistoricalCompare}
disabled={!historicalDate || loading}
className="compare-button"
>
Compare
</button>
</div>
{historicalResult && (
<div className="historical-result">
<h4>Historical Comparison</h4>
<div className="comparison-row">
<span>Today:</span>
<span>{formatCurrency(result.result, toCurrency)}</span>
</div>
<div className="comparison-row">
<span>{historicalResult.date}:</span>
<span>{formatCurrency(historicalResult.result, toCurrency)}</span>
</div>
<div className="comparison-difference">
Difference: {formatCurrency(
result.result - historicalResult.result,
toCurrency
)} ({((result.result - historicalResult.result) / historicalResult.result * 100).toFixed(2)}%)
</div>
</div>
)}
</div>
)}
</div>
</div>
)}
{rates && (
<div className="rates-grid">
<h3>Current Exchange Rates (1 AUD =)</h3>
<div className="rates-container">
{Object.entries(rates.rates).map(([currency, rate]) => (
<div key={currency} className="rate-card">
<div className="rate-currency">{currency}</div>
<div className="rate-value">{rate.toFixed(4)}</div>
</div>
))}
</div>
</div>
)}
<footer className="converter-footer">
<p>Data sourced from Reserve Bank of Australia. Not affiliated with or endorsed by the RBA.</p>
</footer>
</div>
);
};
export default CurrencyConverter;
Styling
Create a modern, responsive stylesheet:
/* components/CurrencyConverter.css */
.converter-container {
max-width: 800px;
margin: 0 auto;
padding: 20px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
}
.converter-header {
text-align: center;
color: white;
margin-bottom: 30px;
}
.converter-header h1 {
font-size: 2.5rem;
margin-bottom: 10px;
font-weight: 300;
}
.converter-header p {
font-size: 1.1rem;
opacity: 0.9;
margin-bottom: 5px;
}
.last-updated {
font-size: 0.9rem !important;
opacity: 0.7 !important;
}
.api-key-setup {
background: white;
border-radius: 12px;
padding: 40px;
text-align: center;
box-shadow: 0 10px 30px rgba(0,0,0,0.2);
}
.api-key-input {
width: 100%;
max-width: 400px;
padding: 12px;
border: 2px solid #e1e5e9;
border-radius: 6px;
font-size: 16px;
margin: 20px 0;
}
.api-help {
color: #666;
font-size: 14px;
}
.api-help a {
color: #667eea;
text-decoration: none;
}
.converter-form {
background: white;
border-radius: 12px;
padding: 30px;
margin-bottom: 20px;
box-shadow: 0 10px 30px rgba(0,0,0,0.1);
}
.amount-input {
margin-bottom: 25px;
}
.amount-input label {
display: block;
margin-bottom: 8px;
font-weight: 600;
color: #333;
}
.amount-input input {
width: 100%;
padding: 12px;
border: 2px solid #e1e5e9;
border-radius: 6px;
font-size: 18px;
font-weight: 500;
}
.currency-selectors {
display: grid;
grid-template-columns: 1fr auto 1fr;
gap: 15px;
align-items: end;
margin-bottom: 25px;
}
.currency-input label {
display: block;
margin-bottom: 8px;
font-weight: 600;
color: #333;
}
.currency-input select {
width: 100%;
padding: 12px;
border: 2px solid #e1e5e9;
border-radius: 6px;
font-size: 16px;
background: white;
}
.swap-button {
background: #f8f9fa;
border: 2px solid #e1e5e9;
border-radius: 50%;
width: 45px;
height: 45px;
font-size: 20px;
cursor: pointer;
transition: all 0.2s ease;
display: flex;
align-items: center;
justify-content: center;
}
.swap-button:hover {
background: #667eea;
color: white;
border-color: #667eea;
}
.convert-button {
width: 100%;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
padding: 15px;
border-radius: 6px;
font-size: 18px;
font-weight: 600;
cursor: pointer;
transition: transform 0.2s ease;
}
.convert-button:hover:not(:disabled) {
transform: translateY(-2px);
}
.convert-button:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.error-message {
background: #fee;
color: #c33;
padding: 15px;
border-radius: 6px;
margin-bottom: 20px;
border: 1px solid #fcc;
}
.result-container {
background: white;
border-radius: 12px;
padding: 30px;
margin-bottom: 20px;
box-shadow: 0 10px 30px rgba(0,0,0,0.1);
}
.conversion-result {
text-align: center;
margin-bottom: 30px;
}
.result-amount {
font-size: 2rem;
font-weight: 600;
color: #333;
margin-bottom: 10px;
}
.exchange-rate {
font-size: 1.1rem;
color: #666;
margin-bottom: 5px;
}
.result-date {
font-size: 0.9rem;
color: #888;
}
.historical-toggle {
background: #f8f9fa;
border: 1px solid #e1e5e9;
padding: 10px 20px;
border-radius: 6px;
cursor: pointer;
margin-bottom: 15px;
}
.historical-compare {
border-top: 1px solid #e1e5e9;
padding-top: 20px;
}
.date-input {
display: flex;
gap: 10px;
align-items: center;
margin-bottom: 20px;
flex-wrap: wrap;
}
.date-input label {
font-weight: 600;
white-space: nowrap;
}
.date-input input {
padding: 8px;
border: 2px solid #e1e5e9;
border-radius: 4px;
}
.compare-button {
background: #28a745;
color: white;
border: none;
padding: 8px 16px;
border-radius: 4px;
cursor: pointer;
}
.historical-result {
background: #f8f9fa;
padding: 20px;
border-radius: 6px;
}
.comparison-row {
display: flex;
justify-content: space-between;
margin-bottom: 10px;
}
.comparison-difference {
font-weight: 600;
color: #667eea;
border-top: 1px solid #e1e5e9;
padding-top: 10px;
margin-top: 10px;
text-align: center;
}
.rates-grid {
background: white;
border-radius: 12px;
padding: 30px;
margin-bottom: 20px;
box-shadow: 0 10px 30px rgba(0,0,0,0.1);
}
.rates-grid h3 {
text-align: center;
margin-bottom: 20px;
color: #333;
}
.rates-container {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
gap: 15px;
}
.rate-card {
background: #f8f9fa;
padding: 15px;
border-radius: 6px;
text-align: center;
border: 1px solid #e1e5e9;
}
.rate-currency {
font-weight: 600;
color: #333;
margin-bottom: 5px;
}
.rate-value {
font-size: 1.1rem;
color: #667eea;
font-weight: 500;
}
.converter-footer {
text-align: center;
color: white;
opacity: 0.8;
font-size: 0.9rem;
margin-top: 30px;
}
/* Responsive Design */
@media (max-width: 768px) {
.converter-container {
padding: 15px;
}
.converter-header h1 {
font-size: 2rem;
}
.currency-selectors {
grid-template-columns: 1fr;
gap: 15px;
}
.swap-button {
order: 3;
justify-self: center;
margin: 10px 0;
}
.result-amount {
font-size: 1.5rem;
}
.rates-container {
grid-template-columns: repeat(auto-fit, minmax(100px, 1fr));
gap: 10px;
}
.date-input {
flex-direction: column;
align-items: stretch;
}
}
App Component
Update your main App component:
// App.js
import React from 'react';
import CurrencyConverter from './components/CurrencyConverter';
import './App.css';
function App() {
return (
<div className="App">
<CurrencyConverter />
</div>
);
}
export default App;
/* App.css */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.App {
min-height: 100vh;
}
Advanced Features
Real-time Updates
Add automatic rate refreshing:
// Add to useExchangeRates hook
const [autoRefresh, setAutoRefresh] = useState(false);
useEffect(() => {
if (!autoRefresh || !apiKey) return;
const interval = setInterval(() => {
fetchLatestRates();
}, 300000); // Refresh every 5 minutes
return () => clearInterval(interval);
}, [autoRefresh, apiKey, fetchLatestRates]);
Currency Favorites
Add user preferences:
// Add to CurrencyConverter component
const [favorites, setFavorites] = useState(() => {
const saved = localStorage.getItem('currency-favorites');
return saved ? JSON.parse(saved) : ['USD', 'EUR', 'GBP'];
});
const toggleFavorite = (currency) => {
const newFavorites = favorites.includes(currency)
? favorites.filter(c => c !== currency)
: [...favorites, currency];
setFavorites(newFavorites);
localStorage.setItem('currency-favorites', JSON.stringify(newFavorites));
};
Chart Integration
Add a simple rate trend chart. For server-side chart generation, see our Python tutorial which includes matplotlib examples:
npm install chart.js react-chartjs-2
// components/RateChart.js
import React, { useState, useEffect } from 'react';
import {
Chart as ChartJS,
CategoryScale,
LinearScale,
PointElement,
LineElement,
Title,
Tooltip,
Legend,
} from 'chart.js';
import { Line } from 'react-chartjs-2';
ChartJS.register(
CategoryScale,
LinearScale,
PointElement,
LineElement,
Title,
Tooltip,
Legend
);
const RateChart = ({ currency, rates, getHistoricalRates }) => {
const [chartData, setChartData] = useState(null);
const [loading, setLoading] = useState(false);
useEffect(() => {
const fetchChartData = async () => {
setLoading(true);
const dates = [];
const values = [];
// Get last 30 days
for (let i = 29; i >= 0; i--) {
const date = new Date();
date.setDate(date.getDate() - i);
const dateStr = date.toISOString().split('T')[0];
try {
const historical = await getHistoricalRates(dateStr);
if (historical?.rates?.[currency]) {
dates.push(dateStr);
values.push(historical.rates[currency]);
}
} catch (error) {
console.log(`No data for ${dateStr}`);
}
}
setChartData({
labels: dates,
datasets: [{
label: `AUD to ${currency}`,
data: values,
borderColor: '#667eea',
backgroundColor: 'rgba(102, 126, 234, 0.1)',
tension: 0.4
}]
});
setLoading(false);
};
if (currency && getHistoricalRates) {
fetchChartData();
}
}, [currency, getHistoricalRates]);
if (loading) return <div>Loading chart...</div>;
if (!chartData) return null;
return (
<div style={{ height: '400px', marginTop: '20px' }}>
<Line
data={chartData}
options={{
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: { position: 'top' },
title: {
display: true,
text: `30-Day Exchange Rate Trend`
},
},
}}
/>
</div>
);
};
export default RateChart;
Deployment
Deploy to Vercel, Netlify, or any static hosting:
# Build for production
npm run build
# Deploy to Vercel
npx vercel --prod
# Or deploy to Netlify
npm install -g netlify-cli
netlify deploy --prod --dir=build
Why React + RBA API?
This combination gives you:
- Real-time Updates: React's state management handles live data perfectly
- Official Data: RBA rates ensure accuracy for Australian businesses
- Modern UX: Responsive design works on all devices
- Extensible: Easy to add charts, notifications, and integrations
- SEO Friendly: Can be server-side rendered with Next.js
Perfect for embedding in business dashboards, e-commerce sites, or financial applications where accurate AUD exchange rates are critical.
Next Steps
Enhance your converter with:
- Next.js for server-side rendering and better SEO
- PWA features for offline functionality
- Push notifications for rate alerts
- Integration with payment processors
- Multi-language support for international users
For backend API integration, check out our Python implementation with Flask and database caching. WordPress users should explore our WordPress plugin tutorial for easy shortcode integration and WooCommerce support.
The official RBA data ensures compliance with Australian financial regulations, making this ideal for business applications.
We are not affiliated with or endorsed by the Reserve Bank of Australia.
For API documentation, visit Exchange Rates API Docs