How to format currency amounts in React Router v7
Display prices with currency symbols and separators
Problem
Displaying prices in web applications requires handling two interrelated localization concerns: the currency being used and the regional conventions for presenting monetary values. A price of 1200.50 US dollars appears as "$1,200.50" to users in the United States but as "1 200,50 $US" in France. The currency symbol position, decimal separator, thousands grouping, and spacing all vary by locale. When these conventions are not respected, users may misread amounts or question whether the price is correct, undermining trust in the application.
The challenge is compounded when an application serves multiple regions or allows users to view prices in different currencies. Hard-coding format strings or manually placing symbols creates brittle code that breaks when new locales or currencies are added. Without a systematic approach, maintaining consistent and correct price displays across an application becomes error-prone.
Solution
Format currency values using both the target currency code and the user's active locale. This approach delegates the complex rules of currency presentation to the browser's internationalization APIs, which already encode the formatting conventions for hundreds of locale-currency combinations.
By passing a numeric amount, a currency code, and the current locale to a formatting function, the system automatically applies the correct symbol, separators, and layout. This ensures that prices always appear in a format familiar to the user, regardless of which currency is being displayed or which region the user is in.
Steps
1. Create a reusable currency formatting component
Build a component that uses react-intl's FormattedNumber with the style and currency props to format monetary values.
import { FormattedNumber } from "react-intl";
interface PriceProps {
amount: number;
currency: string;
}
export function Price({ amount, currency }: PriceProps) {
return (
<FormattedNumber value={amount} style="currency" currency={currency} />
);
}
The FormattedNumber component uses the formatNumber API and accepts Intl.NumberFormatOptions. The style="currency" option tells the formatter to include currency symbols, and the currency prop specifies which currency to display.
2. Use the Price component in route components
Render prices in any React Router route component by passing the numeric value and currency code.
import { Price } from "~/components/Price";
export default function ProductPage() {
const product = {
name: "Wireless Headphones",
price: 129.99,
currency: "USD",
};
return (
<div>
<h1>{product.name}</h1>
<p>
<Price amount={product.price} currency={product.currency} />
</p>
</div>
);
}
The Price component automatically formats the amount according to the locale configured in the IntlProvider. For a US locale, this renders "$129.99"; for a German locale with the same USD currency, it renders "129,99 $".
3. Format currency imperatively when needed
For scenarios where you need the formatted string directly, use the useIntl hook to access the formatNumber method.
import { useIntl } from "react-intl";
export default function CheckoutSummary() {
const intl = useIntl();
const subtotal = 89.99;
const tax = 7.2;
const total = subtotal + tax;
const formattedTotal = intl.formatNumber(total, {
style: "currency",
currency: "EUR",
});
return (
<div>
<h2>Order Summary</h2>
<dl>
<dt>Subtotal</dt>
<dd>
{intl.formatNumber(subtotal, {
style: "currency",
currency: "EUR",
})}
</dd>
<dt>Tax</dt>
<dd>
{intl.formatNumber(tax, {
style: "currency",
currency: "EUR",
})}
</dd>
<dt>Total</dt>
<dd aria-label={`Total: ${formattedTotal}`}>
<strong>{formattedTotal}</strong>
</dd>
</dl>
</div>
);
}
The formatNumber method accepts a value and an options object with FormatNumberOptions. This is useful when you need the formatted string for attributes, logging, or non-JSX contexts.
4. Control decimal precision for whole-number currencies
Some currencies do not use fractional units. Adjust the number of decimal places to match currency conventions.
import { FormattedNumber } from "react-intl";
interface PriceProps {
amount: number;
currency: string;
}
export function Price({ amount, currency }: PriceProps) {
const isWholeNumberCurrency = currency === "JPY" || currency === "KRW";
return (
<FormattedNumber
value={amount}
style="currency"
currency={currency}
minimumFractionDigits={isWholeNumberCurrency ? 0 : 2}
maximumFractionDigits={isWholeNumberCurrency ? 0 : 2}
/>
);
}
Japanese yen does not use a minor unit, so amounts are displayed without decimals. The minimumFractionDigits and maximumFractionDigits options override the default decimal behavior when necessary.