Home Documentation Playground Pricing API Status Blog About FAQ Support

How to Build a Real-Time Currency Converter in React

Reviewed by Madhushan, Fintech Developer — May 2026

What We Will Build

In this tutorial, you will build a fully functional currency converter in React that fetches real-time exchange rates from the AllRatesToday API. The finished app will let users select any two currencies from 160+ options, enter an amount, and see the converted value instantly. We will also add a swap button, proper error handling, and a bonus historical rate chart.

AllRatesToday provides mid-market rates sourced from Reuters (Refinitiv) and interbank feeds, updated every 60 seconds. Unlike tutorial APIs that return stale daily data, your converter will show rates that reflect the actual forex market.

Source code: The complete project is available at the end of this tutorial. You can also follow along step by step.

Prerequisites

Your API key will look like art_live_.... Keep it handy — you will need it in a moment.

Project Setup with Vite

We will scaffold the project using Vite, which gives us fast hot module replacement and a clean starting point.

npm create vite@latest currency-converter -- --template react
cd currency-converter
npm install

Verify the app runs:

npm run dev

You should see the default Vite + React page at http://localhost:5173. Now install the AllRatesToday SDK:

npm install @allratestoday/sdk

The @allratestoday/sdk package provides a typed client with methods for rates, conversion, symbols, and historical data. No need to write raw fetch calls.

Step 1: API Client Setup

Create a file called src/api.js that initializes the AllRatesToday client. This keeps your API key in one place and makes the client available throughout the app.

// src/api.js
import AllRatesToday from '@allratestoday/sdk';

const client = new AllRatesToday('art_live_YOUR_API_KEY');

export default client;

Security note: For a production app, store your API key in an environment variable (VITE_ART_API_KEY) and access it via import.meta.env.VITE_ART_API_KEY. Never commit API keys to version control.

Step 2: Currency Selector Using symbols()

The AllRatesToday SDK includes a symbols() method that returns all supported currencies with their codes and names. We will use this to build a reusable dropdown component.

// src/CurrencySelector.jsx
import { useState, useEffect } from 'react';
import client from './api';

function CurrencySelector({ value, onChange, label }) {
  const [currencies, setCurrencies] = useState([]);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    client.symbols()
      .then((data) => {
        // data.symbols is an object like { USD: "US Dollar", EUR: "Euro", ... }
        const list = Object.entries(data.symbols).map(([code, name]) => ({
          code,
          name,
        }));
        list.sort((a, b) => a.code.localeCompare(b.code));
        setCurrencies(list);
        setLoading(false);
      })
      .catch((err) => {
        console.error('Failed to load currencies:', err);
        setLoading(false);
      });
  }, []);

  if (loading) return <select disabled><option>Loading...</option></select>;

  return (
    <div className="selector">
      <label>{label}</label>
      <select value={value} onChange={(e) => onChange(e.target.value)}>
        {currencies.map((c) => (
          <option key={c.code} value={c.code}>
            {c.code} &mdash; {c.name}
          </option>
        ))}
      </select>
    </div>
  );
}

export default CurrencySelector;

This component fetches the currency list once on mount, sorts it alphabetically, and renders a <select> element. The value and onChange props let the parent component control which currency is selected.

Step 3: Conversion Using convert()

The SDK's convert() method handles the conversion in a single call. It accepts a source currency, target currency, and amount, then returns the converted result along with the rate used.

// Example usage of convert()
const result = await client.convert('USD', 'EUR', 1000);

// result object:
// {
//   source: "USD",
//   target: "EUR",
//   amount: 1000,
//   result: 923.40,
//   rate: 0.9234,
//   time: "2026-05-25T12:00:00Z"
// }

We will wire this into our main converter component next.

Step 4: Amount Input, Display, and Swap Button

Now we build the main Converter component that ties everything together: two currency selectors, an amount input, a swap button, and the conversion result.

// src/Converter.jsx
import { useState, useCallback } from 'react';
import client from './api';
import CurrencySelector from './CurrencySelector';

function Converter() {
  const [from, setFrom] = useState('USD');
  const [to, setTo] = useState('EUR');
  const [amount, setAmount] = useState('1000');
  const [result, setResult] = useState(null);
  const [rate, setRate] = useState(null);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);

  const handleConvert = useCallback(async () => {
    if (!amount || isNaN(amount) || Number(amount) <= 0) {
      setError('Please enter a valid amount greater than zero.');
      return;
    }

    setLoading(true);
    setError(null);

    try {
      const data = await client.convert(from, to, Number(amount));
      setResult(data.result);
      setRate(data.rate);
    } catch (err) {
      setError(err.message || 'Conversion failed. Please try again.');
      setResult(null);
      setRate(null);
    } finally {
      setLoading(false);
    }
  }, [from, to, amount]);

  const handleSwap = () => {
    setFrom(to);
    setTo(from);
    setResult(null);
    setRate(null);
  };

  return (
    <div className="converter">
      <h1>Currency Converter</h1>

      <div className="converter-row">
        <CurrencySelector value={from} onChange={setFrom} label="From" />

        <button className="swap-btn" onClick={handleSwap} title="Swap currencies">
          &#8646;
        </button>

        <CurrencySelector value={to} onChange={setTo} label="To" />
      </div>

      <div className="amount-row">
        <label>Amount</label>
        <input
          type="number"
          value={amount}
          onChange={(e) => setAmount(e.target.value)}
          min="0"
          step="any"
          placeholder="Enter amount"
        />
      </div>

      <button className="convert-btn" onClick={handleConvert} disabled={loading}>
        {loading ? 'Converting...' : 'Convert'}
      </button>

      {error && <div className="error-msg">{error}</div>}

      {result !== null && (
        <div className="result-box">
          <p className="result-amount">
            {Number(amount).toLocaleString()} {from} = <strong>{result.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 4 })} {to}</strong>
          </p>
          <p className="result-rate">
            1 {from} = {rate} {to}
          </p>
        </div>
      )}
    </div>
  );
}

export default Converter;

Key details in this component:

Step 5: Add Styling

Replace the contents of src/App.css with the following styles. This gives the converter a clean, professional look without any external CSS framework.

/* src/App.css */
* {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

body {
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
  background: #f8fafc;
  color: #1a1a1a;
  min-height: 100vh;
  display: flex;
  justify-content: center;
  align-items: flex-start;
  padding: 3rem 1rem;
}

.converter {
  background: white;
  border-radius: 12px;
  box-shadow: 0 4px 24px rgba(0, 0, 0, 0.08);
  padding: 2.5rem;
  max-width: 520px;
  width: 100%;
}

.converter h1 {
  font-size: 1.5rem;
  font-weight: 700;
  margin-bottom: 1.5rem;
  color: #111827;
  text-align: center;
}

.converter-row {
  display: flex;
  align-items: flex-end;
  gap: 0.75rem;
  margin-bottom: 1.25rem;
}

.selector {
  flex: 1;
}

.selector label,
.amount-row label {
  display: block;
  font-size: 0.85rem;
  font-weight: 600;
  color: #374151;
  margin-bottom: 0.4rem;
}

.selector select,
.amount-row input {
  width: 100%;
  padding: 0.65rem 0.75rem;
  border: 1px solid #d1d5db;
  border-radius: 8px;
  font-size: 0.95rem;
  color: #1f2937;
  background: #fff;
}

.swap-btn {
  background: #f0fdf4;
  border: 1px solid #bbf7d0;
  border-radius: 8px;
  font-size: 1.25rem;
  padding: 0.6rem 0.75rem;
  cursor: pointer;
  color: #25a557;
  transition: background 0.2s;
}

.swap-btn:hover {
  background: #dcfce7;
}

.amount-row {
  margin-bottom: 1.25rem;
}

.convert-btn {
  width: 100%;
  padding: 0.75rem;
  background: #2ed06e;
  color: white;
  font-weight: 700;
  font-size: 1rem;
  border: none;
  border-radius: 8px;
  cursor: pointer;
  transition: background 0.2s;
}

.convert-btn:hover {
  background: #25a557;
}

.convert-btn:disabled {
  opacity: 0.6;
  cursor: not-allowed;
}

.error-msg {
  background: #fef2f2;
  color: #991b1b;
  border-left: 4px solid #ef4444;
  padding: 0.75rem 1rem;
  border-radius: 0 8px 8px 0;
  margin-top: 1rem;
  font-size: 0.9rem;
}

.result-box {
  background: #f0fdf4;
  border: 1px solid #bbf7d0;
  border-radius: 8px;
  padding: 1.25rem;
  margin-top: 1.25rem;
  text-align: center;
}

.result-amount {
  font-size: 1.15rem;
  color: #111827;
  margin-bottom: 0.25rem;
}

.result-rate {
  font-size: 0.85rem;
  color: #6b7280;
}

Then update src/App.jsx to render the converter:

// src/App.jsx
import './App.css';
import Converter from './Converter';

function App() {
  return <Converter />;
}

export default App;

At this point, run npm run dev and you should have a working currency converter with live rates from AllRatesToday.

Step 6: Error Handling

The converter component above already includes basic error handling, but let us look at the specific scenarios you should account for in a production app:

Here is a more robust error handler you can use:

function getErrorMessage(err) {
  if (err.status === 401) {
    return 'Invalid API key. Check your AllRatesToday credentials.';
  }
  if (err.status === 429) {
    return 'Rate limit exceeded. Please wait a moment and try again.';
  }
  if (err.message?.includes('network') || err.message?.includes('fetch')) {
    return 'Network error. Check your internet connection.';
  }
  return err.message || 'Something went wrong. Please try again.';
}

Bonus: Historical Rate Chart Using timeSeries()

The AllRatesToday SDK includes a timeSeries() method that returns historical rates for a currency pair over a date range. Let us add a simple chart below the converter to show the last 30 days of rate history.

First, install a lightweight chart library:

npm install recharts

Then create a RateChart component:

// src/RateChart.jsx
import { useState, useEffect } from 'react';
import { LineChart, Line, XAxis, YAxis, Tooltip, ResponsiveContainer } from 'recharts';
import client from './api';

function RateChart({ from, to }) {
  const [data, setData] = useState([]);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    setLoading(true);

    const endDate = new Date().toISOString().split('T')[0];
    const startDate = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000)
      .toISOString()
      .split('T')[0];

    client
      .timeSeries(from, to, startDate, endDate)
      .then((response) => {
        // response.rates is an object keyed by date
        const chartData = Object.entries(response.rates)
          .map(([date, rateInfo]) => ({
            date,
            rate: rateInfo[to],
          }))
          .sort((a, b) => a.date.localeCompare(b.date));

        setData(chartData);
        setLoading(false);
      })
      .catch((err) => {
        console.error('Failed to load historical rates:', err);
        setLoading(false);
      });
  }, [from, to]);

  if (loading) return <p style={{ textAlign: 'center', color: '#6b7280' }}>Loading chart...</p>;
  if (data.length === 0) return null;

  return (
    <div className="chart-container">
      <h3>{from}/{to} &mdash; Last 30 Days</h3>
      <ResponsiveContainer width="100%" height={250}>
        <LineChart data={data}>
          <XAxis
            dataKey="date"
            tick={{ fontSize: 11 }}
            tickFormatter={(d) => d.slice(5)}
          />
          <YAxis
            domain={['auto', 'auto']}
            tick={{ fontSize: 11 }}
            tickFormatter={(v) => v.toFixed(4)}
          />
          <Tooltip
            formatter={(value) => [value.toFixed(4), 'Rate']}
            labelFormatter={(label) => label}
          />
          <Line
            type="monotone"
            dataKey="rate"
            stroke="#2ed06e"
            strokeWidth={2}
            dot={false}
          />
        </LineChart>
      </ResponsiveContainer>
    </div>
  );
}

export default RateChart;

Add the chart below the result in your Converter component:

// Inside Converter.jsx, after the result-box div:
import RateChart from './RateChart';

// Add this after the closing div of result-box:
{result !== null && <RateChart from={from} to={to} />}

Add matching styles to App.css:

.chart-container {
  margin-top: 1.5rem;
  padding-top: 1rem;
  border-top: 1px solid #e5e7eb;
}

.chart-container h3 {
  font-size: 0.95rem;
  color: #374151;
  margin-bottom: 0.75rem;
  text-align: center;
}

Now after each conversion, users see a 30-day trend chart for the selected currency pair, rendered entirely from AllRatesToday historical data.

Full Code Listing

Here is the complete project with all files. Copy each file into the corresponding path in your Vite project.

src/api.js

import AllRatesToday from '@allratestoday/sdk';

const API_KEY = import.meta.env.VITE_ART_API_KEY || 'art_live_YOUR_API_KEY';

const client = new AllRatesToday(API_KEY);

export default client;

src/CurrencySelector.jsx

import { useState, useEffect } from 'react';
import client from './api';

function CurrencySelector({ value, onChange, label }) {
  const [currencies, setCurrencies] = useState([]);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    client.symbols()
      .then((data) => {
        const list = Object.entries(data.symbols).map(([code, name]) => ({
          code,
          name,
        }));
        list.sort((a, b) => a.code.localeCompare(b.code));
        setCurrencies(list);
        setLoading(false);
      })
      .catch((err) => {
        console.error('Failed to load currencies:', err);
        setLoading(false);
      });
  }, []);

  if (loading) return <select disabled><option>Loading...</option></select>;

  return (
    <div className="selector">
      <label>{label}</label>
      <select value={value} onChange={(e) => onChange(e.target.value)}>
        {currencies.map((c) => (
          <option key={c.code} value={c.code}>
            {c.code} - {c.name}
          </option>
        ))}
      </select>
    </div>
  );
}

export default CurrencySelector;

src/Converter.jsx

import { useState, useCallback } from 'react';
import client from './api';
import CurrencySelector from './CurrencySelector';
import RateChart from './RateChart';

function getErrorMessage(err) {
  if (err.status === 401) {
    return 'Invalid API key. Check your AllRatesToday credentials.';
  }
  if (err.status === 429) {
    return 'Rate limit exceeded. Please wait a moment and try again.';
  }
  if (err.message?.includes('network') || err.message?.includes('fetch')) {
    return 'Network error. Check your internet connection.';
  }
  return err.message || 'Something went wrong. Please try again.';
}

function Converter() {
  const [from, setFrom] = useState('USD');
  const [to, setTo] = useState('EUR');
  const [amount, setAmount] = useState('1000');
  const [result, setResult] = useState(null);
  const [rate, setRate] = useState(null);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);

  const handleConvert = useCallback(async () => {
    if (!amount || isNaN(amount) || Number(amount) <= 0) {
      setError('Please enter a valid amount greater than zero.');
      return;
    }

    setLoading(true);
    setError(null);

    try {
      const data = await client.convert(from, to, Number(amount));
      setResult(data.result);
      setRate(data.rate);
    } catch (err) {
      setError(getErrorMessage(err));
      setResult(null);
      setRate(null);
    } finally {
      setLoading(false);
    }
  }, [from, to, amount]);

  const handleSwap = () => {
    setFrom(to);
    setTo(from);
    setResult(null);
    setRate(null);
  };

  return (
    <div className="converter">
      <h1>Currency Converter</h1>

      <div className="converter-row">
        <CurrencySelector value={from} onChange={setFrom} label="From" />
        <button className="swap-btn" onClick={handleSwap} title="Swap currencies">
          &#8646;
        </button>
        <CurrencySelector value={to} onChange={setTo} label="To" />
      </div>

      <div className="amount-row">
        <label>Amount</label>
        <input
          type="number"
          value={amount}
          onChange={(e) => setAmount(e.target.value)}
          min="0"
          step="any"
          placeholder="Enter amount"
        />
      </div>

      <button className="convert-btn" onClick={handleConvert} disabled={loading}>
        {loading ? 'Converting...' : 'Convert'}
      </button>

      {error && <div className="error-msg">{error}</div>}

      {result !== null && (
        <div className="result-box">
          <p className="result-amount">
            {Number(amount).toLocaleString()} {from} ={' '}
            <strong>
              {result.toLocaleString(undefined, {
                minimumFractionDigits: 2,
                maximumFractionDigits: 4,
              })}{' '}
              {to}
            </strong>
          </p>
          <p className="result-rate">1 {from} = {rate} {to}</p>
        </div>
      )}

      {result !== null && <RateChart from={from} to={to} />}
    </div>
  );
}

export default Converter;

src/RateChart.jsx

import { useState, useEffect } from 'react';
import {
  LineChart, Line, XAxis, YAxis, Tooltip, ResponsiveContainer,
} from 'recharts';
import client from './api';

function RateChart({ from, to }) {
  const [data, setData] = useState([]);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    setLoading(true);

    const endDate = new Date().toISOString().split('T')[0];
    const startDate = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000)
      .toISOString()
      .split('T')[0];

    client
      .timeSeries(from, to, startDate, endDate)
      .then((response) => {
        const chartData = Object.entries(response.rates)
          .map(([date, rateInfo]) => ({
            date,
            rate: rateInfo[to],
          }))
          .sort((a, b) => a.date.localeCompare(b.date));

        setData(chartData);
        setLoading(false);
      })
      .catch((err) => {
        console.error('Failed to load historical rates:', err);
        setLoading(false);
      });
  }, [from, to]);

  if (loading) {
    return <p style={{ textAlign: 'center', color: '#6b7280' }}>Loading chart...</p>;
  }
  if (data.length === 0) return null;

  return (
    <div className="chart-container">
      <h3>{from}/{to} -- Last 30 Days</h3>
      <ResponsiveContainer width="100%" height={250}>
        <LineChart data={data}>
          <XAxis dataKey="date" tick={{ fontSize: 11 }} tickFormatter={(d) => d.slice(5)} />
          <YAxis domain={['auto', 'auto']} tick={{ fontSize: 11 }} tickFormatter={(v) => v.toFixed(4)} />
          <Tooltip formatter={(value) => [value.toFixed(4), 'Rate']} labelFormatter={(l) => l} />
          <Line type="monotone" dataKey="rate" stroke="#2ed06e" strokeWidth={2} dot={false} />
        </LineChart>
      </ResponsiveContainer>
    </div>
  );
}

export default RateChart;

src/App.jsx

import './App.css';
import Converter from './Converter';

function App() {
  return <Converter />;
}

export default App;

What You Learned

AllRatesToday gives you real-time mid-market rates for 160+ currencies, sourced from Reuters/Refinitiv and interbank feeds. The free tier is enough to build and test your converter, and you can upgrade as your traffic grows.

Build Your Own Currency Converter Today

Get your free API key in 30 seconds. Real-time rates for 160+ currencies, official SDKs, and no credit card required. See all plans on our Exchange Rate API page.

Get Your Free API Key