Two-way currency conversion keeps two amount fields synchronized through an exchange rate. Build it as a single React component with US dollars on top, euros below, and a fixed rate of 1 USD = 0.92 EUR. Typing in either field recomputes the other without creating an update loop.
Implement the default App component in App.tsx. The starter renders both inputs with 100 USD and 92 EUR but leaves their state and edit handlers incomplete:
usd and eur as two separate useState strings.eur = usd * 0.92; typing in EUR sets usd = eur / 0.92. Round the displayed counterpart to 2 decimals.onChange updates the field you typed in and its counterpart. Do not add one effect that mirrors USD to EUR and another that mirrors EUR to USD.50 in USD, and EUR shows 46.00.92 in EUR, and USD shows 100.00.onChange, write both usd and the derived eur. Do not set up one effect that watches usd and writes eur plus another that watches eur and writes usd — that is the loop.usd, eur), so the cursor never jumps and only the edited field drives the other.toFixed(2) on the counterpart you write; keep the field you are typing in exactly as typed.styles.css; focus on the state and handlers.Two linked inputs stay stable when the handler for the edited field owns both writes: it preserves the typed text and computes the counterpart.
A currency converter shows the same money in two units. Change the dollars and the euros follow; change the euros and the dollars follow. The trap is that "follow" runs both ways, so a careless wiring makes field A update field B, which updates field A, which updates B… forever. We need a genuine two-way binding that never chases its own tail.
Think of it as two one-way streets, not one two-way street. When the user edits USD, onUsd stores that exact text and computes eur = usd * rate. When the user edits EUR, onEur stores that text and computes usd = eur / rate. No effect watches the computed field and writes back, so there is no cycle.
The tempting move is to hold one value and mirror it with two effects:
const [usd, setUsd] = useState('100');
const [eur, setEur] = useState('92');
useEffect(() => { setEur((Number(usd) * 0.92).toFixed(2)); }, [usd]);
useEffect(() => { setUsd((Number(eur) / 0.92).toFixed(2)); }, [eur]);
Editing USD runs the first effect and sets eur. But setting eur re-runs the second effect, which sets usd, which re-runs the first… The two effects keep firing off each other — an update loop (and the cursor fights you the whole way). The fix is to stop treating "the other field changed" as a trigger.
import { useState } from 'react';
import './styles.css';
const RATE = 0.92;
function convert(value: string, factor: number): string {
if (value.trim() === '') return '';
const n = Number(value);
if (!Number.isFinite(n)) return '';
return (n * factor).toFixed(2); // round the displayed counterpart
}
export default function App() {
const [usd, setUsd] = useState('100');
const [eur, setEur] = useState('92');
function onUsd(e: React.ChangeEvent<HTMLInputElement>) {
const next = e.target.value;
setUsd(next); // the field being typed in
setEur(convert(next, RATE)); // write only the counterpart
}
function onEur(e: React.ChangeEvent<HTMLInputElement>) {
const next = e.target.value;
setEur(next);
setUsd(convert(next, 1 / RATE));
}
return (
<main className="container">
<h1>Currency Converter</h1>
<label className="row">
<span className="cur">USD</span>
<input className="input" type="number" value={usd} onChange={onUsd} />
</label>
<label className="row">
<span className="cur">EUR</span>
<input className="input" type="number" value={eur} onChange={onEur} />
</label>
<p className="rate">1 USD = 0.92 EUR</p>
</main>
);
}
The loop is gone because the counterpart is computed inside the handler of the field being edited, not by an effect that watches the other field. setEur in onUsd updates euros, but nothing is subscribed to eur to write back, so the chain ends. Each input is a controlled component bound to its own state.
usd = '100', eur = '92'. Both inputs show their values, label reads the rate.50 in USD: onUsd runs, setUsd('50'), then setEur(convert('50', 0.92)) = setEur('46.00'). EUR now reads 46.00. No effect re-fires.next is '', convert returns '', so setEur('') blanks EUR too.useEffect([usd]) writing eur and useEffect([eur]) writing usd bounce off each other forever. Compute the counterpart in the edit handler instead.toFixed(2) only to the counterpart; formatting the active field mid-type moves the cursor and blocks typing 1. or 0.5.0 — Number('') is 0, not NaN, so a bare Number check would fill the other field with 0.00 on clear. Guard the empty string explicitly and return ''.<select> per row and recompute through the chosen pair's rate.Intl.NumberFormat for grouping and currency symbols, formatting only on blur so typing stays raw.This version models an edit as a currency and a raw string. One reducer transition preserves the edited amount and computes its partner, so both values commit together without effects.
import { useReducer } from 'react';
import './styles.css';
type Currency = 'usd' | 'eur';
type Amounts = { usd: string; eur: string };
type Edit = { currency: Currency; value: string };
const RATE = 0.92;
function converted(value: string, factor: number): string {
if (value.trim() === '') return '';
const amount = Number(value);
return Number.isFinite(amount) ? (amount * factor).toFixed(2) : '';
}
function applyEdit(_amounts: Amounts, edit: Edit): Amounts {
if (edit.currency === 'usd') {
return { usd: edit.value, eur: converted(edit.value, RATE) };
}
return { eur: edit.value, usd: converted(edit.value, 1 / RATE) };
}
export default function App() {
const [amounts, edit] = useReducer(applyEdit, { usd: '100', eur: '92' });
return (
<main className="container">
<h1>Currency Converter</h1>
<label className="row">
<span className="cur">USD</span>
<input
className="input"
type="number"
value={amounts.usd}
onChange={(event) => edit({ currency: 'usd', value: event.target.value })}
/>
</label>
<label className="row">
<span className="cur">EUR</span>
<input
className="input"
type="number"
value={amounts.eur}
onChange={(event) => edit({ currency: 'eur', value: event.target.value })}
/>
</label>
<p className="rate">1 USD = 0.92 EUR</p>
</main>
);
}Keep practising the same patterns with a nearby challenge.
No submissions yet
Share your approach and start the discussion.
Two-way currency conversion keeps two amount fields synchronized through an exchange rate. Build it as a single React component with US dollars on top, euros below, and a fixed rate of 1 USD = 0.92 EUR. Typing in either field recomputes the other without creating an update loop.
Implement the default App component in App.tsx. The starter renders both inputs with 100 USD and 92 EUR but leaves their state and edit handlers incomplete:
usd and eur as two separate useState strings.eur = usd * 0.92; typing in EUR sets usd = eur / 0.92. Round the displayed counterpart to 2 decimals.onChange updates the field you typed in and its counterpart. Do not add one effect that mirrors USD to EUR and another that mirrors EUR to USD.50 in USD, and EUR shows 46.00.92 in EUR, and USD shows 100.00.onChange, write both usd and the derived eur. Do not set up one effect that watches usd and writes eur plus another that watches eur and writes usd — that is the loop.usd, eur), so the cursor never jumps and only the edited field drives the other.toFixed(2) on the counterpart you write; keep the field you are typing in exactly as typed.styles.css; focus on the state and handlers.Two linked inputs stay stable when the handler for the edited field owns both writes: it preserves the typed text and computes the counterpart.
A currency converter shows the same money in two units. Change the dollars and the euros follow; change the euros and the dollars follow. The trap is that "follow" runs both ways, so a careless wiring makes field A update field B, which updates field A, which updates B… forever. We need a genuine two-way binding that never chases its own tail.
Think of it as two one-way streets, not one two-way street. When the user edits USD, onUsd stores that exact text and computes eur = usd * rate. When the user edits EUR, onEur stores that text and computes usd = eur / rate. No effect watches the computed field and writes back, so there is no cycle.
The tempting move is to hold one value and mirror it with two effects:
const [usd, setUsd] = useState('100');
const [eur, setEur] = useState('92');
useEffect(() => { setEur((Number(usd) * 0.92).toFixed(2)); }, [usd]);
useEffect(() => { setUsd((Number(eur) / 0.92).toFixed(2)); }, [eur]);
Editing USD runs the first effect and sets eur. But setting eur re-runs the second effect, which sets usd, which re-runs the first… The two effects keep firing off each other — an update loop (and the cursor fights you the whole way). The fix is to stop treating "the other field changed" as a trigger.
import { useState } from 'react';
import './styles.css';
const RATE = 0.92;
function convert(value: string, factor: number): string {
if (value.trim() === '') return '';
const n = Number(value);
if (!Number.isFinite(n)) return '';
return (n * factor).toFixed(2); // round the displayed counterpart
}
export default function App() {
const [usd, setUsd] = useState('100');
const [eur, setEur] = useState('92');
function onUsd(e: React.ChangeEvent<HTMLInputElement>) {
const next = e.target.value;
setUsd(next); // the field being typed in
setEur(convert(next, RATE)); // write only the counterpart
}
function onEur(e: React.ChangeEvent<HTMLInputElement>) {
const next = e.target.value;
setEur(next);
setUsd(convert(next, 1 / RATE));
}
return (
<main className="container">
<h1>Currency Converter</h1>
<label className="row">
<span className="cur">USD</span>
<input className="input" type="number" value={usd} onChange={onUsd} />
</label>
<label className="row">
<span className="cur">EUR</span>
<input className="input" type="number" value={eur} onChange={onEur} />
</label>
<p className="rate">1 USD = 0.92 EUR</p>
</main>
);
}
The loop is gone because the counterpart is computed inside the handler of the field being edited, not by an effect that watches the other field. setEur in onUsd updates euros, but nothing is subscribed to eur to write back, so the chain ends. Each input is a controlled component bound to its own state.
usd = '100', eur = '92'. Both inputs show their values, label reads the rate.50 in USD: onUsd runs, setUsd('50'), then setEur(convert('50', 0.92)) = setEur('46.00'). EUR now reads 46.00. No effect re-fires.next is '', convert returns '', so setEur('') blanks EUR too.useEffect([usd]) writing eur and useEffect([eur]) writing usd bounce off each other forever. Compute the counterpart in the edit handler instead.toFixed(2) only to the counterpart; formatting the active field mid-type moves the cursor and blocks typing 1. or 0.5.0 — Number('') is 0, not NaN, so a bare Number check would fill the other field with 0.00 on clear. Guard the empty string explicitly and return ''.<select> per row and recompute through the chosen pair's rate.Intl.NumberFormat for grouping and currency symbols, formatting only on blur so typing stays raw.This version models an edit as a currency and a raw string. One reducer transition preserves the edited amount and computes its partner, so both values commit together without effects.
import { useReducer } from 'react';
import './styles.css';
type Currency = 'usd' | 'eur';
type Amounts = { usd: string; eur: string };
type Edit = { currency: Currency; value: string };
const RATE = 0.92;
function converted(value: string, factor: number): string {
if (value.trim() === '') return '';
const amount = Number(value);
return Number.isFinite(amount) ? (amount * factor).toFixed(2) : '';
}
function applyEdit(_amounts: Amounts, edit: Edit): Amounts {
if (edit.currency === 'usd') {
return { usd: edit.value, eur: converted(edit.value, RATE) };
}
return { eur: edit.value, usd: converted(edit.value, 1 / RATE) };
}
export default function App() {
const [amounts, edit] = useReducer(applyEdit, { usd: '100', eur: '92' });
return (
<main className="container">
<h1>Currency Converter</h1>
<label className="row">
<span className="cur">USD</span>
<input
className="input"
type="number"
value={amounts.usd}
onChange={(event) => edit({ currency: 'usd', value: event.target.value })}
/>
</label>
<label className="row">
<span className="cur">EUR</span>
<input
className="input"
type="number"
value={amounts.eur}
onChange={(event) => edit({ currency: 'eur', value: event.target.value })}
/>
</label>
<p className="rate">1 USD = 0.92 EUR</p>
</main>
);
}Keep practising the same patterns with a nearby challenge.
No submissions yet
Share your approach and start the discussion.