With the latest version of the Ionic framework, a tool that lets you write mobile apps with web technologies, you can use React to build your mobile application. Before v4, Ionic only provided components for Angular, but now React has complete support for the components.
Ionic 4 is a mobile app framework and a component library. You can build mobile apps, progressive web apps, and normal web apps. The component library can also be used on its own. Of course, the hardware support remains in place for you to use if you wish.
The full reference for the React version of Ionic is at https://ionicframework.com/docs.
In this article, we will build a React web app with Ionic that does currency conversion. The home page will display the list of the latest exchange rates and another page will have a form to convert the currencies of your choice. To get the data, we use the Foreign exchange rates API located at https://exchangeratesapi.io/. We use the Open Exchange Rates API located at https://openexchangerates.org/ to get the list of currencies.
Getting Started
To start, we will install the Ionic CLI. We run:
npm install -g ionic@latest
Next, we run:
ionic start currency-converter --type=react
Then we select the Sidenav option to create an Ionic project with a left sidebar.
We will also need to install some other packages. We need Axios to make HTTP requests and MobX for state management. Run npm i axios mobx mobx-react
in our project folder to install them.
Now we are ready to create some pages. In the pages
folder, create ConvertCurrencyPage.jsx
and add:
import {
IonButtons,
IonContent,
IonHeader,
IonItem,
IonList,
IonMenuButton,
IonPage,
IonTitle,
IonToolbar,
IonInput,
IonLabel,
IonSelect,
IonSelectOption,
IonButton
} from "@ionic/react";
import React from "react";
import { observer } from "mobx-react";
import { getExchangeRates } from "../requests";
const ConvertCurrencyPage = ({ currenciesStore }) => {
const [fromCurrencies, setFromCurrencies] = React.useState({});
const [toCurrencies, setToCurrencies] = React.useState({});
const [values, setValues] = React.useState({ amount: 0 } as any);
const [submitting, setSubmitting] = React.useState(false);
const [toAmount, setToAmount] = React.useState(0);
const convertCurrency = async () => {
setSubmitting(true);
if (values.amount <= 0 || !values.from || !values.to) {
return;
}
const { data } = await getExchangeRates(values.from);
const rate = data.rates[values.to];
setToAmount(values.amount * rate);
};
React.useEffect(() => {
const fromCurrencies = {};
for (let key in currenciesStore.currencies) {
if (key != values.to) {
fromCurrencies[key] = currenciesStore.currencies[key];
}
}
setFromCurrencies(fromCurrencies);
const toCurrencies = {};
for (let key in currenciesStore.currencies) {
if (key != values.from) {
toCurrencies[key] = currenciesStore.currencies[key];
}
}
setToCurrencies(toCurrencies);
}, [currenciesStore.currencies, values.from, values.to]);
return (
<IonPage>
<IonHeader>
<IonToolbar>
<IonButtons slot="start">
<IonMenuButton />
</IonButtons>
<IonTitle>Convert Currency</IonTitle>
</IonToolbar>
</IonHeader>
<IonContent>
<IonList lines="none">
<IonItem>
<IonInput
type="number"
value={values.amount}
color={!values.amount && submitting ? "danger" : undefined}
min="0"
onIonChange={ev =>
setValues({ ...values, amount: (ev.target as any).value })
}
></IonInput>
</IonItem>
<IonItem>
<IonLabel>Currency to Convert From</IonLabel>
<IonSelect
placeholder="Select One"
color={!values.from && submitting ? "danger" : undefined}
onIonChange={ev =>
setValues({ ...values, from: (ev.target as any).value })
}
>
{Object.keys(fromCurrencies).map(key => {
return (
<IonSelectOption value={key} key={key}>
{(fromCurrencies as any)[key]}
</IonSelectOption>
);
})}
</IonSelect>
</IonItem>
<IonItem>
<IonLabel>Currency to Convert To</IonLabel>
<IonSelect
placeholder="Select One"
color={!values.to && submitting ? "danger" : undefined}
onIonChange={ev =>
setValues({ ...values, to: (ev.target as any).value })
}
>
{Object.keys(toCurrencies).map(key => {
return (
<IonSelectOption value={key} key={key}>
{(toCurrencies as any)[key]}
</IonSelectOption>
);
})}
</IonSelect>
</IonItem>
<IonItem>
<IonButton size="default" fill="solid" onClick={convertCurrency}>
Convert
</IonButton>
</IonItem>
{toAmount ? (
<IonItem>
{values.amount} {values.from} is {toAmount} {values.to}.
</IonItem>
) : (
undefined
)}
</IonList>
</IonContent>
</IonPage>
);
};
export default observer(ConvertCurrencyPage);
This adds a form to convert currency from one to another. In this form, we filter by choices by excluding from the currency being converted to from the first drop down and exclude the currency being converted from in the second dropdown. Also, we have an ion-input
for the amount being converted. We get the values of the currency from the currenciesStore
, which is the MobX store that gets the list of currencies from. In the IonSelect
components we set the onIonChange
props to the handler functions that set the drop down’s values. We also set the placeholder for all the inputs and selects. In the IonInput
component, we do the same with the onIonChange
handler. We show the correct value by using the variables set in the values
object during change events.
When the user clicks Convert, we run the convertCurrency
function. We check if the values are set correctly before running the rest of the code. If that succeeds, then we run the getExchangeRates
function imported from requests.js
, then we set the final toAmount
by multiplying the rate with the amount.
The useEffect
callback is used for excluding the currency to convert from the currency to convert to list and vice versa. The array in the second argument useEffect
specifies which values to watch for.
The observer
function in the last line is for designating the component to watch the latest values from the MobX stores.
Next in Home.tsx
, we replace the existing code with:
import {
IonButtons,
IonContent,
IonHeader,
IonItem,
IonLabel,
IonList,
IonListHeader,
IonMenuButton,
IonPage,
IonTitle,
IonToolbar,
IonSelect,
IonSelectOption
} from "@ionic/react";
import React from "react";
import "./Home.css";
import { getExchangeRates } from "../requests";
import { CurrencyStore } from "../stores";
import { observer } from "mobx-react";
const HomePage = ({ currencyStore, currenciesStore }) => {
const [rates, setRates] = React.useState({});
const getRates = async () => {
const { data } = await getExchangeRates(
(currencyStore as CurrencyStore).currency
);
setRates(data.rates);
};
React.useEffect(() => {
getRates();
}, [(currencyStore as CurrencyStore).currency]);
return (
<IonPage>
<IonHeader>
<IonToolbar>
<IonButtons slot="start">
<IonMenuButton />
</IonButtons>
<IonTitle>Home</IonTitle>
</IonToolbar>
</IonHeader>
<IonContent>
<IonList lines="none">
<IonListHeader>Latest Exchange Rates</IonListHeader>
<IonItem>
<IonLabel>Currency</IonLabel>
<IonSelect
placeholder="Select One"
onIonChange={ev => {
(currencyStore as CurrencyStore).setCurrency(
ev.target && (ev.target as any).value
);
}}
>
{Object.keys(currenciesStore.currencies).map(key => {
return (
<IonSelectOption
value={key}
key={key}
selected={
(currencyStore as CurrencyStore).currency
? key == (currencyStore as CurrencyStore).currency
: key == "AUD"
}
>
{(currenciesStore.currencies as any)[key]}
</IonSelectOption>
);
})}
</IonSelect>
</IonItem>
</IonList>
<IonList lines="none">
<IonListHeader>Exchange Rates</IonListHeader>
{Object.keys(rates).map(key => {
console.log(rates);
return (
<IonItem>
{key}: {rates[key]}
</IonItem>
);
})}
</IonList>
</IonContent>
</IonPage>
);
};
export default observer(HomePage);
In this file, we display the exchange rates from an API. We get the currencies from the currenciesStore
so that users can see exchange rates based on different currencies. The items are displayed in an IonList
provided by Ionic.
The observer
function in the last line is for designating the component to watches the latest values from the MobX stores.
Next in App.tsx
, replace the following code with:
import React from "react";
import { Redirect, Route } from "react-router-dom";
import { IonApp, IonRouterOutlet, IonSplitPane } from "@ionic/react";
import { IonReactRouter } from "@ionic/react-router";
import { AppPage } from "./declarations";
import Menu from "./components/Menu";
import Home from "./pages/Home";
import { home, cash } from "ionicons/icons";
/* Core CSS required for Ionic components to work properly */
import "@ionic/react/css/core.css";
/* Basic CSS for apps built with Ionic */
import "@ionic/react/css/normalize.css";
import "@ionic/react/css/structure.css";
import "@ionic/react/css/typography.css";
/* Optional CSS utils that can be commented out */
import "@ionic/react/css/padding.css";
import "@ionic/react/css/float-elements.css";
import "@ionic/react/css/text-alignment.css";
import "@ionic/react/css/text-transformation.css";
import "@ionic/react/css/flex-utils.css";
import "@ionic/react/css/display.css";
/* Theme variables */
import "./theme/variables.css";
import ConvertCurrencyPage from "./pages/ConvertCurrencyPage";
import { CurrencyStore, CurrenciesStore } from "./stores";
import { getCurrenciesList } from "./requests";
const currencyStore = new CurrencyStore();
const currenciesStore = new CurrenciesStore();
const appPages: AppPage[] = [
{
title: "Home",
url: "/home",
icon: home
},
{
title: "Convert Currency",
url: "/convertcurrency",
icon: cash
}
];
const App: React.FC = () => {
const [initialized, setInitialized] = React.useState(false);
const getCurrencies = async () => {
const { data } = await getCurrenciesList();
currenciesStore.setCurrencies(data);
setInitialized(true);
};
React.useEffect(() => {
if (!initialized) {
getCurrencies();
}
});
return (
<IonApp>
<IonReactRouter>
<IonSplitPane contentId="main">
<Menu appPages={appPages} />
<IonRouterOutlet id="main">
<Route
path="/home"
render={() => (
<Home
currencyStore={currencyStore}
currenciesStore={currenciesStore}
/>
)}
exact={true}
/>
<Route
path="/convertcurrency"
render={() => (
<ConvertCurrencyPage
currenciesStore={currenciesStore}
/>
)}
exact={true}
/>
<Route exact path="/" render={() => <Redirect to="/home" />} />
</IonRouterOutlet>
</IonSplitPane>
</IonReactRouter>
</IonApp>
);
};
export default App;
We modified the routes so that we use the render
prop instead of the component
prop since we want to pass in our MobX stores into the components. The stores contain the currently selected currency for the home page and the list of currencies for both pages.
We get/set the list of currencies here for both components.
Next, create requests.ts
in the src
folder and add:
const axios = require('axios');
const APIURL = 'https://api.exchangeratesapi.io';
const OPEN_EXCHANGE_RATES_URL = 'http://openexchangerates.org/api/currencies.json';
export const getExchangeRates = (baseCurrency: string) => axios.get(`${APIURL}/latest?base=${baseCurrency}`)
export const getCurrenciesList = () => axios.get(OPEN_EXCHANGE_RATES_URL)
This the code for making the HTTP requests to get the currencies and exchange rates.
Next create store.ts
and add:
import { observable, action } from "mobx";
class CurrencyStore {
@observable currency: string = 'AUD';
@action setCurrency(currency: string) {
this.currency = currency;
}
}
class CurrenciesStore {
@observable currencies = {};
@action setCurrencies(currencies) {
this.currencies = currencies;
}
}
export { CurrencyStore, CurrenciesStore };
The CurrencyStore
is for storing the selected currency in the home page and to show the exchange rates based on the selected currency. currency
is the value observed by the home page and setCurrency
sets the currency value. Similarly, CurrenciesStore
stores a list of currencies retrieved in App.tsx
where setCurrencies
is called.
Next in tsconfig.json
, we replace the existing code with:
{
"compilerOptions": {
"experimentalDecorators": true,
"target": "es5",
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "preserve",
"noImplicitAny": false
},
"include": [
"src"
]
}
This changes noImplicitAny
and sets it to true
.