Home Documentation Playground Pricing API Status Blog About FAQ Support

How to Handle Currency Conversion in React

Reviewed by Madhushan, Fintech Developer — May 2026

Multi-currency support sounds like a small feature until you ship it. You start with a tidy convertPrice() helper, and a month later you are debugging why German users see $1,234.56 instead of 1.234,56 €, why a stale rate caused a free shipping threshold to flip, and why your test suite breaks every time the exchange rate moves.

This guide walks through the patterns that hold up in production React apps: where to fetch rates, how to cache them, how to display them so they look native to the user's locale, and where the line is between a display conversion and a money-touching one.

What you will build: A React component that fetches live exchange rates, converts prices client-side, formats them with locale-correct symbols, and falls back gracefully when the API is unreachable.

Two ways to do it

For a typical React app, you have two reasonable approaches:

Approach Best for Effort
Official React SDK (react-currency-localizer-realtime) Drop-in product price localization, marketing pages, MVPs ~5 minutes
Custom hook over the REST API Apps that need their own caching, server-side rendering, or business logic around rates 30 minutes

Both call the same underlying real-time exchange rate API. The SDK is a convenience wrapper; nothing prevents you from mixing them in the same app.

The quickest path: the official React hook

If you only need to localize prices on a product page or a pricing table, the official React hook is the fastest route. Install the package:

npm install react-currency-localizer-realtime

Then use the useCurrencyConverter hook to convert a single price, or useCurrencyLocalizer to convert many at once with a single rate fetch.

import { useCurrencyConverter } from 'react-currency-localizer-realtime';

function ProductPrice({ priceUSD }) {
  const { convertedPrice, localCurrency, isLoading, error } = useCurrencyConverter({
    basePrice: priceUSD,
    baseCurrency: 'USD',
    apiKey: process.env.NEXT_PUBLIC_ALLRATESTODAY_KEY,
  });

  if (isLoading) return <span className="price-skeleton" />;
  if (error) return <span>${priceUSD}</span>; // fall back to base price

  return (
    <span>
      {new Intl.NumberFormat(undefined, {
        style: 'currency',
        currency: localCurrency || 'USD',
      }).format(convertedPrice ?? priceUSD)}
    </span>
  );
}

The hook detects the user's currency from their browser locale, fetches the rate once per session, and handles loading/error states so you do not have to wire them up by hand.

For a product list, batch the conversion so you only fetch the rate once for the whole list:

import { useCurrencyLocalizer } from 'react-currency-localizer-realtime';

function ProductList({ products }) {
  const { convertAndFormat, isReady } = useCurrencyLocalizer({
    baseCurrency: 'USD',
    apiKey: process.env.NEXT_PUBLIC_ALLRATESTODAY_KEY,
  });

  return (
    <ul>
      {products.map(p => (
        <li key={p.id}>
          {p.name}: {isReady ? convertAndFormat(p.priceUSD) : '…'}
        </li>
      ))}
    </ul>
  );
}

Building your own custom hook

If you want full control — your own cache key, server-side rendering, custom error handling, or rate freshness rules tied to business logic — write a thin custom hook. The structure below is the one I keep ending up at after every greenfield project.

Step 1: Wrap the API call

// lib/getRate.ts
const API = 'https://allratestoday.com/api/v1/rates';

export async function getRate(source: string, target: string, apiKey: string) {
  const res = await fetch(`${API}?source=${source}&target=${target}`, {
    headers: { Authorization: `Bearer ${apiKey}` },
  });
  if (!res.ok) {
    throw new Error(`Rate fetch failed: ${res.status}`);
  }
  const data = await res.json();
  return data.rate as number;
}

Keep this function pure — no React, no global state, no caching. That makes it trivial to unit-test and to reuse on the server (Next.js route handlers, server actions, getServerSideProps).

Step 2: Cache with SWR or React Query

Do not roll your own cache. SWR and React Query both handle deduplication, stale-while-revalidate, retries, and SSR hydration in a few lines. Here is the SWR version:

// hooks/useRate.ts
import useSWR from 'swr';
import { getRate } from '@/lib/getRate';

const ONE_MINUTE = 60 * 1000;

export function useRate(source: string, target: string) {
  return useSWR(
    source === target ? null : ['rate', source, target],
    () => getRate(source, target, process.env.NEXT_PUBLIC_ALLRATESTODAY_KEY!),
    {
      refreshInterval: 5 * ONE_MINUTE, // refresh in the background every 5 min
      dedupingInterval: ONE_MINUTE,
      revalidateOnFocus: false,
    }
  );
}

Two things worth noticing:

Step 3: A reusable converter component

// components/Money.tsx
import { useRate } from '@/hooks/useRate';

type Props = {
  amount: number;
  from: string;
  to: string;
  locale?: string;
};

export function Money({ amount, from, to, locale }: Props) {
  const { data: rate, error, isLoading } = useRate(from, to);

  if (error) {
    // Fall back to the base currency rather than blanking the price.
    return <Format amount={amount} currency={from} locale={locale} />;
  }
  if (isLoading || rate == null) {
    return <span className="money-skeleton" aria-busy="true">…</span>;
  }

  return <Format amount={amount * rate} currency={to} locale={locale} />;
}

function Format({ amount, currency, locale }: { amount: number; currency: string; locale?: string }) {
  return (
    <span>
      {new Intl.NumberFormat(locale, { style: 'currency', currency }).format(amount)}
    </span>
  );
}

Now <Money amount=29.99 from="USD" to="EUR" /> works anywhere in the app, and the rate is cached app-wide.

Locale-aware formatting (the part everyone gets wrong)

The number 1234.5 is rendered six different ways across major locales:

LocaleOutput
en-US$1,234.50
en-GB£1,234.50
de-DE1.234,50 €
fr-FR1 234,50 €
ja-JP¥1,235
en-IN₹1,234.50

You do not need a library for this. Intl.NumberFormat ships in every modern runtime — browser, Node, Deno, Cloudflare Workers — and handles symbol placement, separators, and currency-specific rounding (yen has no decimals, dinar has three).

new Intl.NumberFormat('de-DE', {
  style: 'currency',
  currency: 'EUR',
}).format(1234.5);
// "1.234,50 €"

Pass undefined as the locale to use the user's browser default. Pass an explicit locale when you have one (from a user preference or your i18n library).

App-wide currency state

For an e-commerce or SaaS app where the user picks their preferred currency once and every price reflects it, lift the choice into a context.

// contexts/CurrencyContext.tsx
import { createContext, useContext, useState } from 'react';

type Ctx = { currency: string; setCurrency: (c: string) => void };
const CurrencyContext = createContext<Ctx | null>(null);

export function CurrencyProvider({ children }: { children: React.ReactNode }) {
  const [currency, setCurrency] = useState(() => {
    if (typeof window === 'undefined') return 'USD';
    return localStorage.getItem('currency')
      ?? new Intl.NumberFormat().resolvedOptions().currency
      ?? 'USD';
  });

  const set = (c: string) => {
    setCurrency(c);
    localStorage.setItem('currency', c);
  };

  return (
    <CurrencyContext.Provider value={{ currency, setCurrency: set }}>
      {children}
    </CurrencyContext.Provider>
  );
}

export const useCurrency = () => {
  const ctx = useContext(CurrencyContext);
  if (!ctx) throw new Error('useCurrency must be used inside CurrencyProvider');
  return ctx;
};

Then the Money component reads from context instead of taking to as a prop:

function Price({ usd }: { usd: number }) {
  const { currency } = useCurrency();
  return <Money amount={usd} from="USD" to={currency} />;
}

The display-vs-money line

Everything above is a display conversion — it shows a friendlier number to the user. The moment money actually changes hands, the rules change:

Never charge the user the client-side converted amount. The browser sees rates that may be stale, may have been tampered with by an extension, or may have failed and silently fallen back. Always re-fetch the rate on the server at the moment of the transaction and store it on the order.

The pattern that holds up:

  1. Display the converted price client-side for the shopper. Be honest in the UI: "Charged in USD; shown in EUR for reference at today's rate."
  2. On checkout, the server calls the rate API itself, calculates the final charge, and writes the rate, the source amount, and the target amount onto the order.
  3. Refunds, invoices, and accounting all read the locked rate from the order — never re-fetch.

Common pitfalls

Floating-point drift on multi-step conversion

If you convert USD → EUR → GBP via two separate rates, you will accumulate rounding error. Either fetch the direct USD → GBP rate, or do the math in cents (integers) and round only at display time.

Symbol-only formatting

Hard-coding $ in your JSX assumes the user knows whose dollar you mean. CAD, AUD, NZD, HKD, SGD, MXN, ARS, COP and others all use a dollar sign. Use Intl.NumberFormat with currency, not raw symbols.

SSR hydration mismatches

If the server renders prices in USD and the client switches to EUR after hydration, React will warn about mismatched HTML. Either render the converted price on the server using a default currency (USD/EUR is fine) or render only a placeholder until isReady on the client.

Refetching every render

If your custom hook calls fetch directly without a cache, every component re-render and every navigation will fire a request. SWR or React Query solves this in one line. If you really want to avoid a dependency, at minimum memoize the rate in a module-level Map with a TTL.

Frequently Asked Questions

What is the best way to convert currency in a React app?

Fetch live exchange rates from a real-time API once per session, cache them with SWR or React Query, and convert prices client-side for display. For a drop-in solution, react-currency-localizer-realtime wraps the AllRatesToday API in two purpose-built hooks.

Should I convert currency on the client or the server?

Client-side conversions are fine for display — pricing tables, product cards, dashboards. Anything that touches money you actually charge — checkout totals, invoices, refunds — must be converted on the server using the rate at transaction time, then locked into the order record.

How do I format currency in React?

Use the built-in Intl.NumberFormat API with style: 'currency' and an ISO currency code. It handles symbol placement, decimal separators, thousands separators, and currency-specific rounding for every locale, with no extra dependencies.

How often should a React app refresh exchange rates?

For most apps, every 5 to 15 minutes is plenty. Real-time mid-market data (60-second freshness) only matters when the user is actively making a financial decision — checkout, transfer, or quote. For a marketing pricing page, a once-per-session fetch is enough.

Can I use this with Next.js, Remix, or Astro?

Yes. The custom hook pattern works in any React framework. For server components and route handlers, call the same getRate() helper directly — the SDK is React-only, but the underlying REST API is just HTTP.

Build with Real-Time Rates

Get a free API key for 160+ currencies updated every 60 seconds. Works with the React hook, the JS SDK, or plain fetch.

Get Your Free API Key