Как создать конвертер валют с React и API обменных курсов в реальном времени
Создание конвертера валют с React — отличный проект для разработчиков, которые хотят практиковать интеграцию API, React-хуки и управление состоянием в реальных приложениях. В этом пошаговом туториале вы создадите полнофункциональный конвертер валют в реальном времени, который получает актуальные обменные курсы из валютного API, поддерживает более 170 валют и изящно обрабатывает ошибки — всё с использованием современных паттернов React.
Если вы новичок и хотите понять, как работают обменные курсы, изучите наш объяснитель обменных курсов перед началом.
Что вы создадите
К концу туториала у вас будет конвертер валют на React, который:
- Получает обменные курсы в реальном времени из API Finexly
- Поддерживает более 170 валют через выпадающий список
- Конвертирует суммы в реальном времени по мере ввода пользователя (с дебаунсингом)
- Изящно обрабатывает состояния загрузки и ошибки API
- Отображает курс и временную метку последнего обновления
Структура проекта:
currency-converter/
├── src/
│ ├── components/
│ │ └── CurrencyConverter.tsx
│ ├── hooks/
│ │ └── useExchangeRate.ts
│ ├── App.tsx
│ └── main.tsx
├── package.json
└── vite.config.tsПредварительные условия
Прежде чем начать, убедитесь, что у вас есть:
- Установленный Node.js 18+
- Базовые знания хуков React (
useState,useEffect) - Бесплатный API-ключ Finexly (получить занимает около 30 секунд)
Шаг 1: Настройка проекта React
В качестве инструмента сборки будем использовать Vite:
npm create vite@latest currency-converter -- --template react-ts
cd currency-converter
npm install
npm run devСервер разработки запустится по адресу http://localhost:5173.
Шаг 2: Понимание API Finexly
API Finexly предоставляет обменные курсы в реальном времени и исторические данные для более чем 170 валют через простой REST-интерфейс:
GET https://finexly.com/api/v1/latest?base=USDТипичный ответ:
{
"base": "USD",
"date": "2026-04-07",
"rates": {
"EUR": 0.9182,
"GBP": 0.7871,
"JPY": 151.42
},
"timestamp": 1744012800
}Включите свой ключ в заголовок запроса:
Authorization: Bearer ВАШ_API_КЛЮЧВы можете зарегистрироваться для получения бесплатного плана с 1 000 запросов в месяц.
Шаг 3: Создание кастомного хука
Создайте файл src/hooks/useExchangeRate.ts:
import { useState, useEffect, useCallback } from "react";
interface ExchangeRateResult {
rates: Record<string, number> | null;
loading: boolean;
error: string | null;
lastUpdated: Date | null;
}
const API_KEY = import.meta.env.VITE_FINEXLY_API_KEY;
const BASE_URL = "https://finexly.com/api/v1";
const rateCache: Record<string, { rates: Record<string, number>; timestamp: number }> = {};
const CACHE_TTL_MS = 5 * 60 * 1000;
export function useExchangeRate(baseCurrency: string): ExchangeRateResult {
const [rates, setRates] = useState<Record<string, number> | null>(null);
const [loading, setLoading] = useState<boolean>(false);
const [error, setError] = useState<string | null>(null);
const [lastUpdated, setLastUpdated] = useState<Date | null>(null);
const fetchRates = useCallback(async (base: string) => {
const cached = rateCache[base];
if (cached && Date.now() - cached.timestamp < CACHE_TTL_MS) {
setRates(cached.rates);
setLastUpdated(new Date(cached.timestamp));
return;
}
setLoading(true);
setError(null);
try {
const res = await fetch(`${BASE_URL}/latest?base=${base}`, {
headers: { Authorization: `Bearer ${API_KEY}` },
});
if (!res.ok) throw new Error(`Ошибка API: ${res.status} ${res.statusText}`);
const data = await res.json();
rateCache[base] = { rates: data.rates, timestamp: Date.now() };
setRates(data.rates);
setLastUpdated(new Date());
} catch (err) {
setError(err instanceof Error ? err.message : "Не удалось получить обменные курсы");
} finally {
setLoading(false);
}
}, []);
useEffect(() => {
if (baseCurrency) fetchRates(baseCurrency);
}, [baseCurrency, fetchRates]);
return { rates, loading, error, lastUpdated };
}Шаг 4: Создание компонента CurrencyConverter
Создайте src/components/CurrencyConverter.tsx:
import { useState, useMemo } from "react";
import { useExchangeRate } from "../hooks/useExchangeRate";
const CURRENCIES = [
"USD", "EUR", "GBP", "JPY", "CAD", "AUD", "CHF", "CNY",
"INR", "MXN", "BRL", "KRW", "SGD", "HKD", "NOK", "SEK",
];
export default function CurrencyConverter() {
const [amount, setAmount] = useState<string>("100");
const [baseCurrency, setBaseCurrency] = useState<string>("USD");
const [targetCurrency, setTargetCurrency] = useState<string>("EUR");
const { rates, loading, error, lastUpdated } = useExchangeRate(baseCurrency);
const convertedAmount = useMemo(() => {
if (!rates || !amount) return null;
const numAmount = parseFloat(amount);
if (isNaN(numAmount) || numAmount < 0) return null;
const rate = rates[targetCurrency];
return rate ? (numAmount * rate).toFixed(2) : null;
}, [rates, amount, targetCurrency]);
const handleSwap = () => {
setBaseCurrency(targetCurrency);
setTargetCurrency(baseCurrency);
};
return (
<div className="converter-card">
<h2>Конвертер валют в реальном времени</h2>
<div className="input-row">
<label htmlFor="amount">Сумма</label>
<input
id="amount"
type="number"
min="0"
value={amount}
onChange={(e) => setAmount(e.target.value)}
placeholder="Введите сумму"
/>
</div>
<div className="currency-row">
<div className="select-group">
<label htmlFor="base">Из</label>
<select id="base" value={baseCurrency} onChange={(e) => setBaseCurrency(e.target.value)}>
{CURRENCIES.map((c) => <option key={c} value={c}>{c}</option>)}
</select>
</div>
<button className="swap-btn" onClick={handleSwap} aria-label="Поменять валюты местами">⇄</button>
<div className="select-group">
<label htmlFor="target">В</label>
<select id="target" value={targetCurrency} onChange={(e) => setTargetCurrency(e.target.value)}>
{CURRENCIES.map((c) => <option key={c} value={c}>{c}</option>)}
</select>
</div>
</div>
<div className="result-area">
{loading && <p>Получение актуальных курсов...</p>}
{error && <p className="error-text">⚠ {error}</p>}
{!loading && !error && convertedAmount !== null && (
<>
<p className="converted-value">
{parseFloat(amount).toLocaleString()} {baseCurrency} =
<strong>{parseFloat(convertedAmount).toLocaleString()} {targetCurrency}</strong>
</p>
{lastUpdated && <p>Курсы обновлены: {lastUpdated.toLocaleTimeString()}</p>}
</>
)}
</div>
</div>
);
}Шаг 5: Переменные окружения
Создайте файл .env в корне проекта:
VITE_FINEXLY_API_KEY=ваш_api_ключ_здесьДобавьте .env в .gitignore.
Шаг 6: Сборка приложения
Обновите src/App.tsx:
import CurrencyConverter from "./components/CurrencyConverter";
function App() {
return (
<main>
<CurrencyConverter />
<footer>
<p>Обменные курсы предоставлены <a href="https://finexly.com">Finexly</a></p>
</footer>
</main>
);
}
export default App;Оптимизация производительности с дебаунсингом
// src/hooks/useDebounce.ts
import { useState, useEffect } from "react";
export function useDebounce<T>(value: T, delayMs: number): T {
const [debouncedValue, setDebouncedValue] = useState<T>(value);
useEffect(() => {
const timer = setTimeout(() => setDebouncedValue(value), delayMs);
return () => clearTimeout(timer);
}, [value, delayMs]);
return debouncedValue;
}Дальнейшее развитие
Ценные улучшения включают: исторический график курсов через историческое API Finexly, одновременное конвертирование в несколько валют, сохранение любимых пар в localStorage и рендеринг на стороне сервера с Next.js.
Часто задаваемые вопросы
Какой лучший API обменных курсов для React? Для React-приложений нужен REST API с поддержкой CORS. API Finexly поддерживает CORS на всех планах и охватывает более 170 валют с бесплатным уровнем.
Как часто обновляются обменные курсы? API Finexly обновляет курсы каждые 60 секунд для планов реального времени и раз в день для стандартных планов.
Можно ли использовать валютный API в клиентском React-приложении? Да, если API поддерживает CORS. Для продакшн-приложений рекомендуется маршрутизировать запросы через собственный бэкенд.
Как не раскрыть API-ключ в React?
Для разработки используйте файл .env Vite с префиксом VITE_. Для продакшна маршрутизируйте запросы через Next.js API Route или serverless-функцию.
Как конвертировать между двумя валютами без USD? API Finexly поддерживает любую базовую валюту на всех планах, что значительно упрощает эту задачу.
Готовы добавить обменные курсы в реальном времени в своё React-приложение? Получите бесплатный API-ключ Finexly — без кредитной карты. Начните с 1 000 бесплатных запросов в месяц и масштабируйтесь по мере роста.
Explore More
Vlado Grigirov
Senior Currency Markets Analyst & Financial Strategist
Vlado Grigirov is a senior currency markets analyst and financial strategist with over 14 years of experience in foreign exchange markets, cross-border finance, and currency risk management. He has wo...
View full profile →