Commerce TypeScript helper for React
v1.0.0Added in 1.0.0
Price formatting and cart math for React shops and marketplaces: prices, discounts, unit prices, coupons and cart totals with VAT.
Change the quantities in the cart with the minus and plus buttons: the subtotal, the savings from sale prices, the coupon, the free-delivery bar, the VAT line and the total all come from one computeCartTotals call and always add up. Pick a coupon to see it apply or explain why it does not (a minimum order shows what is still missing). The strip above shows the same prices through formatPrice, a range, a discount and a price per m² in the chosen locale and currency.
Commerce is a Pro React helper in the React hooks and utilities category, written in TypeScript for React 19. It installs as source with npx wingo-ui@latest add commerce, together with Number format.
Add to a project
Requirements: React 19, in Next.js (App Router or Pages), Vite or React Router. Installation guide · Next.js setup · Vite setup
npx wingo-ui@latest add commerceCommerce is part of Pro, with 298 more Pro items: $8 a month or $150 once. 14-day refund before you download any Pro item. See plans
Usage
Import formatPrice from @/lib/commerce.
import { computeCartTotals, formatPrice, RO_VAT_RATE } from "@/lib/commerce"
const totals = computeCartTotals(
[
{ id: "sofa", price: 850, quantity: 1 },
{ id: "pillows", price: 49.9, quantity: 2, compareAtPrice: 69.9 },
],
{ shipping: 19.99, freeShippingThreshold: 1000, taxRate: RO_VAT_RATE, coupons: [{ code: "FALL15", type: "percent", value: 15 }] },
)
formatPrice(totals.total, { locale: "en-US", currency: "USD" }) // "$827.32"
formatPrice(1200, { locale: "ro-RO", currency: "RON" }) // "1.200 lei"API
localedefault"en-US"stringThe demo's locale. Every formatter takes options.locale; pass the one from useLocale so the server and the browser agree.
currencydefault"USD"stringoptions.currency: an ISO 4217 code (the lib's own default is "USD"). Printed with the narrow symbol ("$", "€", "lei"), never Intl's "US$" or "RON".
fractionDigitsdefault"auto""auto" | numberoptions.fractionDigits: auto prints whole amounts without decimals ("$1,200") and the currency's digits otherwise ("$1,284.90"); a number forces that many.
coupondefault"none"stringThe demo's coupon: a percent (FALL15, capped at 150), a fixed amount (SAVE50), free shipping (FREESHIP) or one with a minimum order (MIN2000).
freeShippingThresholddefault1000number | nullcomputeCartTotals options.freeShippingThreshold: spend this much on items (after coupons) and delivery is free. 0 or null turns the offer off.
taxRatedefault21numberoptions.taxRate as a fraction (0.21, exported as RO_VAT_RATE; RO_REDUCED_VAT_RATE is 0.11); the demo shows it in percent. Lines can override it with their own taxRate (the book below is at 11%).
pricesIncludeTaxdefaulttruebooleanoptions.pricesIncludeTax: true (consumer retail) reports the VAT inside the total; false adds it on top (B2B price lists).
formatPrice(amount: number | null | undefined, options?: FormatPriceOptions) => stringThe main helper, a price as text: "$1,200", "$1,284.90", "1.284,90 lei" (ro-RO, RON), "489.000 €" (de-DE, EUR); "" for null and NaN. Options: currency (default USD), locale, fractionDigits, currencyDisplay ("narrowSymbol" | "symbol" | "code" | "name" | "none"), notation ("compact" gives "$489K"), signDisplay. Thin and narrow spaces become one no-break space so the server and the browser print the same text.
Parts
formatPriceParts
functionThe pieces of a price for styling, in the locale's order: "$" before "1,284.90" in en-US, "1.284,90" then "lei" after a space in ro-RO. Returns { formatted, amount, sign, currency, currencyPosition, spaced } or null.
formatPriceRange
functionTwo prices as one range joined by a spaced en dash, with the currency written once when it follows the number (ro-RO) and on both ends otherwise (en-US). A missing end prints the other alone.
discountPercent
functionThe whole-percent saving of a sale price against its old price: discountPercent(1199, 1599) is 25. 0 without a higher old price.
price*numberThe sale price.
compareAtPrice*number | null | undefinedThe old price.
returnsnumberA whole percent, e.g. 25.
formatDiscount
functionA discount badge in the locale: formatDiscount(25, "en-US") is "-25%" ("-25 %" in ro-RO); "" for 0.
unitPrice
functionThe price per unit, rounded to cents: unitPrice(489000, 54) is 9055.56. null when the quantity is not positive.
formatUnitPrice
functionA unit price as text: $489,000 for 54 m² reads "$9,056/m²". Whole units from 100 up, cents below ("$4.90/kg").
amount*numberThe total price.
quantity*numberHow many units it covers.
unit*stringThe unit after the slash: m², kg, sq ft.
optionsFormatPriceOptionsThe same options as formatPrice.
roundMoney
functionRounds half away from zero without float noise: roundMoney(1.005) is 1.01. Every amount the lib returns goes through it.
couponDiscount
functionWhat one coupon takes off, or why it does not apply: { valid: true, amount }, { valid: false, reason: "min-subtotal", missing } or { valid: false, reason: "not-applicable" }. Checking that a code exists and has not expired is the server's job; this is the math.
computeCartTotals
functionThe summary of a cart or checkout: item count, subtotal, savings from sale prices, coupon discounts and rejected coupons, shipping with the free-shipping progress, VAT and total. Every amount is rounded to cents, so the lines always add up to the total.
lines*CartLineInput[]{ id, price, quantity, compareAtPrice?, taxRate? } per line.
options.couponsdefault"[]"Coupon[]Applied in order, each on what the ones before it left.
options.shippingdefault"null"number | nullThe delivery price; null means not chosen yet.
options.freeShippingThresholddefault"null"number | nullSpend this much on items (after coupons) and delivery is free.
options.taxRatedefault0numberThe VAT rate as a fraction (0.21); 0 leaves out the VAT line.
options.pricesIncludeTaxdefaulttruebooleantrue reports the VAT inside the total; false adds it on top.
returnsCartTotalsitemCount, subtotal, savings, discount, discounts, rejectedCoupons, shipping, freeShippingRemaining, freeShippingProgress, tax, total and more.
DEFAULT_CURRENCY
constantThe currency every formatter uses when options.currency is left out: "USD".
RO_VAT_RATE
constantRomania's standard VAT rate as a fraction: 0.21, in force since Aug 1, 2025.
RO_REDUCED_VAT_RATE
constantRomania's reduced VAT rate for food, medicines and books: 0.11.
Coupon
type{ code, type: "percent" | "fixed" | "shipping", value, minSubtotal?, maxDiscount?, label? }.
FormatPriceOptions
typecurrency, locale, fractionDigits, currencyDisplay, notation and signDisplay, shared by every formatter.
CartTotals
typeWhat computeCartTotals returns, every amount in major units (dollars, not cents).
Examples
Product card prices
A sale price, the old price struck through and the discount badge.
Prices by locale
The same amounts through formatPrice in four locales and in compact notation.
Coupon checks
couponDiscount explains a minimum order instead of failing silently.
Unit prices
formatUnitPrice for real estate and groceries.
Dependencies
Files
- lib/commerce.ts
FAQ
Is the Commerce helper free?
The preview, props and docs are free; copying the source needs a Pro plan. See plans.
How do I install Commerce?
Run npx wingo-ui@latest add commerce. It also adds the Wingo UI item it builds on (Number format). With shadcn, add the @wingo-ui registry with your API key, then run npx shadcn@latest add @wingo-ui/commerce.