Foreign Exchange Checker
Convert, compare and keep a record of the numbers.
A currency converter with rate history, saved pairs and a conversion log, built on the Frankfurter API with Redux Toolkit.
- React
- Redux Toolkit
- Frankfurter API
- Recharts
- Tailwind CSS
The premise
Foreign Exchange Checker converts an amount between currencies, shows rate history over six ranges, compares a base against a fixed candidate list, and keeps saved pairs and manual conversion snapshots in the browser. It consumes the Frankfurter API directly from the browser with no API key, and the design system and assets come from a Frontend Mentor challenge. The work here covers the state architecture, the request lifecycle, the anchor picker and how each view presents a missing or empty result.
State gallery
Compare the interface states documented in the source. The gallery labels its recreations and uses a captured conversion example rather than current rates. The original captures follow below.
Jump to a figure
- 01
Conversion result

Captured example: sending 1,000 USD displays 892.54 EUR. These are captured values, not current exchange rates. Read the related decision - 02
History view

USD/EUR history with the one-month range selected. The displayed statistics and plotted history are separate requests in the source. - 03
Currency comparison

Comparison list for an amount of 1,000 USD. A desktop screenshot notification obscures part of this capture; those obscured values are not interpreted. - 04
Empty conversion log

Empty log view beneath the converter. The captured session wording differs from the browser-local persistence in the repository. - 05
Saved currency pair

Favorites view with a saved USD to BRL pair. The picker and saved-pair behavior are separate interactions. Read the related decision
Decisions
Render the currency picker inside the panel that opens it
Both currency panels shared one picker controlled through onOpenPicker, rendered as a centred desktop overlay.
- Options
- Keep the single shared picker mounted at the page level.
- Move picker-open state into CurrencyPanel and render the picker inside the button's wrapper.
- Choice
- Each panel owns its picker, positioned over the control that opened it, with the fixed bottom sheet kept on mobile.
- Trade-off
- The picker now appears where the visitor clicked, and each mounted picker fetches the currency list, so the list is requested more than once.
Evidence
- Picker state moved into the panel
foreign-exchange-checker commit a88b805 - Positioned wrapper and mobile bottom sheet
foreign-exchange-checker:starter-code/src/components/CurrencyPanel.jsx - Seed record and specificity verdict
docs/truth/foreign-exchange-checker-decisions.md, seed 4; docs/seed-specificity-review.md
Format the receive amount instead of the input
Typing a thousand in the source field needs a readable separator without fighting the caret.
- Options
- Format the amount the visitor types, re-writing the input as digits are entered.
- Leave the input raw and format only the computed receive amount shown beside it.
- Choice
- The input stays as typed, and the receive amount is formatted with locale grouping before it is rendered.
- Trade-off
- The caret is never disturbed while typing, and the two sides of the converter are formatted differently: receive caps at two fraction digits while the input shows what was typed.
Evidence
- Receive amount formatting in the converter view
foreign-exchange-checker:starter-code/src/components/Convert.jsx - Contrasting formatting in compare and log views
docs/truth/foreign-exchange-checker.md, section 6 - Seed record and specificity verdict
docs/truth/foreign-exchange-checker-decisions.md, seed 5; docs/seed-specificity-review.md
Move currency state into a Redux slice
Currency data and request lifecycle lived in local component state, so each view held its own copy.
- Options
- Keep per-view local state and pass fetched results between components.
- Move currency state into a Redux Toolkit slice with thunks that carry request cancellation.
- Choice
- One slice owns currency state, the thunks and the cancellation, and selectors read it from each view.
- Trade-off
- Requests cancel cleanly and views share one source of truth, at the cost of a store, selectors and thunks to maintain around what was a local fetch.
Evidence
- Currency slice, thunks and selectors
foreign-exchange-checker:starter-code/src/features/currency/ - Cancellation handling in the thunks
foreign-exchange-checker:starter-code/src/features/currency/currencySlice.js - Seed record and specificity verdict
docs/truth/foreign-exchange-checker-decisions.md, seed 1; docs/seed-specificity-review.md
The tools
- React
- Component model for the convert, history, compare, favorites and log views.
- Redux Toolkit
- A currency slice with thunks that carry request cancellation, plus selectors for each view.
- Frankfurter API
- Browser-side endpoints for conversion, history and currencies, called without an API key.
- Recharts
- Area chart rendering for the selected history range.
- Tailwind CSS
- Utility styling built on the challenge's supplied design tokens.
Problems and fixes
One picker served two panels from the middle of the screen
Both currency panels shared one picker controlled through onOpenPicker, rendered as a centred desktop overlay, so the control that opened the list and the control the visitor then used sat in different places. Commit a88b805 moves picker-open state into CurrencyPanel and renders the picker inside the button's positioned wrapper, keeping the fixed bottom sheet on mobile. Each mounted picker now fetches the currency list, so the list is requested more than once.
The deployed page ships the starter template's title
starter-code/index.html sets the document title to starter-code, the Frontend Mentor starter folder name, so the live deployment's browser tab and link previews read as the template rather than the product. The string matches the file exactly, which also confirms the deployment builds from this repository. The fix is one line, and it has not been applied.
Saved snapshots dropped the provider's rate date
Log entries are user-triggered snapshots of the converter fields with a local timestamp and a generated id, and they do not preserve the provider's rate date. The local-storage hook catches neither invalid JSON nor storage exceptions, and saved pairs and entries live under fx-favorites and fx-log in the browser origin, so persistence is tied to one browser and one origin with no export.
What I would change
What I would change first is the document title, then make formatting consistent: receive amounts cap at two fraction digits while compare and log use default locale settings, and no currency-specific minor-unit policy exists. I would give the local-storage hook parsing and exception recovery, store the rate date with each snapshot, and reconcile the log's session copy with what local storage actually persists. The picker also needs dialog semantics and focus restoration, and the view buttons need selection state for assistive technology. Each of these was read from the source rather than assumed.