---
title: "How to Build a Real-Time Currency Converter in React"
description: "Step-by-step tutorial to build a real-time currency converter in React using the AllRatesToday API. Includes live rates, conversion, and a polished UI."
canonical: https://allratestoday.com/blog/how-to-build-currency-converter-react-2026/
source: AllRatesToday (https://allratestoday.com/) — Markdown mirror of the page above; index of all pages at https://allratestoday.com/llms.txt
generated: 2026-10-08T09:34:08.553Z
---

![React frontend development](https://images.pexels.com/photos/4578665/pexels-photo-4578665.jpeg?auto=compress&cs=tinysrgb&dpr=2&h=650&w=940)

## 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 institutional interbank market data, 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

- **Node.js 18+** installed on your machine
- Basic familiarity with **React** (hooks, state, effects)
- A free **AllRatesToday API key** — [sign up here](https://allratestoday.com/register) (no credit card required)

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:

- The **swap button** flips the `from` and `to` currencies and clears the previous result so users know they need to convert again.
- The **amount input** uses `type="number"` with validation to prevent non-numeric or negative values.
- The **result display** formats numbers with locale-aware separators and shows both the total and the per-unit rate.

## 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: #1a7a40;
  transition: background 0.2s;
}

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

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

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

.convert-btn:hover {
  background: #1a7a40;
}

.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:

- **Invalid API key:** The SDK throws an authentication error. Display a message prompting the user to check their key.
- **Rate limit exceeded:** If you exceed your plan's request limit, the API returns a 429 status. Show a "try again later" message and consider caching results.
- **Network failure:** Wrap API calls in try/catch and display a user-friendly error instead of letting the app crash.
- **Invalid input:** Validate the amount before calling `convert()`. Reject negative numbers, empty strings, and non-numeric input.

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

- How to initialize the `@allratestoday/sdk` client in a React project
- How to populate currency dropdowns dynamically using `symbols()`
- How to convert amounts in real time using `convert()`
- How to fetch and display historical exchange rates using `timeSeries()`
- How to handle API errors gracefully (auth failures, rate limits, network issues)
- How to build a polished converter UI with vanilla CSS

AllRatesToday gives you real-time mid-market rates for 160+ currencies, sourced from institutional interbank market data. 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](https://exchange-rateapi.com/) page.

 [Get Your Free API Key](https://allratestoday.com/register)

## Related Articles

- [Handle Currency Conversion in React](https://allratestoday.com/blog/how-to-handle-currency-conversion-react/)
- [Build a Currency Converter in JavaScript](https://allratestoday.com/blog/how-to-build-currency-converter-javascript/)
- [Best Exchange Rate API for Node.js](https://allratestoday.com/blog/best-exchange-rate-api-for-nodejs-2026/)
- [Best Currency API for JavaScript](https://allratestoday.com/best/best-currency-api-for-javascript/)
