I recently deployed my first Ionic React app to the Apple App and Google Play stores. Overall, I’m pretty impressed by the Ionic development experience, and I would work with Ionic again. I felt empowered by Ionic — excited that I could create a mobile application with my knowledge as a JavaScript developer. However, the intent of this blog post is neither to celebrate nor do a holistic review of Ionic React. I’d like to point out and document the things that I found unexpected, and in some cases frustrating.
Topics covered in this post:
For reasons that are not entirely clear to me (but that I assume are reasonable), Ionic does some of its own page and DOM management alongside React. If I use the browser inspector and watch my Ionic app’s DOM as I flip between pages, I see new pages added to the DOM as they’re opened, but the page and its components are not removed after clicking to a different page.
As a result, in some scenarios, the typical lifecycle management techniques I’d use as a React developer didn’t always work as expected with Ionic React. For instance, there could be scenarios where the following useEffect
function does not execute when a component returns to the view:
import React, { useEffect } from 'react';
const Cat: React.FC = () => {
useEffect(() => {
console.log('meow');
}, []);
return (
<h1>Do you hear the cat?</h1>
);
}
Because of this, the Ionic framework provides its own lifecycle methods for class components and hooks for functional components. As a general rule of thumb, I used the Ionic lifecycle hooks for Ionic page components, and my “regular” tactics for shared or inner page or “regular” components. This strategy caused no issues until I needed to have a user action (swiping on a push notification) trigger the app to open to a specific page, display a shared component, and have that shared component call an API to refresh some data. I found that in this scenario, I needed to use both a useEffect
with an empty dependency array and a useIonViewWillEnter
hook together, something like:
useEffect(() => {
refreshData();
}, []);
useIonViewWillEnter(() => {
refreshData();
});
I had a lot of trouble scrolling the user to a certain point on a page over a specified time duration…until it suddenly worked. I can’t remember or say for certain whether the initial problem stemmed from my implementation or a bug in Ionic. At the time, I was convinced it was a bug in Ionic related to the GitHub issue bug: client rects of any Ionic component in React is not measurable #19770. Ultimately, after upgrading from Capacitor 2.x to 3.x, while making some other changes, my problem just went away. I’m not sure whether it was the upgrade or the “some other changes” that fixed it.
It’s worth noting that Ionic has its own scroll events that you need to enable.
I wanted to reuse Ionic’s modal component as a menu. The menu needed to have a variable number of options, but it would never have enough options for the modal to fill the height of the screen. Thus, per the mock from my designers, the modal’s height needed to adjust to the size of the content. However, the Ionic modal component wants a set height
.
I used a comment in this GitHub ticket to set the height of the modal to the height of its content. The key code looked something roughly like:
.modal-custom-class {
align-items: flex-end;
}
.modal-custom-class .ion-page {
display: block;
position: relative;
/* not sure what this is, but needed for android */
contain: content;
}
At first, I omitted the contain: content
line because I didn’t know what it was. Everything worked on iOS, but the modal appeared empty on Android. Adding that line resolved the Android issue.
There was a difference in how iOS and Android devices resized and aligned my images. This turned into a frustrating issue. The app I was working on was image heavy. I spent a long time only developing in iOS, believing we would launch the initial version of our app only for iPhones. In iOS, all of our images — as page backgrounds and in card components and in carousels and as button backgrounds — looked great with minimal additional styling to any Ionic component that I used. But then we decided to throw in the launch of the Android version, and on Android, nearly every use of an image looked awkward.
For iOS, if I was using an image as a background, I could generally just throw in code that looked like…
.my-background-image {
background-position: center center;
background-repeat: no-repeat;
background-size: cover;
}
…and move on, feeling like a master of the craft of CSS.
But on Android, nearly every image seemed misaligned. I didn’t find a polyfill to make my problem go away, or isolate a CSS property that could make everything work on both platforms. Instead, for every case in which I used an image or background image, I checked the platform (using Ionic’s isPlatform('android')
) and added a class such as:
.my-android-background-image {
background-position: 50% 15%;
}
ion-img.my-android-ionic-image-component::part(image) {
object-position: 50% 10%;
}
One of our layouts included an element that needed to “break out” of its parent and appear to lay on top of the parent’s border, but the child element was getting clipped — visually, any part of the child that extended outside of the parent was invisible or hidden. To get this to work on Android, I was able to just add contain: none
to the parent element. I don’t remember ever thinking deeply about a contain
CSS property in my previous five years as a web developer, but it was crucial to solving at least two Android issues in Ionic React.
Unexpected stacking behavior on Android devices also appears to be an issue in React Native. I suggest checking out these issues: [Android] overflow visible doesn’t work #6802 and [Android] position: absolute shouldn’t be cut off within a container with border #12534.
If you’re generally having trouble getting a child to visually overlap its parent on a website or Ionic iOS, I recommend reading this blog post: How to make absolute positioned elements overlap their overflow hidden parent.
Ionic has an infinite scroll component, but it isn’t supported in Ionic React (and only Ionic React). However, implementing infinite scroll in an Ionic React component was pretty easy. I followed this blog post: Infinite Scroll for Ionic/React by Andrey Lechev
We wanted to implement a back button that always lead back to the most recent time that someone visited the home page. I tried to use Ionic’s back button component, but the button seemed to mostly just hide itself — and when I could get it to appear, it didn’t seem to lead to the right point in history.
I suspect that Ionic’s back button component was designed for one-page applications like Instagram, and that the back button for each page considers that page to be its root, or something. I didn’t figure it out. Ultimately, I just did something like this:
import React from 'react';
import { useHistory } from 'react-router-dom';
import SomeIcon from 'some-icon-library';
const BackButton: React.FC = () => {
const history = useHistory();
return (
<SomeIcon onClick={() => history.goBack()} />
);
};
export default BackButton;
At the time I was working on an Ionic app, Fullstory did not support Ionic. We were told by a representative that it would be coming soon. We used UX Cam instead. It was easy to install and seemed fine to our marketing people.
When I used the useIonModal
hook, Ionic seemed to place the modal at the end of the DOM — not cleanly inside my page or content or, more pertinently, inside of my use of IonReactRouter
. This meant I couldn’t just import { useHistory } from 'react-router-dom'
and use React Router directly in my modal component. However, using the router within a modal was still pretty easy. I just passed a function that calls history.push
as a callback from the component opening the modal to the modal itself. The result was something like:
import React from 'react';
import {
IonCard,
IonCardContent,
useIonModal
} from '@ionic/react';
import { useHistory } from 'react-router-dom';
import ModalBody from './ModalBody';
const SomeCard: React.FC = () => {
const history = useHistory();
const [presentModal, dismissModal] = useIonModal(ModalBody, {
onDismiss: () => dismissModal(),
handleClick: (catId: number) =>
history.push(`/cats/${catId}`)
});
return (
<IonCard
onClick={() =>
presentModal({
cssClass: 'custom-modal-class'
})
}
>
<IonCardContent>
<p>
I am some card content, and I open a modal. The modal will have a bunch
of pictures of cats. You can click on a picture of a cat to be lead
to a profile page for that cat.
</p>
</IonCardContent>
</IonCard>
);
};
export default SomeCard;
The modal can then call handleClick
with the catId
to route to the cat’s profile page.