Currency converter app in React and Mlyn
Tags: javascript, react, mlyn, webdev
Published: 2021-12-14 14:11:21

👋 Let build a currency converter app:
![App example](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ytbazym8urus3sy1mgrr.gif) 
The application should allow to edit the amount in the input fields and change currency. The amount in another input should change in the base of the conversion rate.
For a working example see [this codesandbox](https://codesandbox.io/s/mlyn-currency-converter-t5w0o?file=/src/App.js) (It also contains an advanced example).

First of all, we need to define our data domain. We need to take a currency as a reference point, let use USD:
```ts
// USD to currency price
const usdRates = {
  USD: 1,
  BYN: 2.5,
  CAD: 1.260046,
  CHF: 0.933058,
  EUR: 0.806942,
  GBP: 0.719154
};

// list of currency names
const availableCurrencies = Object.keys(usdRates);
```
Now we can setup the root state:
```ts
export default function App() {
  const state$ = useSubject({
    // entered amount expressed in USD
    baseAmount: 0,
    // list of currently compared currencies
    currencies: ["USD", "EUR"]
  });

  return (/* jsx here */);
}

```
Yeah, that's all boilerplate we need. And finally some JSX:
```ts
<div className="App">
  <Currency
    amount$={state$.baseAmount}
    currency$={state$.currencies[0]}
  />
  <Currency
    amount$={state$.baseAmount}
    currency$={state$.currencies[1]}
  />
</div>
```
Operation `state$.baseAmount` created a read/write lens to `baseAmount` property. Calling `state$.baseAmount()` will return its current value and `state$.baseAmount(1)` will change the `baseAmount` value. The update will bubble to the root state, cause encapsulated object is immutable. Also, you can subscribe to this value. This enables 2-way binding. 
Same thing for `state$.currencies[0]`, it will read/write the first element of the `currency` array.
Now let write an incomplete version of the `Currency` component.
```ts
const Currency = seal(({ amount$, currency$ }) => {
  return (
    <div>
      <Mlyn.select bindValue={currency$}>
        {availableCurrencies.map((c) => (
          <option key={c}>{c}</option>
        ))}
      </Mlyn.select>
      {/* text input code here */}
    </div>
  );
});
```
`Mlyn.select` is a wrapper over the plain `select` element, it has a property `bindValue` which accepts a read/write value, and creates a 2-way binding to it. Internally `Mlyn.select` will observe `currency$` value, and re-render when it's changed. When a selector option will be selected `currency$` (and hence the root state) will be updated. 
To write the input we can't just bind `amount$` to it, cause we need to display the derived value of the currency:
```ts
// will not give the expected result,
// cause USD amount will be displayed
<Mlyn.input bindValue={amount$} />
```
Ok. This will be the hardest part.
One of good things of 2-way binding, is that you can wrap binded value within a function, that will perform read/write derivation logic. So let create a function that will convert amount in a currency to/from USD amount:
```ts
// function that will curry references to `baseAmount$`
// and `currency$` subjects
const convertCurrencyAmount = (baseAmount$, currency$) =>
  // returns function to use as 2-way bindable value
  (...args) => {
    // if function has been invoked with params
    // checks if it is a write operation
    if (args.length > 0) {
      const newAmount = parseFloat(args[0]);
      // writes new value to the subject 
      baseAmount$(newAmount / ratesToUSD[currency$()]);
    } else {
      // it is a a read operation, return converted value
      // note that this code will create subscription and
      // routing will rerun whenever baseAmount$ or currency$
      // values will changed 
      return baseAmount$() * ratesToUSD[currency$()];
    }
  };
```
The above function is a simplified version, in reality we should do some input validation:
```ts
const convertCurrencyAmount = (baseAmount$, currency$) =>
  (...args) => {
    if (args.length > 0) {
      // if user erases all text make value 0.
      const value = args[0] === "" ? 0 : parseFloat(args[0]);
      // skip non-numeric updates
      if (!isNaN(value)) {
        baseAmount$(value / usdRates[currency$()]);
      }
    } else {
      const newAmount = baseAmount$() * usdRates[currency$()];
      // avoid very long numbers like 0.999999999
      return Math.round(newAmount * 100) / 100;
    }
  };
```
Now you can use pass the converted currency lens to the amount input:
```ts
<Mlyn.input
  bindValue={convertCurrencyAmount(baseAmount$, currency$)}
/>
```
For more examples and docs about mlyn, I invite you to check the [github repo page](https://github.com/vaukalak/react-mlyn).