Building a mobile app with React and Ionic
Tags: react, reactnative, ionic
Published: 2022-12-28 23:51:21

by [Miracle Onyenma](https://blog.openreplay.com/authors/miracle-onyenma)

In this article, we will cover the basics of React and the Ionic framework. 
We will learn how to build a simple application with React and Ionic. We will cover the application structure by explaining how React and  Ionic applications are laid out and organized.


## What is Ionic?

According to its documentation, Ionic is an open-source UI toolkit for building performant, high-quality mobile apps using web technologies — HTML, CSS, and JavaScript — with integrations for popular frameworks like Angular, React, and Vue. Ionic allows developers to build apps for all major app stores and the mobile web, all while keeping the UI consistent with the device OS using [Adaptive Styling](https://ionicframework.com/docs/theming/platform-styles).

Previously, Ionic was tightly coupled with and built mainly for Angular. Still, from version 4, it has been re-engineered to work as a standalone web component library. With integrations for the latest JavaScript frameworks, current versions can be used in most JavaScript, like React, as we'll look for in this article.

### Ionic with React
Ionic officially supports React, and with `@ionic/react`, developers can use their current web development skills to create iOS, Android, and web apps. This allows devs to use all the Ionic core components, but in a way that feels like using native React components.

### Ionic CLI
Ionic provides an official CLI tool that quickly scaffolds Ionic apps and provides a number of helpful commands. It is also used for installing and updating Ionic. The CLI also comes with a built-in development server, and build and debugging tools.

Now that we have an idea of what Ionic is all about, let's dive into building an Ionic application.



## Set up the Ionic project

The Ionic CLI provides various development tools and support choices and is the recommended installation approach.

### Install the Ionic CLI
We'll install the Ionic CLI  globally with npm, well also install `native-run`, used to run native binaries on devices and simulators/emulators, and `cordova-res`, used to generate native app icons and splash screens:

```
npm install -g @ionic/cli native-run cordova-res
```


### Create an Ionic React app
To create an Ionic React app that uses the "Tabs" starter template and adds Capacitor for native functionality, we'll run the following command:


```
ionic start ionic-react-app tabs --type=react --capacitor
```


Once the installation is complete, we can navigate to and start our starter template:


```
cd ionic-react-app/
ionic serve
```


Now we should see something like this when we go to `http://http://localhost:8100/` in our browser. We should see this:


![1](https://blog.openreplay.com/images/bulding-a-mobile-app-with-react-and-ionic/images/image01.png)


Awesome!
Since the starter project comes complete with three pre-built pages and best practices for Ionic development, we can easily add more features as we start working on the app.



## Theming the app

Ionic is built to be very customizable, and there are multiple ways we can customize colors and styling to suit our needs.
First, we'll look at how to customize the app's color.

### Customizing colors
Numerous Ionic components can change their colors using one of the nine default colors. Each color in Ionic is essentially a collection of various properties, including a `shade` and `tint`.

To successfully change or add color, all the related properties, including `shade` and `tint`, must be added to the list of properties for that color.
To make this easier, Ionic provides a Color Generator tool that makes this simple to accomplish, but if preferred, these can also be manually generated.

To generate a color theme, go to [Ionic color generator](https://ionicframework.com/docs/theming/color-generator) and customize the colors:


![2](https://blog.openreplay.com/images/bulding-a-mobile-app-with-react-and-ionic/images/image02.png)


Once satisfied with the customization, we can copy out the generated code and place it in the `./src/theme/variables.css`:

```css
/* ./src/theme/variables.css */
/* Ionic Variables and Theming. For more info, please see:
http://ionicframework.com/docs/theming/ */
/** Ionic CSS Variables **/
:root {
  /** primary **/
  --ion-color-primary: #04724f;
  --ion-color-primary-rgb: 4, 114, 79;
  --ion-color-primary-contrast: #ffffff;
  --ion-color-primary-contrast-rgb: 255, 255, 255;
  --ion-color-primary-shade: #046446;
  --ion-color-primary-tint: #1d8061;
  /** secondary **/
  --ion-color-secondary: #0f9b9c;
  --ion-color-secondary-rgb: 15, 155, 156;
  --ion-color-secondary-contrast: #ffffff;
  --ion-color-secondary-contrast-rgb: 255, 255, 255;
  --ion-color-secondary-shade: #0d8889;
  --ion-color-secondary-tint: #27a5a6;
  /** tertiary **/
  --ion-color-tertiary: #ffb129;
  --ion-color-tertiary-rgb: 255, 177, 41;
  --ion-color-tertiary-contrast: #000000;
  --ion-color-tertiary-contrast-rgb: 0, 0, 0;
  --ion-color-tertiary-shade: #e09c24;
  --ion-color-tertiary-tint: #ffb93e;
  /** success **/
  --ion-color-success: #2dd36f;
  --ion-color-success-rgb: 45, 211, 111;
  --ion-color-success-contrast: #ffffff;
  --ion-color-success-contrast-rgb: 255, 255, 255;
  --ion-color-success-shade: #28ba62;
  --ion-color-success-tint: #42d77d;
  /** warning **/
  --ion-color-warning: #ffc409;
  --ion-color-warning-rgb: 255, 196, 9;
  --ion-color-warning-contrast: #000000;
  --ion-color-warning-contrast-rgb: 0, 0, 0;
  --ion-color-warning-shade: #e0ac08;
  --ion-color-warning-tint: #ffca22;
  /** danger **/
  --ion-color-danger: #d04155;
  --ion-color-danger-rgb: 208, 65, 85;
  --ion-color-danger-contrast: #ffffff;
  --ion-color-danger-contrast-rgb: 255, 255, 255;
  --ion-color-danger-shade: #b7394b;
  --ion-color-danger-tint: #d55466;
  /** dark **/
  --ion-color-dark: #222428;
  --ion-color-dark-rgb: 34, 36, 40;
  --ion-color-dark-contrast: #ffffff;
  --ion-color-dark-contrast-rgb: 255, 255, 255;
  --ion-color-dark-shade: #1e2023;
  --ion-color-dark-tint: #383a3e;
  /** medium **/
  --ion-color-medium: #2a4f48;
  --ion-color-medium-rgb: 42, 79, 72;
  --ion-color-medium-contrast: #ffffff;
  --ion-color-medium-contrast-rgb: 255, 255, 255;
  --ion-color-medium-shade: #25463f;
  --ion-color-medium-tint: #3f615a;
  /** light **/
  --ion-color-light: #7cda98;
  --ion-color-light-rgb: 124, 218, 152;
  --ion-color-light-contrast: #000000;
  --ion-color-light-contrast-rgb: 0, 0, 0;
  --ion-color-light-shade: #6dc086;
  --ion-color-light-tint: #89dea2;
}
```

The colors here apply to the default light mode. To apply the colors to the dark node as well, we have to replace the colors `primary`, `secondary`, `success`, `warning`, and `danger` in the `@media (prefers-color-scheme: dark)`  section. 
We'll leave out `dark`, `medium`, and `light` since they are inverses of the light theme colors.

Here's what we should have about now:


![3](https://blog.openreplay.com/images/bulding-a-mobile-app-with-react-and-ionic/images/image03.gif)


Nice. 

### Adding new colors
We can always add new colors in addition to the `--ion-color-primary` and others that are provided by default. 
First, let's generate a few colors using the [new color creator](https://ionicframework.com/docs/theming/colors#new-color-creator) tool:


![4](https://blog.openreplay.com/images/bulding-a-mobile-app-with-react-and-ionic/images/image04.png)


Next, define the CSS variables for all color variations at the `:root`.:


```css
:root {
  /* ... */
  --ion-color-custom: #347348;
  --ion-color-custom-rgb: 52,115,72;
  --ion-color-custom-contrast: #ffffff;
  --ion-color-custom-contrast-rgb: 255,255,255;
  --ion-color-custom-shade: #2e653f;
  --ion-color-custom-tint: #48815a;
}
```


Then, create a new class that uses these CSS variables:


```css
.ion-color-custom {
    --ion-color-base: var(--ion-color-custom);
    --ion-color-base-rgb: var(--ion-color-custom-rgb);
    --ion-color-contrast: var(--ion-color-custom-contrast);
    --ion-color-contrast-rgb: var(--ion-color-custom-contrast-rgb);
    --ion-color-shade: var(--ion-color-custom-shade);
    --ion-color-tint: var(--ion-color-custom-tint);
}
```


Now, we can use our class in any component that supports the `class` attribute. To illustrate this, in the `./src/components/ExploreContainer.tsx` file, we can add a button with the custom class:


```javascript
// ./src/components/ExploreContainer.tsx
import { IonButton } from "@ionic/react";
import "./ExploreContainer.css";
interface ContainerProps {
  name: string;
}
const ExploreContainer: React.FC<ContainerProps> = ({ name }) => {
  return (
    <div className="container">
      <strong>{name}</strong>
      <p>
        Explore{" "}
        <a
          target="_blank"
          rel= "noopener noreferrer"
          href="https://ionicframework.com/docs/components"
        >
          UI Components
        </a>
      </p>
      {/* button with custom class */}
      <IonButton color={"custom"}>custom button</IonButton>
    </div>
  );
};
export default ExploreContainer;
```


We should have something like this now:


![5](https://blog.openreplay.com/images/bulding-a-mobile-app-with-react-and-ionic/images/image05.png)


So far, we've covered what will be just the tip of the iceberg in theming Ionic apps in React, and more information, resources, and guides on themes can be found in [the Ionic docs](https://ionicframework.com/docs/theming/basics).

In the next section, we'll look at how we can fetch data and create components in Ionic React.


## Fetch data from REST API

Since we're using React in our Ionic application, there are multiple packages we can use to handle data fetching. Still, in this article, we'll take a more general approach and use the Fetch API.

We will create an image gallery app powered by the [Unsplash API](https://unsplash.com/documentation). To obtain API keys, we have to set up a developer account by following the steps in the [developers' docs](https://unsplash.com/documentation#creating-a-developer-account).
Once the developer account is set up, we can obtain API keys by creating a new app in the Unsplash dashboard.


![6](https://blog.openreplay.com/images/bulding-a-mobile-app-with-react-and-ionic/images/image06.png)


Back in our application, create a new file `./services/api.ts`:


```typescript
const ACCESS_KEY = "xxxxxxxxxxxxxxxxxxx";
const headerList = {
  "Content-Type": "application/json",
  Authorization: `Client-ID ${ACCESS_KEY}`,
};
export const getPhotos = async () => {
  try {
    const response = await fetch(`https://api.unsplash.com/photos/`, {
      headers: headerList,
    });
    const data = await response.json();
    return data;
  } catch (err) {
    return console.log(err);
  }
};
```

Here, we're exporting a function `getPhotos()` that fetches photos from the Unsplash API. Next, we'll create a PhotoCard component to display photos.

### Create PhotoCard component
Create a new file - `./src/components/PhotoCard.tsx`:


```javascript
// ./src/components/PhotoCard.tsx

import {
  IonButton,
  IonCard,
  IonCardContent,
  IonCardSubtitle,
  IonCol,
  IonIcon,
  IonRow,
  useIonToast,
} from "@ionic/react";
import {  heartOutline } from "ionicons/icons";

import { Photo } from "../types/Photo";
const PhotoCard = ({ photo }: { photo: Photo }) => {
  const { id, urls, description, user, likes } = photo;

  return (
    <IonCard>
      <img
        src={urls.regular}
        alt={description}
        style={{
          height: "24rem",
          width: "100%",
          objectFit: "cover",
        }}
      />
      <IonCardContent>
        <IonRow>
          <IonCol style={{ display: "flex", flexDirection: "column" }}>
            <IonCardSubtitle>@{user.username}</IonCardSubtitle>
            <IonCardSubtitle>
              {likes} like{likes > 1 && "s"}
            </IonCardSubtitle>
          </IonCol>
          <IonCol className="ion-text-right">
            <IonButton>
              <IonIcon icon={heartOutline} />
            </IonButton>
          </IonCol>
        </IonRow>
      </IonCardContent>
    </IonCard>
  );
};
export default PhotoCard;
```


Here, we're using the IonCard component to wrap other components like IonCardContent and others to make up the photo card.

Now, to get the photos that will be displayed using the component we just created, in our `./pages/Tab1.tsx` file, we can call our `getPhotos()` function and create a grid layout to display each photo card:

```javascript
// ./pages/Tab1.tsx

import {
  IonCol,
  IonContent,
  IonGrid,
  IonHeader,
  IonPage,
  IonRow,
  IonTitle,
  IonToolbar,
} from "@ionic/react";
import { useEffect, useState } from "react";
import PhotoCard from "../components/PhotoCard";
import { getPhotos } from "../services/api";
import { Photo } from "../types/Photo";
import "./Tab1.css";
const Tab1: React.FC = () => {
  const [photos, setPhotos] = useState<Photo[]>([]);
  const fetchPhotos = async () => {
    const photos = await getPhotos();
    console.log({ photos });
    setPhotos(photos);
  };
  useEffect(() => {
    fetchPhotos();
  }, []);
  return (
    <IonPage>
      <IonHeader>
        <IonToolbar>
          <IonTitle>Photos</IonTitle>
        </IonToolbar>
      </IonHeader>
      <IonContent fullscreen>
        <div className="gallery">
          <IonGrid className="photo-list">
            <IonRow>
              {photos.map((photo) => (
                <IonCol
                  sizeXs="12"
                  sizeMd="6"
                  sizeXl="4"
                  className="photo-list-item"
                  key={photo.id}
                >
                  <PhotoCard photo={photo} />
                </IonCol>
              ))}
            </IonRow>
          </IonGrid>
        </div>
      </IonContent>
    </IonPage>
  );
};
export default Tab1;
```


We should have something like this:


![7](https://blog.openreplay.com/images/bulding-a-mobile-app-with-react-and-ionic/images/image07.png)


Sweet!
So far, we've seen how to set up an Ionic React app and perform functions like data fetching. In the following sections, we'll look at how we can make our app more interactive by allowing users to save their favorite pictures.



<h2>Open Source Session Replay</h2> 

_<a href="https://github.com/openreplay/openreplay" target="_blank">OpenReplay</a> is an open-source, session replay suite that lets you see what users do on your web app, helping you troubleshoot issues faster. OpenReplay is self-hosted for full control over your data._

![OpenReplay](https://blog.openreplay.com/banner-blog_1oYPGT.png)

_Start enjoying your debugging experience - <a href="https://github.com/openreplay/openreplay" >start using OpenReplay for free</a>._






## Set up application storage in Ionic React

First, we have to set up our application storage. We'll use the [ionic/storage](https://github.com/ionic-team/ionic-storage#ionic-storage) package, a simple key-value Storage module for Ionic apps. This utility uses the best storage engine available on the platform without having to interact with it directly.
With ionic/storage, we don't need to worry about the underlying storage method used on the target platform. 


```
npm install @ionic/storage
```

Once that's installed, create a new file - `./modules/Store.ts` and initialize the store:


```typescript
// ./src/modules/Store.ts

import { Storage } from "@ionic/storage";
export const store = new Storage();
store.create();
```


Awesome!
Now, we can use it in our PhotoCard component to save favorite photos.


## Add favorite functionality to the PhotoCard component

We'll add a couple of functions to get favorites from the store, check if the current photo is already in the favorites list, add the photo to favorites, and remove the photo from favorites.

In the `./src/components/PhotoCard.tsx` file, enter the following:


```javascript
// ./src/components/PhotoCard.tsx

import { useEffect, useState } from "react";
import {
  IonButton,
  IonCard,
  IonCardContent,
  IonCardSubtitle,
  IonCol,
  IonIcon,
  IonRow,
} from "@ionic/react";
import { heart, heartOutline } from "ionicons/icons";
import { store } from "../modules/Store";
import { Photo } from "../types/Photo";
const PhotoCard = ({ photo }: { photo: Photo }) => {
  const { id, urls, description, user, likes } = photo;
  const [isFavorite, setIsFavorite] = useState(false);

  /**
   * function to get the favorites from the store
   * @returns {Promise<Photo[]>} the favorites
   */
  const getFavorites = async (): Promise<Photo[]> => {
    const favorites = await store.get("favorites");
    console.log({ favorites });
    return favorites || [];
  };

  /**
   * function to check if a photo is already in favorites
   */
  const checkIsFavorite = async () => {
    const favorites = await getFavorites();
    const isFavorite = favorites.some((favorite: any) => favorite.id === id);
    console.log({ isFavorite });
    setIsFavorite(isFavorite);
  };

  /**
   * function to add a photo to favorites
   * @param photo
   */
  const addToFavorites = async (photo: Photo) => {
    let favorites = await getFavorites();
    store.set("favorites", [...favorites, photo]);
    console.log({ favorites });
    setIsFavorite(true);
  };

  /**
   * function to remove a photo from favorites
   */
  const removeFromFavorites = async () => {
    let favorites = await getFavorites();
    store.set(
      "favorites",
      favorites.filter((favorite: { id: string }) => favorite.id !== id)
    );
    setIsFavorite(false);
  };

  /**
   * function to add or remove a photo from favorites depending on the current `isFavorite` state
   */
  const toggleFavorite = async () => {
    if (isFavorite) {
      removeFromFavorites();
    } else {
      addToFavorites(photo);
    }
  };

  useEffect(() => {
    checkIsFavorite();
  }, []);

  return (
    <IonCard>
      <img
        src={urls.regular}
        alt={description}
        style={{
          height: "24rem",
          width: "100%",
          objectFit: "cover",
        }}
      />
      <IonCardContent>
        <IonRow>
          <IonCol style={{ display: "flex", flexDirection: "column" }}>
            <IonCardSubtitle>@{user.username}</IonCardSubtitle>
            <IonCardSubtitle>
              {likes} like{likes > 1 && "s"}
            </IonCardSubtitle>
          </IonCol>
          <IonCol className="ion-text-right">
            <IonButton
              onClick={() => {
                toggleFavorite();
              }}
            >
              <IonIcon icon={isFavorite ? heart : heartOutline} />
            </IonButton>
          </IonCol>
        </IonRow>
      </IonCardContent>
    </IonCard>
  );
};
export default PhotoCard;
```


Let's quickly look at the changes we've made and the functions we've added to our component.
To access our ionic storage, we have to import `store`,


```javascript
import { store } from "../modules/Store"; 
```

Now, we can create the functions:

- `getFavorites` - to get the favorites from the store
- `checkIsFavorite` - to check if the photo is already in the favorites list
- `addToFavorites` - to add a photo to favorites
- `removeFromFavorites` - to remove a photo from favorites
- `toggleFavorite` - to add or remove a photo from favorites depending on the current `isFavorite` state

To check if the photo is already in the favorites list when mounted, we use `useEffect()` to run the `checkIsFavorite` function, which sets the `isFavorite` state to `true` or `false`.

To add or remove a photo from favorites, we add an `onClick` handler to our button and change the icon accordingly:


```javascript
<IonButton
  onClick={() => {
    toggleFavorite();
  }}
>
  <IonIcon icon={isFavorite ? heart : heartOutline} />
</IonButton>
```



### Using IonToast for subtle notifications
To let the user know when a photo has been added or removed from the favorites list, we'll use `useIonToast` to trigger a subtle toast element.

First, we add it to our imports:


```javascript
// ./src/components/PhotoCard.tsx

import {
  ...
  useIonToast,
} from "@ionic/react";
```


Next, we initialize `useIonToast` and assign it to `present`, which accepts the toast message and other configurations and triggers the toast.


```javascript
// ./src/components/PhotoCard.tsx

import {
  ...
  useIonToast,
} from "@ionic/react";

// ...

const PhotoCard = ({ photo }: { photo: Photo }) => {
  // ...
  const [present] = useIonToast();

  /**
   * function to present a toast message
   * @param message the message to display
   */
  const presentToast = (message: string) => {
    present({
      message,
      duration: 2000,
      animated: true,
      position: "bottom",
    });
  };

  // ...

  /**
   * function to add or remove a photo from favorites depending on the current `isFavorite` state
   */
  const toggleFavorite = async () => {
    if (isFavorite) {
      removeFromFavorites();
    } else {
      addToFavorites(photo);
    }
    presentToast(isFavorite ? "Removed from favorites" : "Added to favorites");
  };
  useEffect(() => {
    checkIsFavorite();
  }, []);
  return (
    {/*  */}
  )
}
```


As you can see, we call `presentToast` from our `toggleFavorite` function, allowing the message to be shown after a photo has been added or removed from the favorites list.


## Create a favorites page

Now, we'll create a page to display our favorite photos. For that, we'll modify `./src/pages/Tab2.tsx`:


```javascript
// ./src/pages/Tab2.tsx
import { useState } from "react";
import {
  IonCol,
  IonContent,
  IonGrid,
  IonHeader,
  IonPage,
  IonRow,
  IonTitle,
  IonToolbar,
  useIonViewWillEnter,
} from "@ionic/react";
import { store } from "../modules/Store";
import PhotoCard from "../components/PhotoCard";
import { Photo } from "../types/Photo";
import "./Tab1.css";
const Tab2: React.FC = () => {
  const [photos, setPhotos] = useState<Photo[]>([]);

  const getFavorites = async () => {
    const favorites = await store.get("favorites");

    return favorites || [];
  };

  useIonViewWillEnter(async () => {
    setPhotos(await getFavorites());
  });
  return (
    <IonPage>
      <IonHeader>
        <IonToolbar>
          <IonTitle>Favorites</IonTitle>
        </IonToolbar>
      </IonHeader>
      <IonContent fullscreen>
        <div className="gallery">
          <IonGrid className="photo-list">
            <IonRow>
              {photos.map((photo) => (
                <IonCol
                  sizeXs="12"
                  sizeMd="6"
                  sizeXl="4"
                  className="photo-list-item"
                  key={photo.id}
                >
                  <PhotoCard photo={photo} />
                </IonCol>
              ))}
            </IonRow>
          </IonGrid>
        </div>
      </IonContent>
    </IonPage>
  );
};
export default Tab2;
```


Although this is very similar to the Tab1 page, the notable differences are:

- We're using `getFavorites` to get photos from storage and assigning the value to the `photo` state.
- We're using `useIonViewWillEnter`, which is fired when the Tab2 page is about to animate into view and get the photos instead of `useEffect`. 

Finally, we're going to remove the third tab link and change the icons in the tab bar in `./src/App.tsx`:

```javascript
// ./src/App.tsx

import { Redirect, Route } from "react-router-dom";
import {
  IonApp,
  IonIcon,
  IonLabel,
  IonRouterOutlet,
  IonTabBar,
  IonTabButton,
  IonTabs,
  setupIonicReact,
} from "@ionic/react";
import { IonReactRouter } from "@ionic/react-router";
import { heart, image } from "ionicons/icons";
import Tab1 from "./pages/Tab1";
import Tab2 from "./pages/Tab2";
import Tab3 from "./pages/Tab3";
/* 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";
setupIonicReact();
const App: React.FC = () => (
  <IonApp>
    <IonReactRouter>
      <IonTabs>
        <IonRouterOutlet>
          <Route exact path="/tab1">
            <Tab1 />
          </Route>
          <Route exact path="/tab2">
            <Tab2 />
          </Route>
          <Route exact path="/">
            <Redirect to="/tab1" />
          </Route>
        </IonRouterOutlet>
        <IonTabBar slot="bottom">
          <IonTabButton tab="tab1" href="/tab1">
            <IonIcon icon={image} />
            <IonLabel>Photos</IonLabel>
          </IonTabButton>
          <IonTabButton tab="tab2" href="/tab2">
            <IonIcon icon={heart} />
            <IonLabel>Favorites</IonLabel>
          </IonTabButton>
        </IonTabBar>
      </IonTabs>
    </IonReactRouter>
  </IonApp>
);
export default App;
```



With that, we should have this as our final result:


![8](https://blog.openreplay.com/images/bulding-a-mobile-app-with-react-and-ionic/images/image08.gif)



## Conclusion

Congratulations on getting to the end of this article. So far, we've learned how to easily develop cross-platform applications using Ionic and React. We could customize our app, fetch data from the Unsplash API, and use multiple Ionic components and utilities.

### Further reading and resources

Although not covered in this article, it's pretty easy to build and deploy applications to the target platform. You can read more about it in the Ionic documentation: [Deploying to iOS and Android](https://ionicframework.com/docs/react/your-first-app/deploying-mobile) 

Other resources you might find helpful:

- [Theming in Ionic](https://ionicframework.com/docs/theming/basics)
- [Ionic UI components](https://ionicframework.com/docs/components)
- [Ionic React Guide](https://ionicframework.com/docs/react)

You can find the [source code for this project on GitHub](https://github.com/miracleonyenma/photos-ionic-react-app)

> A TIP FROM THE EDITOR: Don't miss the Ionic+Capacitor combination, as described in our recent [Cross-Platform Development Using Ionic+Capacitor](https://blog.openreplay.com/cross-platform-development-with-ionic-capacitor/) article!




[![newsletter](https://blog.openreplay.com/newsletter_Z1P0UIP.png)](https://newsletter.openreplay.com/)