TypeScript Types You Need To Know

If You’re Always Using Any For Ambiguous Situations You Are Doing It Wrong. There Are Better Safer Alternatives.


If you learn better with video try this

When you first start using TypeScript any situation where it is difficult to know the right type for a variable may cause you to set the type as any. Obviously this is not ideal as the whole point of using TypeScript is to have clear types set so that the compiler can help you catch errors — before your code makes it into production. Let’s see what methods there are already in the language that allow us to avoid using any and therefore gain the full benefits of using TypeScript.

Unknown Type

This is a type that I’m sure most TypeScript developers are aware of, but only a few are actually using. In addition there are some features of unknown that make it significantly different and safer to use than any. For example you might know that the unknown type allows setting of a variable to a value of any type. And that value can also be reset afterwards to a completely different type, just like the any type. However this is where the similarities end.

In the case of type any the compiler will not do type checking on your behalf. Once you set something to type any the compiler simply gives up and leaves you to your own devices. But in the case of the type unknown the compiler believes that your variable actually does have a type. However it does not know or even try and guess what that type is. It demands that you verify the type yourself and prove that it is of one type or another. Only then will the compiler allow you to either call into the variable’s members or set the variable to another variable of the same type (note without these checks it is still possible to set an unknown to another unknown or to an any). Let’s take a look at some examples of how unknown works.

First let’s look at a sample any variable.

let val: any = 22;
val = "hello";
val = new Array();
val.myNonExistantFunction();
console.log(val);
view raw any type sample hosted with ❤ by GitHub

As you can see I am able to set and then reset the variable val to a number and then to a string and an array. Now the most noteworthy part is that I am also able to call a function, myNonExistantFunction, that does not exist. This code compiles without error using the TypeScript compiler. However when you run this code, on the JavaScript output you get this error.

Clearly this sort of runtime failure could be avoided by using a proper type declaration. Unfortunately not every situation provides clear information on what the correct type of some variable or return should be. For example when calling a web service or accepting json data. The type information may not be clearly defined or not directly compatible. It may even change under certain circumstances. This is why the unknown type is so useful. Let’s look at the same example using unknown as the type.

let val: unknown = 22;
val = "hello";
val = new Array();
val.myNonExistantFunction();
console.log(val);
view raw unknown type hosted with ❤ by GitHub

As soon as you enter this code you should see your editor complain about the function myNonExistantFunction, that it does not exist for type unknown. But here’s the thing with unknown it will not allow you to call any members of a variable or set that variable to anything until you have proven that it is of the type required. So if I do this it also complains.

let val: unknown = 22;
val = "hello";
val = new Array();
val.push(44);
console.log(val);
view raw unknown array hosted with ❤ by GitHub

This may be unexpected, because clearly right above the line val.push() I set the val variable to a new array. So then how do I make use of a variable of type unknown? Let’s update the code so that adding to the array works.

let val: unknown = 22;
val = "hello";
val = new Array();
if(val instanceof Array) {
val.push(44);
}
console.log(val);

So there’s two things to note here. First an unknown variable or return does have an actual type above and beyond unknown. So a simple way of thinking about it is that the type unknown is more like a label and “hidden” inside is the real type. Second once I’ve confirmed what type is actually being used only then can I call into the variable members or set it to some other variable of the same type.

So unknown is a good alternative to use instead of any in situations where it’s not immediately clear what type something should be or if that type could potentially change. However there is another way to deal with type ambiguity, union types.

Union Type

In the case of union types they can be used similarly to unknown types. However they are a little less flexible in that you must know up front all the possible types that may be returned by some call. Here’s an example to clarify.

let acceptApiResult: undefined | null | Array<string>;
acceptApiResult = undefined;
console.log(acceptApiResult);
acceptApiResult = null;
console.log(acceptApiResult);
acceptApiResult = new Array<string>();
acceptApiResult.push("A");
acceptApiResult.push("B");
console.log(acceptApiResult);
view raw union hosted with ❤ by GitHub

In the above example the result of compiling and running this code is this.

As you can see the TypeScript compiler accepts all three distinct types without issue (note in JavaScript undefined and null are different types, undefined is its own type and null is an object type). And JavaScript runs this code to completion. So again this is another alternative to the type any that still allows you to maintain type safety.

Interface Type

If you’re a TypeScript developer you’re already familiar with this type. However you may not have yet used it for difficult typing situations. For example for a recent project on my app, DzHaven, I had to setup payments. And the payment provider returned an enormous object for one of its calls. It had several dozen properties and some of those properties in turn had their own properties. However I was only using a tiny subset of those members. Additionally the payment vendor was not using TypeScript so it was not always clear what type each field should be. It would have taken hours to figure this out, but the return on time investment would have been very small since as mentioned I only needed a few fields.

These kinds of situations can cause some developers to just set the object as type any and be done with it, but again there’s lots of issues with doing that. So what I did instead is defined my own interface with only a subset of the fields and just set the return type as that. Here’s a possible example of such a scenario to clarify what I mean.

interface MyData {
name: string;
age: number;
}
const sourceData = {
name: "jon",
age: 25,
country: "United States",
sex: "male",
occupation: "Programmer"
}
const receivedData: MyData = sourceData;
console.log(receivedData);
view raw interface hosted with ❤ by GitHub

If you compile and run this code you will see that it compiles and runs without error and returns this.

You may be wondering why it returns all the fields instead of just the ones that are part of the type declaration. This is because the compiler does not change the set data, it only checks for a type match (the fields of type MyData). So when console.log runs it returns the entire object’s members.


As TypeScript continues to grow in popularity type definition files will continue to fill in type information and reduce the amount of ambiguous typing scenarios we face. However until that happens completely, as TypeScript developers we need to be careful to use the language in a best practices sort of way.

Cool that was a small overview of the ways in which you can maintain type safety in your TypeScript code and use the language in the way it was intended. Happy coding.

As always if you like helping other devs try DzHaven.com

Is It Possible To Build A Mobile iOS And Android App Using Standard React And Web Technologies?

Yes. In a normal React web app you can access the camera, location services, the file system and more


Did you ever think how awesome it would be if you could keep coding with React and web technologies and somehow still get your apps onto the Apple App and Google Play stores with native functionality? You wouldn’t need to learn Swift or Kotlin. Nor would you have to build the same app multiple times. I am going to show you a simple seamless way to do this.

We are going to create a photo app that lets us take pictures, save it onto the device, and view later with information about where the image was taken. Let’s create a new project using create-react-app and call it reactmobile. Now just to be clear I’ve not tricked you into becoming a React Native or Flutter dev. This app will be entirely a React web app, using all the common web technologies you already know — like HTML/JSX and CSS. So when you build the app UI you will be using those technologies like you always do. However we will add a set of npm packages that will allow us to interface into native hardware, while still writing pure JavaScript only. Let’s get started.

First open up a command line or in VSCode open your terminal. Let’s add our packages.

npm i @capacitor/core @capacitor/cli

Capacitor is a library that allows any browser based web application to access hardware services on mobile devices. This means that we can keep writing our code not only using React, but also continuing to use url routing, through React Router, and other familiar React tools like Redux. That’s right, nothing changes in your workflow. The only difference is through Capacitor you now have access to device hardware and are able to create apps for the Apple App store and Google Play store.

Now that we’ve installed Capacitor we simply need to turn it on for the mobile platforms that we care about in our project. In the interests of time I’ll keep this story for iOS devices only, but Android works just as well. To enable Capacitor for iOS run these commands. Note when running init you’ll be prompted for an app name and id. You can use whatever you like since this is just a test.

npx cap init
npx cap add ios

So what we’ve done here is enable Capacitor in our project and created a folder that has all the iOS related files and assets. You will never need to touch these files, they are auto created for you by Capacitor and then copied to your Mac’s XCode when you’re ready to build your iOS project. Let’s make one small change before moving on. Open the file capacitor.config.json and update the webDir variable to be “build” instead of “www”. Since our React project saves its production build into the build folder we need to let Capacitor know this. Now I’ll show how to add camera capability. Open the App.js file and update it like this.

import React, { useEffect, useCallback } from 'react';
import {
CameraResultType,
CameraSource,
Plugins
} from "@capacitor/core";
import './App.css';
function App() {
const { Camera } = Plugins;
const triggerCamera = useCallback(async () => {
await Camera.getPhoto({
quality: 100,
allowEditing: false,
resultType: CameraResultType.Uri,
source: CameraSource.Camera
});
}, [Camera.getPhoto]);
const onClickCamera = () => {
triggerCamera();
}
return (
<div className="App">
<header className="App-header">
<button onClick={onClickCamera} style={{ width: '10em'}}>Camera</button>
</header>
</div>
);
}
export default App;
view raw App.js camera hosted with ❤ by GitHub

As you can see the @capacitor/core package is imported with some properties. The most important one, Plugins, is used to access the camera. We use a useCallback React hook to initialize the camera with some default configs onto a simple interface with just one button to trigger the camera. Let’s run this on our iOS device by running the Capacitor cli commands to move our code over to XCode.

npm run build // need to build our project successfully first
npx cap sync // need to sync the ios folder into our XCode project oncenpx cap copy ios // copies all our build files into XCode
npx cap open ios // opens XCode project

You might find the addition of these commands cumbersome, but realize what’s going on here. Since we are building a native app we need to compile our app into a native iOS binary. Obviously the only way to do this is with XCode. So that’s what these steps are for. Now run the project on your iOS device and since we have no UI, you will see only one button that says camera. If you click on it you will see the camera app that looks like this.

Sweet we now have a working camera with almost no effort. Now let’s save the photo onto the filesystem so we can view it later. Add this code into the App.js file.

import React, { useEffect, useState, useCallback } from 'react';
import {
CameraResultType,
CameraSource,
Plugins,
FilesystemDirectory,
Capacitor
} from "@capacitor/core";
import './App.css';
function App() {
const [photo, setPhoto] = useState();
const [lastWebPath, setLastWebPath] = useState();
const [lastPhotoPath, setLastPhotoPath] = useState();
const { Camera, Filesystem } = Plugins;
const triggerCamera = useCallback(async () => {
const cameraPhoto = await Camera.getPhoto({
quality: 100,
allowEditing: false,
resultType: CameraResultType.Uri,
source: CameraSource.Camera
});
setPhoto(cameraPhoto);
}, [Camera.getPhoto]);
useEffect(() => {
if(photo && photo.webPath !== lastWebPath){
setLastWebPath(photo.webPath);
Filesystem.readFile({
path: photo.path
})
.then(async photoInTemp => {
const fileName = new Date().toString() + ".jpg";
await Filesystem.writeFile({
data: photoInTemp.data,
path: fileName,
directory: FilesystemDirectory.Data
});
const finalPhotoUrl = await Filesystem.getUri({
directory: FilesystemDirectory.Data,
path: fileName
});
const filePath = finalPhotoUrl.uri;
const photoUrl = Capacitor.convertFileSrc(filePath);
if (photoUrl !== lastPhotoPath) {
console.log('photoUrl', photoUrl);
setLastPhotoPath(photoUrl);
}
});
}
}, [photo]);
const onClickCamera = () => {
triggerCamera();
}
return (
<div className="App">
<div style={{ marginTop: '40px'}}>
<div>
<img
src={lastPhotoPath}
alt="last img taken"
style={{
border: 'solid 1px red',
marginLeft: 'auto',
marginRight: 'auto',
maxWidth: '200px',
height: '300px'
}} />
</div>
<div>
<button
onClick={onClickCamera}
style={{
width: '10em',
}}>Camera</button>
</div>
</div>
</div>
);
}
export default App;
view raw App add save hosted with ❤ by GitHub

You can see we are now using a useEffect that reacts when the photo object is changed. This photo object takes the immediately taken photo from the camera. Now the thing with photos is that whenever a picture is taken it is first saved into temp storage and may be deleted by the device at some point in the future. So we need to move it into a permanent app directory. This is where Capacitor’s filesystem API comes in to help us access the native filesystem and read and write files. So the next thing we do in the code is we first read the file from its temp location with readFile, then we write it with writeFile into a permanent directory, and once written we retrieve the final path and then convert that filesystem path into a url — so that we can pass it to the img tag’s src attribute later. Note we also updated the JSX for alignment. Before looking at the screen again, let’s add code to save these image file names into local storage so that we can retrieve the images and display them into a list.

import React, { useEffect, useState, useCallback } from 'react';
import {
CameraResultType,
CameraSource,
Plugins,
FilesystemDirectory,
Capacitor
} from "@capacitor/core";
import moment from 'moment';
import './App.css';
const LOG_PREFIX = "[App] ";
function App() {
const [allImages, setAllImages] = useState();
const [photo, setPhoto] = useState();
const [lastWebPath, setLastWebPath] = useState();
const [lastPhotoPath, setLastPhotoPath] = useState();
const { Camera, Filesystem, Storage } = Plugins;
const triggerCamera = useCallback(async () => {
const cameraPhoto = await Camera.getPhoto({
quality: 100,
allowEditing: false,
resultType: CameraResultType.Uri,
source: CameraSource.Camera
});
setPhoto(cameraPhoto);
}, [Camera]);
useEffect(() => {
if(photo && photo.webPath !== lastWebPath){
const setupImages = async (fileName) => {
try {
const imgKey = {
key: "imagePaths"
};
const imagePaths = await Storage.get(imgKey);
console.log(LOG_PREFIX + "imagePaths", imagePaths);
const imagePathsArray = imagePaths.value ? JSON.parse(imagePaths.value) : [];
imagePathsArray.push({
fileName
});
console.log(LOG_PREFIX + "imagePathsArray", imagePathsArray);
const imagePathsArrayString = JSON.stringify(imagePathsArray);
await Storage.set({
key: "imagePaths",
value: imagePathsArrayString
});
const images = Promise.all(imagePathsArray.map(async img => {
const fileUri = await Filesystem.getUri({
directory: FilesystemDirectory.Data,
path: img.fileName
});
const photoUrl = Capacitor.convertFileSrc(fileUri.uri);
return (<div key={fileName}>
<img
style={{
border: 'solid 1px red',
marginLeft: 'auto',
marginRight: 'auto',
maxWidth: '200px',
height: '300px'
}}
src={photoUrl}
alt="img" />
</div>
);
}));
const finalImages = await images;
setAllImages(finalImages);
} catch(err) {
console.log(LOG_PREFIX + err);
}
}
setLastWebPath(photo.webPath);
Filesystem.readFile({
path: photo.path
})
.then(async photoInTemp => {
const date = moment().format("MM-DD-YY-h-mm-ss");
const fileName = date + ".jpg";
await Filesystem.writeFile({
data: photoInTemp.data,
path: fileName,
directory: FilesystemDirectory.Data
});
const finalPhotoUrl = await Filesystem.getUri({
directory: FilesystemDirectory.Data,
path: fileName
});
const filePath = finalPhotoUrl.uri;
const photoUrl = Capacitor.convertFileSrc(filePath);
if (photoUrl !== lastPhotoPath) {
console.log(LOG_PREFIX + 'photoUrl', photoUrl);
setLastPhotoPath(photoUrl);
}
setupImages(fileName);
});
}
}, [photo, Filesystem, Storage, lastPhotoPath, lastWebPath]);
const onClickCamera = () => {
triggerCamera();
}
return (
<div className="App">
<div style={{ marginTop: '40px', border: 'solid 1px black', padding: '10px' }}>
<div>
<img
src={lastPhotoPath}
alt="last img taken"
style={{
border: 'solid 1px red',
marginLeft: 'auto',
marginRight: 'auto',
maxWidth: '200px',
height: '300px'
}} />
</div>
<div>
<button
onClick={onClickCamera}
style={{
width: '10em',
}}>Camera</button>
</div>
</div>
<div style={{ marginTop: '20px'}}>
<strong>My Pics</strong>
{allImages}
</div>
</div>
);
}
export default App;

There’s quite a bit of new code but the main thing to see is that we are using Capacitors Storage API to retrieve and set the local storage, the get and set methods, with the names of the photo files. Notice also I’m using moment to make the file name a little cleaner. The key thing to pay attention to in the code is that the convertFileSrc function is required to convert the file path from a system file path to a url. Or else img tags will not recognize the path and they will fail. Here’s what the screen now looks like.

You can see not only the last taken pic on top but a list of all previous pictures beneath the camera button. Awesome we created a React web app that works on mobile devices as a native app. Now let’s do one more thing and add locations for each image taken. Update your App file like this.

import React, { useEffect, useState, useCallback } from 'react';
import {
CameraResultType,
CameraSource,
Plugins,
FilesystemDirectory,
Capacitor
} from "@capacitor/core";
import moment from 'moment';
import './App.css';
const LOG_PREFIX = "[App] ";
function App() {
const [allImages, setAllImages] = useState();
const [photo, setPhoto] = useState();
const [lastWebPath, setLastWebPath] = useState();
const [lastPhotoPath, setLastPhotoPath] = useState();
const { Camera, Filesystem, Storage, Geolocation } = Plugins;
const triggerCamera = useCallback(async () => {
const cameraPhoto = await Camera.getPhoto({
quality: 100,
allowEditing: false,
resultType: CameraResultType.Uri,
source: CameraSource.Camera
});
setPhoto(cameraPhoto);
}, [Camera]);
const getCurrentPosition = async () => {
const coordinates = await Geolocation.getCurrentPosition();
return coordinates;
}
useEffect(() => {
if(photo && photo.webPath !== lastWebPath){
const setupImages = async (fileName) => {
try {
const imgKey = {
key: "imagePaths"
};
const imagePaths = await Storage.get(imgKey);
console.log(LOG_PREFIX + "imagePaths", imagePaths);
const imagePathsArray = imagePaths.value ? JSON.parse(imagePaths.value) : [];
const coordinates = await getCurrentPosition();
console.log(LOG_PREFIX + 'Current location', coordinates.coords);
imagePathsArray.push({
fileName,
latitude: coordinates.coords.latitude,
longitude: coordinates.coords.longitude
});
console.log(LOG_PREFIX + "imagePathsArray", imagePathsArray);
const imagePathsArrayString = JSON.stringify(imagePathsArray);
await Storage.set({
key: "imagePaths",
value: imagePathsArrayString
});
const images = Promise.all(imagePathsArray.map(async img => {
const fileUri = await Filesystem.getUri({
directory: FilesystemDirectory.Data,
path: img.fileName
});
const photoUrl = Capacitor.convertFileSrc(fileUri.uri);
return (<div key={img.fileName}>
<label>{img.fileName}</label>
<br/>
<label>{`latitude: ${img.latitude}, longitude: ${img.longitude}`}</label>
<br/>
<img
style={{
border: 'solid 1px red',
marginLeft: 'auto',
marginRight: 'auto',
maxWidth: '200px',
height: '300px'
}}
src={photoUrl}
alt="img" />
</div>
);
}));
const finalImages = await images;
setAllImages(finalImages);
} catch(err) {
console.log(LOG_PREFIX + err);
}
}
setLastWebPath(photo.webPath);
Filesystem.readFile({
path: photo.path
})
.then(async photoInTemp => {
const date = moment().format("MM-DD-YY-h-mm-ss");
const fileName = date + ".jpg";
await Filesystem.writeFile({
data: photoInTemp.data,
path: fileName,
directory: FilesystemDirectory.Data
});
const finalPhotoUrl = await Filesystem.getUri({
directory: FilesystemDirectory.Data,
path: fileName
});
const filePath = finalPhotoUrl.uri;
const photoUrl = Capacitor.convertFileSrc(filePath);
if (photoUrl !== lastPhotoPath) {
console.log(LOG_PREFIX + 'photoUrl', photoUrl);
setLastPhotoPath(photoUrl);
}
setupImages(fileName);
});
}
}, [photo, Filesystem, Storage, lastPhotoPath, lastWebPath, getCurrentPosition]);
const onClickCamera = () => {
triggerCamera();
}
return (
<div className="App">
<div style={{ marginTop: '40px', border: 'solid 1px black', padding: '10px' }}>
<div>
<img
src={lastPhotoPath}
alt="last img taken"
style={{
border: 'solid 1px red',
marginLeft: 'auto',
marginRight: 'auto',
maxWidth: '200px',
height: '300px'
}} />
</div>
<div>
<button
onClick={onClickCamera}
style={{
width: '10em',
}}>Camera</button>
</div>
</div>
<div style={{ marginTop: '20px'}}>
<strong>My Pics</strong>
{allImages}
</div>
</div>
);
}
export default App;
view raw App with coordinates hosted with ❤ by GitHub

All we did was grab the Geolocation object from Plugins on line 19. Then we use that object in the function getCurrentPosition, which get’s the coordinates. Then we call that function later when building the array of photos and pass the latitude and longitude together with the file name starting at line 46. We then later add the file name and coordinates as labels onto our picture list. On the screen it looks like this. Note the prior images wont have coordinates since we hadn’t taken them at that time.

Let’s think about what just happened here. You took your existing React and web skills and created a native app. No Flutter, PhoneGap, Java, Kotlin, Swift, etc. No need to write the same code two or three times. No need to build a quasi web app using React Native and React Native Web … You’re welcome.

As always if you like helping other devs try DzHaven.com

Build Mobile iOS, Android, and Web Apps Using React and Ionic, Part IV

Write Once Run Anywhere Is Alive And Well In Modern JavaScript Development

This is Part IV in a series. You can find the previous Parts here III, III. In today’s story we will complete our second tab and list out our previously taken photos.

Layout In Ionic

Since this screen has more content let’s learn a bit more about layout in Ionic before continuing. If your page has multiple container elements and requires some strict rules around layout, the Ionic grid system is a built-in easy way of getting that layout. It is very similar to Bootstrap and uses grid, row, and column components for controlling your layout. The column system has up to 12 columns per screen and the columns resize based upon the size of the screen, again like Bootstrap. In addition, the columns have size tiers like small, medium, large, etc. which are based on minimum widths so up to and above a certain width that tier would apply. Note you can even have the grid system use more than 12 columns by changing the — ion-grid-columns CSS variable. There’s a lot more configurability to the grid system and more details can be found here. But for our purposes we’ll use it in a simple form within the new second tab.

You may be wondering if it’s worth it to use the Ionic grid system instead of something more established like Bootstrap or your own classes. However since the Ionic grid system is integrated with their other controls, including themes and styles, I would recommend using their system.

Let’s use this grid system in our new second tab. But first let’s create it without grids to see some of the issues grids solve. Start our second tab by creating a new page inside of the pages folder called Photos.tsx. For now create it as an empty functional component. Since we already know how to modify routes and tabs, based upon the last stories, I’ll just show you the updated App.tsx file that will display the new second tab called photos. This is what it should look like after adding Photos as the second tab. Quick note if you’re curios where the new photos icon comes from, all icons can be found and searched by going to Ionicons (the default icons for Ionic).

import React from "react";
import { Redirect, Route } from "react-router-dom";
import {
IonApp,
IonIcon,
IonLabel,
IonRouterOutlet,
IonTabBar,
IonTabButton,
IonTabs
} from "@ionic/react";
import { IonReactRouter } from "@ionic/react-router";
import { photos, camera } from "ionicons/icons";
import Tab2 from "./pages/Tab2";
import Details from "./pages/Details";
/* 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 Camera from "./pages/Camera";
const App: React.FC = () => (
<IonApp>
<IonReactRouter>
<IonTabs>
<IonRouterOutlet>
<Route path="/camera" component={Camera} exact={true} />
<Route path="/photos" component={Tab2} exact={true} />
<Route path="/photos/details" component={Details} />
<Route
path="/"
render={() => <Redirect to="/camera" />}
exact={true}
/>
</IonRouterOutlet>
<IonTabBar slot="bottom">
<IonTabButton tab="camera" href="/camera">
<IonIcon icon={camera} />
<IonLabel>Camera</IonLabel>
</IonTabButton>
<IonTabButton tab="photos" href="/photos">
<IonIcon icon={photos} />
<IonLabel>Photos</IonLabel>
</IonTabButton>
</IonTabBar>
</IonTabs>
</IonReactRouter>
</IonApp>
);
export default App;
view raw App.tsx part 4 hosted with ❤ by GitHub

Now go back to the new Photos.tsx file and add this code.

import React from "react";
import {
IonContent,
IonGrid,
IonRow,
IonCol,
IonPage,
IonHeader,
IonToolbar,
IonTitle,
IonText
} from "@ionic/react";
const Photos: React.FC = () => {
return (
<IonPage>
<IonHeader>
<IonToolbar>
<IonTitle>Photos</IonTitle>
</IonToolbar>
</IonHeader>
<IonContent>
<IonText>test</IonText>
</IonContent>
</IonPage>
);
};
export default Photos;

If you reload the screen you will see that the text alignment is not centered and on desktop screens it starts all the way to the left.

Now let’s replace this with a grid layout so we can get automatic centering and resizing. Update the Photos.tsx file with this code.

import React from "react";
import {
IonContent,
IonGrid,
IonRow,
IonCol,
IonPage,
IonHeader,
IonToolbar,
IonTitle,
IonText
} from "@ionic/react";
const Photos: React.FC = () => {
return (
<IonPage>
<IonHeader>
<IonToolbar>
<IonTitle>Photos</IonTitle>
</IonToolbar>
</IonHeader>
<IonContent>
<IonGrid>
<IonRow>
<IonCol size-md="6" offset-md="3" className="ion-text-center">
<IonText>test</IonText>
</IonCol>
</IonRow>
</IonGrid>
</IonContent>
</IonPage>
);
};
export default Photos;

After rebuilding you should see this for desktop screens.

The grid, along with its styling automatically centers itself across different devices. This is what it looks like on mobile devices.

Let’s review this code to understand how this centering is done. The IonGrid is the main container for grid items. The immediate child of an IonGrid must be an IonRow. And it is possible to have multiple IonRow components in an IonGrid. Inside the IonRow is an IonCol, which will contain all of our content. Now in the IonCol you can see we have a size property for medium sized screens, size-md, that we set to 6 columns. If we left it with this property alone, then this column would shift to the left as the total count needs to be 12. However since we also use the offset property, we are telling Ionic to offset this column by 3 from the left, which puts it in the horizontal middle of the screen. Let’s continue to the grid’s content. You’ll notice that the column has a CSS class “ion-text-center”, which centers the text in the IonText component. This is one of many styling and theme related shortcuts, for all Ionic components, that can be used to very quickly get desired styling effects, while maintaining adherence to the core theme. Particularly useful are the ion-padding and ion-margin CSS classes. These classes provide a theme consistent offset for your controls that’s easy to use and ensures the same spacing throughout the entire app. There are dozens of styles that can be used to adjust text, alignment, and spacing. You can see a complete list here.

Before continuing lets run the app on our iOS device or simulator. Normally you need to run Ionic build first and then the other commands that were mentioned in our previous stories, but here’s a faster shortcut.

ionic cap run ios

This single command will build, copy, and then open your XCode project. However note it does not do the sync piece. So if you’ve not done that you’ll need to do it just once, before running this command. Also note in order to sync, your Mac needs to have Cocoapods installed. Now let’s list out the photos we saved previously into the filesystem.

Bugs and Updates

The Ionic team has just released an update to their @ionic/react-hooks package and it now includes hooks for the Filesystem. So we’ll update our Camera code to use those hooks. We’ll also need to update that file as I had an infinite loop bug which was causing the code inside useEffect to run multiple times. My apologies for the issue. So let’s first update the hooks package. I had some trouble getting it to auto increase to version 0.0.6, which is the one with hooks. So I had to first update the package.json file with that version and then run below.

npm install @ionic/react-hooks

Once you do that the code in Camera.tsx should look like this.

import React, { useCallback, useEffect, useState } from "react";
import {
CameraResultType,
FilesystemDirectory,
Capacitor,
CameraSource
} from "@capacitor/core";
import { useCamera, availableFeatures } from "@ionic/react-hooks/camera";
import { useFilesystem } from "@ionic/react-hooks/filesystem";
import { useStorage } from "@ionic/react-hooks/storage";
import moment from "moment";
import {
IonButton,
IonContent,
IonHeader,
IonToolbar,
IonTitle,
IonPage,
IonText,
IonCard,
IonCardHeader,
IonCardContent,
IonCardTitle,
IonItemDivider
} from "@ionic/react";
export const LOG_PREFIX = "[Camera] ";
const Camera: React.FC = () => {
const { readFile, writeFile, getUri } = useFilesystem();
const { photo, getPhoto } = useCamera();
const [lastWebPath, setLastWebPath] = useState("");
const [lastPhotoPath, setLastPhotoPath] = useState("");
const { get, set } = useStorage();
const triggerCamera = useCallback(async () => {
if (availableFeatures.getPhoto) {
await getPhoto({
quality: 100,
allowEditing: false,
resultType: CameraResultType.Uri,
source: CameraSource.Camera
});
}
}, [getPhoto]);
useEffect(() => {
if (photo && photo.webPath !== lastWebPath) {
setLastWebPath(photo.webPath || "");
console.log(LOG_PREFIX + "photo.webPath", photo.webPath);
console.log(LOG_PREFIX + "lastWebPath", lastWebPath);
readFile({
path: photo ? photo.path || photo.webPath || "" : ""
})
.then(photoInTemp => {
let date = moment().format("MM-DD-YY-h-mm-ss");
let fileName = date + ".jpg";
// copy file into local filesystem, as temp will eventually be deleted
writeFile({
data: photoInTemp.data,
path: fileName,
directory: FilesystemDirectory.Documents
}).then(() => {
// now we try to read the file
getUri({
directory: FilesystemDirectory.Documents,
path: fileName
}).then(async finalPhotoUri => {
const filePath = finalPhotoUri.uri;
const photoUrl = Capacitor.convertFileSrc(filePath);
if (photoUrl !== lastPhotoPath) {
setLastPhotoPath(photoUrl);
}
const imagePaths = await get("imagePaths");
const imagePathsArray = imagePaths ? JSON.parse(imagePaths) : [];
imagePathsArray.push({
fileName
});
const imagePathsArrayString = JSON.stringify(imagePathsArray);
await set("imagePaths", imagePathsArrayString);
console.log(LOG_PREFIX + "imagePaths", await get("imagePaths"));
});
});
})
.catch(err => {
console.log(err);
});
}
}, [
photo,
readFile,
writeFile,
getUri,
get,
set,
lastPhotoPath,
lastWebPath
]);
const onClick = () => {
triggerCamera();
};
if (availableFeatures.getPhoto) {
return (
<IonPage>
<IonHeader>
<IonToolbar>
<IonTitle>Camera</IonTitle>
</IonToolbar>
</IonHeader>
<IonContent>
<IonCard>
<IonItemDivider>
<IonCardHeader>
<IonCardTitle>My Photo</IonCardTitle>
</IonCardHeader>
</IonItemDivider>
<IonCardContent>
{lastPhotoPath && <img src={lastPhotoPath} alt="my pic" />}
<IonButton style={{ marginTop: ".75em" }} onClick={onClick}>
camera
</IonButton>
</IonCardContent>
</IonCard>
</IonContent>
</IonPage>
);
}
return (
<IonPage>
<IonContent>
<IonText>No Camera Available</IonText>
</IonContent>
</IonPage>
);
};
export default Camera;
view raw Camera bug fix hosted with ❤ by GitHub

Starting near the top, as you can see we import the filesystem hook and get useFilesystem. I’ve also created a lastWebPath state object to detect when a new picture has been taken and prevent the infinite looping issue. I’ve also forced the camera source to be CameraSource.Camera so the user will not have to choose which source object to use. Moving down to the bottom you’ll also notice that imageNames is gone now and imagePaths no longer references the photoUrl. This is because on the Photos.tsx page I am now only going to use the file name instead of the path; because converting the imagePaths from string to JSON and back was causing some issues in reading the file paths by the img tag.

Using Ionic Lifecycle Events

The Camera.tsx file is now updated and is using Capacitor to save the new picture names into local storage. Now within our Photos tab, we can use storage and filesystem hooks to retrieve the file paths and display the images into a list. Let’s update the Photos.tsx file like this.

import React, { useState } from "react";
import {
IonContent,
IonGrid,
IonRow,
IonCol,
IonPage,
IonHeader,
IonToolbar,
IonTitle,
IonList,
IonLabel,
useIonViewWillEnter,
IonItemSliding,
IonItem,
IonItemOptions,
IonItemOption,
IonAvatar
} from "@ionic/react";
import { useStorage } from "@ionic/react-hooks/storage";
import { FilesystemDirectory, Capacitor } from "@capacitor/core";
import { useFilesystem } from "@ionic/react-hooks/filesystem";
export const LOG_PREFIX = "[Photos] ";
const Photos: React.FC = () => {
const [photoItems, setPhotoItems] = useState<JSX.Element[] | null>(null);
const { getUri } = useFilesystem();
const { get } = useStorage();
useIonViewWillEnter(async () => {
const imgPaths = await get("imagePaths");
const images = imgPaths ? JSON.parse(imgPaths) : null;
console.log(LOG_PREFIX + "images", images);
if (images && images.length > 0) {
const photos: JSX.Element[] = [];
for await (let image of images) {
console.log(LOG_PREFIX + "checking if file exists", image.fileName);
const finalPhotoUri = await getUri({
directory: FilesystemDirectory.Documents,
path: image.fileName
});
console.log(LOG_PREFIX + "image.fileName", image.fileName);
console.log(LOG_PREFIX + "finalPhotoUri", finalPhotoUri);
const photoUrl = Capacitor.convertFileSrc(finalPhotoUri.uri);
console.log(LOG_PREFIX + "converted photoUrl", photoUrl);
photos.push(
<IonItemSliding key={image.fileName}>
<IonItem>
<IonAvatar slot="start">
{photoUrl && <img src={photoUrl} alt="my pic" />}
</IonAvatar>
<IonLabel>{image.fileName}</IonLabel>
</IonItem>
<IonItemOptions side="end">
<IonItemOption onClick={() => {}}>Delete</IonItemOption>
</IonItemOptions>
</IonItemSliding>
);
}
console.log(LOG_PREFIX + "setPhotoItems");
setPhotoItems(photos);
}
});
return (
<IonPage>
<IonHeader>
<IonToolbar>
<IonTitle>Photos</IonTitle>
</IonToolbar>
</IonHeader>
<IonContent>
<IonGrid>
<IonRow>
<IonCol size-md="6" offset-md="3" className="ion-text-center">
<IonList>{photoItems}</IonList>
</IonCol>
</IonRow>
</IonGrid>
</IonContent>
</IonPage>
);
};
export default Photos;
view raw Photos hosted with ❤ by GitHub

As you can see we have a new hook called useIonViewWillEnter. This lifecycle hook is one of four event hooks that occur on all IonPage components. The others are useIonViewWillLeave, useIonViewDidEnter, and useIonViewDidLeave. Since we want our file data to arrive before the screen completes drawing, we are placing code to retrieve that data into the useIonViewWillEnter handler. Note the React class lifecycle events are still available, like componentDidMount and the others, but you should avoid them because according to Ionic documentation Ionic has taken over lifecycle control and so these events may not occur at the time you expect them to. Let’s continue. Within the useIonViewWillEnter hook we are using the storage and filesystem hooks to retrieve the file names we had saved earlier and get the photo’s path. In case you’ve not used it before, for await is a newer way of doing a for loop using async await. If there are async calls inside the for loop it will wait on that line before continuing. I added this call to make sure that by the time setPhotoItems is called the photos is fully populated with elements. Now once getUri gives us back the photo path we create some list items that we will later feed to a parent IonList component. These IonItems contain an IonAvatar for our image and an IonLabel for our image name. In addition since this is a native mobile app I thought it would look nicer if we could follow some mobile UI paradigms and so I am also using the IonItemSliding component. This component will allow the listed items to slide out to the left and reveal a Delete button. Let’s start the app again with this latest update.

ionic cap run ios

This is what you should see on your device when you slide an item out.

As you can see the IonItemSliding component also accepts an IonItemOptions component which takes the IonItemOption component that will ultimately handle the Delete button click. But before we add the handler for deletion, let’s add a click event handler to show a larger modal view of each image avatar that is clicked. Here’s the updated Photos.tsx file with the modal code and event handlers.

import React, { useState } from "react";
import {
IonContent,
IonGrid,
IonRow,
IonCol,
IonPage,
IonHeader,
IonToolbar,
IonTitle,
IonList,
IonLabel,
useIonViewWillEnter,
IonItemSliding,
IonItem,
IonItemOptions,
IonItemOption,
IonAvatar,
IonModal,
IonCard,
IonCardHeader,
IonCardTitle,
IonCardContent,
IonButton
} from "@ionic/react";
import { useStorage } from "@ionic/react-hooks/storage";
import { FilesystemDirectory, Capacitor } from "@capacitor/core";
import { useFilesystem } from "@ionic/react-hooks/filesystem";
import "./Photos.css";
export const LOG_PREFIX = "[Photos] ";
const Photos: React.FC = () => {
const [currentPhotoUrl, setCurrentPhotoUrl] = useState("");
const [currentFileName, setCurrentFileName] = useState("");
const [showModal, setShowModal] = useState(false);
const [photoItems, setPhotoItems] = useState<JSX.Element[] | null>(null);
const { getUri } = useFilesystem();
const { get } = useStorage();
useIonViewWillEnter(async () => {
const imgPaths = await get("imagePaths");
const images = imgPaths ? JSON.parse(imgPaths) : null;
console.log(LOG_PREFIX + "images", images);
if (images && images.length > 0) {
const photos: JSX.Element[] = [];
for await (let image of images) {
console.log(LOG_PREFIX + "checking if file exists", image.fileName);
const finalPhotoUri = await getUri({
directory: FilesystemDirectory.Documents,
path: image.fileName
});
console.log(LOG_PREFIX + "image.fileName", image.fileName);
console.log(LOG_PREFIX + "finalPhotoUri", finalPhotoUri);
const photoUrl = Capacitor.convertFileSrc(finalPhotoUri.uri);
console.log(LOG_PREFIX + "converted photoUrl", photoUrl);
photos.push(
<IonItemSliding key={image.fileName}>
<IonItem
data-name={image.fileName}
data-path={photoUrl}
onClick={onClickSelectPhoto}
>
<IonAvatar slot="start">
{photoUrl && <img src={photoUrl} alt="my pic" />}
</IonAvatar>
<IonLabel>{image.fileName}</IonLabel>
</IonItem>
<IonItemOptions side="end">
<IonItemOption onClick={() => {}}>Delete</IonItemOption>
</IonItemOptions>
</IonItemSliding>
);
}
console.log(LOG_PREFIX + "setPhotoItems");
setPhotoItems(photos);
}
});
const onClickSelectPhoto = (
e: React.MouseEvent<HTMLIonItemElement, MouseEvent>
) => {
const fileName = e.currentTarget.getAttribute("data-name");
const photoUrl = e.currentTarget.getAttribute("data-path");
setCurrentFileName(fileName || "");
setCurrentPhotoUrl(photoUrl || "");
toggleModalPic();
};
const toggleModalPic = () => {
setShowModal(!showModal);
};
return (
<IonPage>
<IonHeader>
<IonToolbar>
<IonTitle>Photos</IonTitle>
</IonToolbar>
</IonHeader>
<IonContent>
<IonGrid>
<IonRow>
<IonCol size-md="6" offset-md="3" className="ion-text-center">
<IonList>{photoItems}</IonList>
</IonCol>
</IonRow>
</IonGrid>
<IonModal id="img-modal" isOpen={showModal}>
<IonItem className="ion-align-self-end">
<IonButton onClick={toggleModalPic}>close</IonButton>
</IonItem>
<IonCard>
<IonCardHeader>
<IonCardTitle>{currentFileName}</IonCardTitle>
</IonCardHeader>
<IonCardContent>
<img src={currentPhotoUrl} alt="current img" />
</IonCardContent>
</IonCard>
</IonModal>
</IonContent>
</IonPage>
);
};
export default Photos;
view raw Photos with modal hosted with ❤ by GitHub

Near the top of the Photos functional component you’ll see we have several new state objects currentPhotoUrl, currentFileName, and showModal. These will be used by the IonModal component to display the appropriate information or to toggle the modal window. Now within the useIonViewWillEnter hook we’ve updated the IonItem component to include the attributes data-name and data-path and the onClick event handler. If you look at the onClickSelectPhoto handler you see that we are using those data attributes to set the state objects we created earlier and opening the modal. If you look all the way to the bottom of the Photos file you’ll see the IonModal and related elements. Notice I am using one of the CSS utility classes for alignment and I also have a stylesheet, Photos.css imported, which sets the — height CSS variable of the modal.

#img-modal {--height: 30em;}

Once the modal is opened it uses the properties of the IonItem to display the appropriate file name and photo. Here’s a look at the open modal.

Great now we’re almost done. Let’s create the delete button handler so that users can delete unwanted images. In your Photos.tsx file find the IonItemOption onClick event and replace the dummy function with the name onClickDelete. Let’s also give this component the data-name attribute and pass it the image.fileName. Now In order to create the onClickDelete function we’ll also need to refactor the code inside of useIonViewWillEnter as we need to use that code also in our onClickDelete function. Let me show you the completed code and I’ll go through it.

import React, { useState } from "react";
import {
IonContent,
IonGrid,
IonRow,
IonCol,
IonPage,
IonHeader,
IonToolbar,
IonTitle,
IonList,
IonLabel,
useIonViewWillEnter,
IonItemSliding,
IonItem,
IonItemOptions,
IonItemOption,
IonAvatar,
IonModal,
IonCard,
IonCardHeader,
IonCardTitle,
IonCardContent,
IonButton
} from "@ionic/react";
import { useStorage } from "@ionic/react-hooks/storage";
import { FilesystemDirectory, Capacitor } from "@capacitor/core";
import { useFilesystem } from "@ionic/react-hooks/filesystem";
import "./Photos.css";
export const LOG_PREFIX = "[Photos] ";
const Photos: React.FC = () => {
const [currentPhotoUrl, setCurrentPhotoUrl] = useState("");
const [currentFileName, setCurrentFileName] = useState("");
const [showModal, setShowModal] = useState(false);
const [photoItems, setPhotoItems] = useState<JSX.Element[] | null>(null);
const { getUri } = useFilesystem();
const { get, set } = useStorage();
useIonViewWillEnter(async () => {
setIonListItems();
});
const setIonListItems = async () => {
const imgPaths = await get("imagePaths");
const images = imgPaths ? JSON.parse(imgPaths) : null;
console.log(LOG_PREFIX + "images", images);
if (images && images.length > 0) {
const photos: JSX.Element[] = [];
for await (let image of images) {
console.log(LOG_PREFIX + "checking if file exists", image.fileName);
const finalPhotoUri = await getUri({
directory: FilesystemDirectory.Documents,
path: image.fileName
});
console.log(LOG_PREFIX + "image.fileName", image.fileName);
console.log(LOG_PREFIX + "finalPhotoUri", finalPhotoUri);
const photoUrl = Capacitor.convertFileSrc(finalPhotoUri.uri);
console.log(LOG_PREFIX + "converted photoUrl", photoUrl);
photos.push(
<IonItemSliding key={image.fileName}>
<IonItem
data-name={image.fileName}
data-path={photoUrl}
onClick={onClickSelectPhoto}
>
<IonAvatar slot="start">
{photoUrl && <img src={photoUrl} alt="my pic" />}
</IonAvatar>
<IonLabel>{image.fileName}</IonLabel>
</IonItem>
<IonItemOptions side="end">
<IonItemOption onClick={onClickDelete} data-name={image.fileName}>
Delete
</IonItemOption>
</IonItemOptions>
</IonItemSliding>
);
}
console.log(LOG_PREFIX + "setPhotoItems");
setPhotoItems(photos);
}
};
const onClickDelete = async (
e: React.MouseEvent<HTMLIonItemOptionElement, MouseEvent>
) => {
const fileName = e.currentTarget.getAttribute("data-name");
const imgPaths = await get("imagePaths");
const images = imgPaths ? JSON.parse(imgPaths) : null;
const newImgPaths = [];
if (imgPaths) {
for (let i = 0; i < images.length; i++) {
const img = images[i];
if (img.fileName !== fileName) {
newImgPaths.push({
fileName: img.fileName
});
}
}
}
const newImagePaths = JSON.stringify(newImgPaths);
await set("imagePaths", newImagePaths);
setIonListItems();
};
const onClickSelectPhoto = (
e: React.MouseEvent<HTMLIonItemElement, MouseEvent>
) => {
const fileName = e.currentTarget.getAttribute("data-name");
const photoUrl = e.currentTarget.getAttribute("data-path");
setCurrentFileName(fileName || "");
setCurrentPhotoUrl(photoUrl || "");
toggleModalPic();
};
const toggleModalPic = () => {
setShowModal(!showModal);
};
return (
<IonPage>
<IonHeader>
<IonToolbar>
<IonTitle>Photos</IonTitle>
</IonToolbar>
</IonHeader>
<IonContent>
<IonGrid>
<IonRow>
<IonCol size-md="6" offset-md="3" className="ion-text-center">
<IonList>{photoItems}</IonList>
</IonCol>
</IonRow>
</IonGrid>
<IonModal id="img-modal" isOpen={showModal}>
<IonItem className="ion-align-self-end">
<IonButton onClick={toggleModalPic}>close</IonButton>
</IonItem>
<IonCard>
<IonCardHeader>
<IonCardTitle>{currentFileName}</IonCardTitle>
</IonCardHeader>
<IonCardContent>
<img src={currentPhotoUrl} alt="current img" />
</IonCardContent>
</IonCard>
</IonModal>
</IonContent>
</IonPage>
);
};
export default Photos;
view raw Photos with delete hosted with ❤ by GitHub

So as shown previously we are using local storage on the device in order to store our file names. The code that was retrieving our file names was initially in the useIonViewWillEnter hook. We have now moved it into a new function called setIonListItems. Now inside of onClickDelete we add some code that retrieves the list of file names, but then creates a new list that does not include the selected file to delete. This new file name list is then once again saved into local storage and then again our setIonListItems function is called which resets the IonItem list of files. Try it out yourself on your device.


That’s it a small but functional photo app that works across iOS, Android, and the Web! I’ve had good results with Ionic so I’m going to update my own application, DzHaven, to use Ionic so I can put it in the app store. I might write about it here as well. Here’s the updated code for this series, https://github.com/dharric/MyPhotos. I hope you try Ionic on your next project. Good luck.

Build Mobile iOS, Android, and Web Apps Using React and Ionic, Part III

Write Once Run Anywhere Is Alive And Well In Modern JavaScript Development

This image has an empty alt attribute; its file name is f29af-1b7uupxvb_hrtgfj6eqkleq.png

This is Part III in a series. Part II added camera functionality onto our app. Part I began by introducing Ionic and setting up our project. Next we’ll continue building out our app by learning about the storage API in Capacitor.

We left off in the previous story having created the camera screen and being able to display the last photo taken. Let’s improve the looks of the Camera screen by adding a Card. Open the Camera.tsx file from the previous project and find the beginning of the JSX return statement. In our current project it should be line 39. Let’s update the file with the following code.

import React, { useCallback, useEffect } from "react";
import { CameraResultType } from "@capacitor/core";
import { useCamera, availableFeatures } from "@ionic/react-hooks/camera";
import {
IonButton,
IonContent,
IonHeader,
IonToolbar,
IonTitle,
IonPage,
IonText,
IonCard,
IonCardHeader,
IonCardContent,
IonCardTitle,
IonItemDivider
} from "@ionic/react";
const Camera: React.FC = () => {
const { photo, getPhoto } = useCamera();
const triggerCamera = useCallback(async () => {
if (availableFeatures.getPhoto) {
await getPhoto({
quality: 100,
allowEditing: false,
resultType: CameraResultType.Uri
});
}
}, [getPhoto]);
useEffect(() => {
console.log("photo", photo);
}, [photo]);
const onClick = () => {
triggerCamera();
};
if (availableFeatures.getPhoto) {
return (
<IonPage>
<IonHeader>
<IonToolbar>
<IonTitle>Camera</IonTitle>
</IonToolbar>
</IonHeader>
<IonContent>
<IonCard>
<IonItemDivider>
<IonCardHeader>
<IonCardTitle>My Photo</IonCardTitle>
</IonCardHeader>
</IonItemDivider>
<IonCardContent>
{photo && <img src={photo.webPath} alt="my pic" />}
<IonButton style={{ marginTop: ".75em" }} onClick={onClick}>
camera
</IonButton>
</IonCardContent>
</IonCard>
</IonContent>
</IonPage>
);
}
return (
<IonPage>
<IonContent>
<IonText>No Camera Available</IonText>
</IonContent>
</IonPage>
);
};
export default Camera;
view raw Camera-3 hosted with ❤ by GitHub

As you can see the buttons now live inside an IonCard component. Inside this component are also the header and content components. Here’s a couple of things to note about components in Ionic. All Ionic components can have two kinds of properties: code properties and CSS properties (same thing as CSS variables). As the name implies code properties can be set within JSX as React props to change style or behavior. CSS properties are variables that have been created in order to change styling of a component. As mentioned previously Ionic components are an aggregation of Web Components and DOM elements. Therefore changing a CSS style on an Ionic component will not necessarily have the desired effect. Let’s see then how we can control styling of components within Ionic. In the Camera.tsx file there is a component called IonCardTitle. Let’s try changing its color to orange. First go to the theme folder and open the variables.css file. Add this style to the file.

ion-card-title {
--color: orange;
}

Run ionic serve and once the server finishes recompiling you should see that the font color of the component has changed to orange. I’ll clarify what’s happening here. The name of the Web Component is ion-card-title. This is the real name of the component in Stencil and IonCardTitle is the JSX name used only in React. Several CSS variables are associated to this component during its definition. One of those variables is — color. Therefore if we select the ion-card-title component, within our CSS file, and reset the — color property any ion-card-title component will have its color changed. Let’s erase that style and continue.

Now when we take a picture we should see a slightly nicer interface.

So changing only a trivial amount of code we already have a better looking screen. Now let’s save each picture’s name into local storage and move the pics into the app’s directory.

As mentioned in my previous story, Part II, access to native capabilities is given by Capacitor. So we will use Capacitor via Hooks to access local storage. Install the package below, if you did not last time.

npm i @ionic/react-hooks

This package acts as a wrapper to use hooks syntax to access Capacitor plugins. Now that’s installed, let’s modify the Camera.tsx file again to manage the taken image file. First let’s add moment to give us easier date and time formatting.

npm i moment

We’re using moment to dynamically create unique file names, so we have no name clashes when saving file paths later. If you’re not familiar with moment don’t worry as we only use the formatter. To clarify, the meaning of the format “MM-DD-YY-h-mm-ss” is month, day, year, hour, minute, and seconds. And we use that as our file name. Now replace the useEffect function with this code

import React, { useCallback, useEffect } from "react";
import {
CameraResultType,
FilesystemDirectory,
Plugins,
Capacitor
} from "@capacitor/core";
import { useCamera, availableFeatures } from "@ionic/react-hooks/camera";
import { useStorage } from "@ionic/react-hooks/storage";
import moment from "moment";
import {
IonButton,
IonContent,
IonHeader,
IonToolbar,
IonTitle,
IonPage,
IonText,
IonCard,
IonCardHeader,
IonCardContent,
IonCardTitle,
IonItemDivider
} from "@ionic/react";
const Camera: React.FC = () => {
const { Filesystem } = Plugins;
const { photo, getPhoto } = useCamera();
const { get, set } = useStorage();
const triggerCamera = useCallback(async () => {
if (availableFeatures.getPhoto) {
await getPhoto({
quality: 100,
allowEditing: false,
resultType: CameraResultType.Uri
});
}
}, [getPhoto]);
useEffect(() => {
if (photo) {
console.log("photo", photo);
Filesystem.readFile({
path: photo ? photo.path || photo.webPath || "" : ""
})
.then(photoInTemp => {
let date = moment().format("MM-DD-YY-h-mm-ss");
let fileName = date + ".jpg";
console.log("fileName", fileName);
// copy file into local filesystem, as temp will eventually be deleted
Filesystem.writeFile({
data: photoInTemp.data,
path: fileName,
directory: FilesystemDirectory.Data
}).then(() => {
// now we try to read the file
Filesystem.getUri({
directory: FilesystemDirectory.Data,
path: fileName
}).then(async finalPhotoUri => {
console.log("file uri", finalPhotoUri.uri);
const photoUrl = Capacitor.convertFileSrc(finalPhotoUri.uri);
console.log("final photo url", photoUrl);
const existingImageNames = await get("imageNames");
let imageNames = existingImageNames || "";
imageNames = imageNames + "," + fileName;
await set("imageNames", imageNames);
console.log("test image name saved", await get("imageNames"));
const imagePaths = await get("imagePaths");
const imagePathsArray = imagePaths ? JSON.parse(imagePaths) : [];
imagePathsArray.push({
fileName,
photoUrl
});
const imagePathsArrayString = JSON.stringify(imagePathsArray);
await set("imagePaths", imagePathsArrayString);
console.log("test image paths saved", await get("imagePaths"));
});
});
})
.catch(err => {
console.log(err);
});
}
}, [photo, Filesystem]);
const onClick = () => {
triggerCamera();
};
if (availableFeatures.getPhoto) {
return (
<IonPage>
<IonHeader>
<IonToolbar>
<IonTitle>Camera</IonTitle>
</IonToolbar>
</IonHeader>
<IonContent>
<IonCard>
<IonItemDivider>
<IonCardHeader>
<IonCardTitle>My Photo</IonCardTitle>
</IonCardHeader>
</IonItemDivider>
<IonCardContent>
{photo && <img src={photo.webPath} alt="my pic" />}
<IonButton style={{ marginTop: ".75em" }} onClick={onClick}>
camera
</IonButton>
</IonCardContent>
</IonCard>
</IonContent>
</IonPage>
);
}
return (
<IonPage>
<IonContent>
<IonText>No Camera Available</IonText>
</IonContent>
</IonPage>
);
};
export default Camera;
view raw Camera-3-storage hosted with ❤ by GitHub

When a photo is taken the photo immediately gets placed into temporary storage, which may be deleted at any time. Since we want to keep our pictures we need to move them into the app’s own directory, which is what we’re doing here. Filesystem.readFile takes the file from temp and grabs it. Then Filesystem.writeFile takes that file and writes it into the directory of the app. After that Filesystem.getUri gets a system file path for the image. Once that system file path is gotten we use Capacitor.convertFileSrc to change the path into a url style file path since the img tags that we are using cannot read file system paths. Let’s try this Filesystem code on an iPhone. You’ll have to run the following commands to get the updated code into XCode

ionic build
ionic cap copy ios
ionic cap sync ios
ionic cap open ios

Again these commands are needed because Ionic apps are pure web apps. So we use these command line tools to compile our web code and bring it into our native coding environment, in this case XCode, so that it can be converted into a native app. Now compile and restart the app on your iPhone and take a new picture. You should see the following in the XCode logs.

You can see the file name, the uri file system path, and a url path. Now let’s save our file name and path into local storage so that we can use it again to display in our second screen as a list. I’ve updated the Camera.tsx file and yours should now look like this.

import React, { useCallback, useEffect } from "react";
import {
CameraResultType,
FilesystemDirectory,
Plugins,
Capacitor
} from "@capacitor/core";
import { useCamera, availableFeatures } from "@ionic/react-hooks/camera";
import { useStorage } from "@ionic/react-hooks/storage";
import moment from "moment";
import {
IonButton,
IonContent,
IonHeader,
IonToolbar,
IonTitle,
IonPage,
IonText,
IonCard,
IonCardHeader,
IonCardContent,
IonCardTitle,
IonItemDivider
} from "@ionic/react";
const Camera: React.FC = () => {
const { Filesystem } = Plugins;
const { photo, getPhoto } = useCamera();
const { get, set } = useStorage();
const triggerCamera = useCallback(async () => {
if (availableFeatures.getPhoto) {
await getPhoto({
quality: 100,
allowEditing: false,
resultType: CameraResultType.Uri
});
}
}, [getPhoto]);
useEffect(() => {
if (photo) {
console.log("photo", photo);
Filesystem.readFile({
path: photo ? photo.path || photo.webPath || "" : ""
})
.then(photoInTemp => {
let date = moment().format("MM-DD-YY-h-mm-ss");
let fileName = date + ".jpg";
console.log("fileName", fileName);
// copy file into local filesystem, as temp will eventually be deleted
Filesystem.writeFile({
data: photoInTemp.data,
path: fileName,
directory: FilesystemDirectory.Data
}).then(() => {
// now we try to read the file
Filesystem.getUri({
directory: FilesystemDirectory.Data,
path: fileName
}).then(async finalPhotoUri => {
console.log("file uri", finalPhotoUri.uri);
const photoUrl = Capacitor.convertFileSrc(finalPhotoUri.uri);
console.log("final photo url", photoUrl);
const existingImageNames = await get("imageNames");
let imageNames = existingImageNames || "";
imageNames = imageNames + "," + fileName;
await set("imageNames", imageNames);
console.log("test image name saved", await get("imageNames"));
const imagePaths = await get("imagePaths");
const imagePathsArray = imagePaths ? JSON.parse(imagePaths) : [];
imagePathsArray.push({
fileName,
photoUrl
});
const imagePathsArrayString = JSON.stringify(imagePathsArray);
await set("imagePaths", imagePathsArrayString);
console.log("test image paths saved", await get("imagePaths"));
});
});
})
.catch(err => {
console.log(err);
});
}
}, [photo, Filesystem]);
const onClick = () => {
triggerCamera();
};
if (availableFeatures.getPhoto) {
return (
<IonPage>
<IonHeader>
<IonToolbar>
<IonTitle>Camera</IonTitle>
</IonToolbar>
</IonHeader>
<IonContent>
<IonCard>
<IonItemDivider>
<IonCardHeader>
<IonCardTitle>My Photo</IonCardTitle>
</IonCardHeader>
</IonItemDivider>
<IonCardContent>
{photo && <img src={photo.webPath} alt="my pic" />}
<IonButton style={{ marginTop: ".75em" }} onClick={onClick}>
camera
</IonButton>
</IonCardContent>
</IonCard>
</IonContent>
</IonPage>
);
}
return (
<IonPage>
<IonContent>
<IonText>No Camera Available</IonText>
</IonContent>
</IonPage>
);
};
export default Camera;
view raw Camera-3-storage hosted with ❤ by GitHub

Towards the top you should notice there is a new hook that’s imported, @ionic/react-hooks/storage. This sub directory gives us the useStorage hook, which will grant us the functions to save and get data from local storage. So we update the useEffect function again, and you can see we use get and set calls to retrieve and update our data. Note that this data is only of type string. Therefore if it’s complex data that we have, we need to put it in as JSON string. Once again save, build, and sync these code changes with XCode, with the commands I just showed above, and then let’s run our app again. If you take a picture and look at the log you should see the file’s name logged and an array with just this one file path. Then if you take another picture you should see the second name and then the array grow to two items. And finally let’s shutdown the app and start it up again to see if our data saves across app loads. Now go ahead and take another picture. This time the log should show the latest file name and then an updated paths array with three pictures in it, including the new file. Here’s what I got in my log from this test.

OK I’m going to end it here. In the next story we’ll develop the second tab which will list out thumbnails of our images and allow us to select each one in order to get a full size view. Here’s the updated code.

Build Mobile iOS, Android, and Web Apps Using React and Ionic, Part II

Write Once Run Anywhere Is Alive And Well In Modern JavaScript Development

This is Part II in a series. Part I described what Ionic is and how to setup a starter project. In this second article I’ll demonstrate how similar Ionic development is to React. And we’ll continue to build out our camera app.

In developing this story I decided to use a photo application, as it demonstrates the hardware interface capabilities of Ionic. Note to keep things simple I’ll always refer to Ionic related components as Ionic, even when the capability may be coming from Stencil or Capacitor. When a distinction would be helpful I’ll be more specific (if you’re not sure what I’m referring to, please read Part I of my story). So let’s layout what we’ll build in some detail before we begin coding. Our app, MyPhotos, will do what most camera focused applications do: allow users to take pictures with the device camera and then display those pictures in a list. For navigation, we’ll use tabs. We’ll have one tab link to a screen that provides our camera access. And we’ll have another tab that uses a list to show thumbnail views of all our app images. We won’t be building a Photos killer, but this exercise should be good enough to help you get a feel for Ionic mobile development on iOS devices. Note I’m skipping Android to make the series a little shorter, but it’s also a fully supported platform.

Getting Started With Code

In order to begin coding with Ionic we need to know what assets are available and how writing code with Ionic actually works. Let’s start doing this by opening our MyPhotos project from Part I and taking a look at the contents of src.

If you open the App.tsx file again you’ll see that our chosen style of UI navigation is tabs (I won’t show the file again as it’s very long and I’ve already displayed it in Part I). Having said that, the actual routing to screens is done with React Router’s Route tags. The components you see like IonReactRouter and IonRouterOutlet are just wrappers to allow Ionic access to React Router services. Now as you look through the folders you’ll see pages and theme. The React components inside pages represent screens on the phone, and those screens are displayed whenever the relevant tab element has been pressed. You might be wondering how url routing ties into a locally running mobile app, but there’s actually no contradiction here. You’ll recall React Router uses virtual routing, that is the routes are not actually existing on a server. They are local to the application and allow the React app to trigger screen loads based upon those url routes. So the Ionic team realized there was no reason to throw out React Router just to do a mobile app. Everything still just works.

Now let’s look at one of the pages files. In the App.tsx file on the first Route tag we see the url “/tab1”, which is associated to the Tab1 component inside the pages folder. Let’s open that file and take a look.

import {
IonCard,
IonCardContent,
IonCardHeader,
IonCardSubtitle,
IonCardTitle,
IonContent,
IonHeader,
IonIcon,
IonItem,
IonLabel,
IonList,
IonListHeader,
IonPage,
IonTitle,
IonToolbar
} from '@ionic/react';
import { book, build, colorFill, grid } from 'ionicons/icons';
import React from 'react';
import './Tab1.css';
const Tab1: React.FC = () => {
return (
<IonPage>
<IonHeader>
<IonToolbar>
<IonTitle>Tab One</IonTitle>
</IonToolbar>
</IonHeader>
<IonContent>
<IonCard className="welcome-card">
<img src="/assets/shapes.svg" alt="" />
<IonCardHeader>
<IonCardSubtitle>Get Started</IonCardSubtitle>
<IonCardTitle>Welcome to Ionic</IonCardTitle>
</IonCardHeader>
<IonCardContent>
<p>
Now that your app has been created, you'll want to start building out features and
components. Check out some of the resources below for next steps.
</p>
</IonCardContent>
</IonCard>
<IonList lines="none">
<IonListHeader>
<IonLabel>Resources</IonLabel>
</IonListHeader>
<IonItem href="https://ionicframework.com/docs/&quot; target="_blank">
<IonIcon slot="start" color="medium" icon={book} />
<IonLabel>Ionic Documentation</IonLabel>
</IonItem>
<IonItem href="https://ionicframework.com/docs/building/scaffolding&quot; target="_blank">
<IonIcon slot="start" color="medium" icon={build} />
<IonLabel>Scaffold Out Your App</IonLabel>
</IonItem>
<IonItem href="https://ionicframework.com/docs/layout/structure&quot; target="_blank">
<IonIcon slot="start" color="medium" icon={grid} />
<IonLabel>Change Your App Layout</IonLabel>
</IonItem>
<IonItem href="https://ionicframework.com/docs/theming/basics&quot; target="_blank">
<IonIcon slot="start" color="medium" icon={colorFill} />
<IonLabel>Theme Your App</IonLabel>
</IonItem>
</IonList>
</IonContent>
</IonPage>
);
};
export default Tab1;
view raw Tab1 hosted with ❤ by GitHub

The Tab1 component represents the first screen that loads when you run the command ionic serve to start the app. Let’s go through the code. As stated previously Hooks is supported out of the box so the component is a functional one. Next in the JSX we can see that IonPage is the root container. Every component in the pages folder should start with an IonPage component at its root. Next we have the IonHeader this obviously represents a container for the very top header of the screen. If you have a header that repeats, you can of course put this code into your own header React component and reuse it. Just below IonHeader is the IonContent component; as the name implies this represents the body of your screen. You should have only one IonContent per screen. This component mostly controls scroll related events in the body, for example ionScrollStart, ionScroll, ionScrollEnd — the names are pretty self explanatory of what they do. Note all standard HTML tags that React supports are still supported. But by using the Ionic components you are getting theme-able, pre-built, great looking controls, with lot’s of additional functionality for free. In other words an Ionic React project, is still just a React project. It’s just that in addition to standard React features Ionic components and services have been added. Take a look at all the control choices here. OK, we’re now almost ready to make changes to code but let’s also take a look at the theme folder and styling.

The theme folder contains a file called variables.css. It is an easy to access container for all of the theme related colors that Ionic uses in its component styles. Modifying this file allows you to quickly change the color scheme of your app from a single location (in a later post I’ll get into how to use this file to create a dark theme for your application). The syntax may seem strange, but attributes like “— ion-color-primary” are actually variables, much like the variables inside SASS files. This capability is a newer feature in standard CSS and allows you to define a variable, for example a color, and reuse it throughout your app. It even works inside your JSX style attributes. After defining a variable, you use it by calling var. For example

color: var(--ion-color-primary);

Placing this attribute inside your CSS or style property allows you to reference the variable’s true value by just passing the name. So instead of using #0cd1e8 use a meaningful variable name instead.

In addition to CSS variables Ionic components have a consistent set of properties that can be used as shortcuts to quickly change the style of an element. For example most Ionic components have a property called color. The value of color can be a string that represents one of the variables from the variables.css file. However instead of using the full variable name you use just the last word. So for example if you use

<IonButton color=”secondary”>My Button</IonButton>

your button will receive the color of the variable “ — ion-color-secondary”. Obviously writing that short property is better than doing something like

<IonButton style={{ color: '#1ed7f82' }}></IonButton>

Not only for length, but also readability. Additionally Ionic components are usually aggregate HTML and Web Component elements. So setting for example the style color will not necessarily have the desired effect, because you might be changing the color of a different element than the one you intended. So again, setting the property of the Ionic component is a shorter way of getting the desired style change. Let’s hook into this Ionic capability by creating our own color variable that we will use for emphasizing certain elements. Open variables.css and add the following

--ion-color-accent: #69bb7b;--ion-color-accent-rgb: 105, 187, 123;--ion-color-accent-contrast: #ffffff;--ion-color-accent-contrast-rgb: 255, 255, 255;--ion-color-accent-shade: #5ca56c;--ion-color-accent-tint: #78c288;

Now we need to create a new CSS file in order to add a class that maps to our newly created variables.css entry. So create a file called base.css inside of the theme folder and add

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

We needed to create this class in order for Ionic to recognize a new CSS variable as an Ionic component property. Now let’s go back to the App.tsx file and add our base.css import so that all components can get this class. Type under the variables.css import

import './theme/base.css';

Now let’s open Tab1.tsx and try resetting all the color properties of the bottom IonIcon entries to use “accent”. It will look like this

The project should auto compile and then your screen should have the new color

As you can see the icons under Resources have changed to pink.

Creating Our First Screen

Let’s start by adding a new file called Camera.tsx and write that from scratch. But before we do this we’ll need to install two more packages. The first is pwa-elements. This package is a Web Components package built with Stencil. It provides the interface for the Camera, and other device services, but only for browsers. On mobile devices you will get the native camera interface. The next package is react-hooks. This package exposes much of Capacitors capabilities as hooks. So we will be able to access the camera by using hooks.

npm i @ionic/pwa-elements @ionic/react-hooks

After installing these two packages open the index.ts file and update like below

import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import * as serviceWorker from './serviceWorker';
import { defineCustomElements } from '@ionic/pwa-elements/loader';
ReactDOM.render(<App />, document.getElementById('root'));
defineCustomElements(window);
// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: https://bit.ly/CRA-PWA
serviceWorker.unregister();
view raw index.ts hosted with ❤ by GitHub

From pwa-elements we are importing defineCustomElements and then wrapping the window object so that calls can be intercepted. Again, this means at runtime on the browser this will cause any calls to the camera or other supported services to have an Ionic UI presentation. If you don’t do this you will get an error like below, when running in the browser, as the desired interface will not exist.

Next let’s create the Camera.tsx file if we have not done so already. For now just create an empty Camera function that is exported. We’ll fill it in later. Now let’s update the tab element inside of Apps.tsx to point to our new Camera.tsx file. We’ll start from the top of the file. Since Ionic uses the ionicons library for its own icons, we need to update the import statement using ‘ionicons/icons’ to replace the flash icon with the camera icon. Next go down to the first Route element and replace the path property with “/camera” and the component property with Camera. We’ll associate this route to the tab later. Next update the last Route element and change the Redirect to property to be “/camera”, so that the camera screen loads by default. And then on the first IonTabButton let’s change the tab property to be “camera” and href to be “/camera”. Since Ionic has integrated React Router to their own controls this change allows a click on this tab to trigger the “/camera” route. Next change IonIcon’s icon property to be “camera” and IonLabel’s text to be Camera. Once complete you should see this code.

import React from 'react';
import { Redirect, Route } from 'react-router-dom';
import {
IonApp,
IonIcon,
IonLabel,
IonRouterOutlet,
IonTabBar,
IonTabButton,
IonTabs
} from '@ionic/react';
import { IonReactRouter } from '@ionic/react-router';
import { apps, camera, send } from 'ionicons/icons';
import Tab2 from './pages/Tab2';
import Tab3 from './pages/Tab3';
import Details from './pages/Details';
/* 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 Camera from './pages/Camera';
const App: React.FC = () => (
<IonApp>
<IonReactRouter>
<IonTabs>
<IonRouterOutlet>
<Route path="/camera" component={Camera} exact={true} />
<Route path="/tab2" component={Tab2} exact={true} />
<Route path="/tab2/details" component={Details} />
<Route path="/tab3" component={Tab3} />
<Route path="/" render={() => <Redirect to="/camera" />} exact={true} />
</IonRouterOutlet>
<IonTabBar slot="bottom">
<IonTabButton tab="camera" href="/camera">
<IonIcon icon={camera} />
<IonLabel>Camera</IonLabel>
</IonTabButton>
<IonTabButton tab="tab2" href="/tab2">
<IonIcon icon={apps} />
<IonLabel>Tab Two</IonLabel>
</IonTabButton>
<IonTabButton tab="tab3" href="/tab3">
<IonIcon icon={send} />
<IonLabel>Tab Three</IonLabel>
</IonTabButton>
</IonTabBar>
</IonTabs>
</IonReactRouter>
</IonApp>
);
export default App;
view raw Camera App.tsx hosted with ❤ by GitHub

Now let’s start updating the Camera file too. Since this article is getting a bit long let’s just get the camera up and running and in the next part we’ll fix the styling. Add this code into the Camera.tsx file.

import React, { useCallback, useEffect } from 'react';
import { CameraResultType } from '@capacitor/core';
import { useCamera, availableFeatures } from '@ionic/react-hooks/camera';
import { IonButton, IonContent, IonHeader, IonToolbar, IonTitle, IonPage, IonText } from '@ionic/react';
const Camera: React.FC = () => {
const { photo, getPhoto } = useCamera();
const triggerCamera = useCallback(async () => {
if(availableFeatures.getPhoto) {
await getPhoto({
quality: 100,
allowEditing: false,
resultType: CameraResultType.Uri
});
}
}, [getPhoto]);
useEffect(() => {
console.log('photo', photo);
}, [photo]);
const onClick = () => {
triggerCamera();
}
if(availableFeatures.getPhoto) {
return (
<IonPage>
<IonHeader>
<IonToolbar>
<IonTitle>Camera</IonTitle>
</IonToolbar>
</IonHeader>
<IonContent>
{photo && <img src={photo.webPath} alt="my pic" />}
<IonButton onClick={onClick}>
camera
</IonButton>
</IonContent>
</IonPage>
);
}
return (
<IonPage>
<IonContent>
<IonText>No Camera Available</IonText>
</IonContent>
</IonPage>
);
}
export default Camera;
view raw Camera.tsx hosted with ❤ by GitHub

Starting from the top of the file it shows we need two new imports: @capacitor/core and @ionic/react-hooks/camera. Capacitor provides the core services to interface into hardware and react-hooks converts those calls into hooks style calls. If you have not enabled Capacitor yet you’ll need to do so first.

ionic integrations enable Capacitor
ionic cap add ios

Moving further down you’ll notice the call to useCamera and the returned properties: photo and getPhoto. GetPhoto initializes the camera instead of retrieving the taken image, which confused me a bit but nevertheless that’s what it does. The photo object is the object that will receive the image data once the actual picture is taken. Depending on the options given to getPhoto it will have the images data and path information or a base64 encoded string representing the image. After getPhoto you can see we call useCallback to memoize the actual call to getPhoto. The resultant triggerCamera call is what is used to trigger the actual initialization of the camera later in the code. After the useCallback call, you can see there is a test for the availability of the getPhoto function, availableFeatures.getPhoto. In the @ionic/react-hooks component there is a separate folder for each device service. In our case, as is shown, we are using the camera. In each service folder there is an availableFeatures call that allows you to test if the device has that capability or not. Since we are working cross platform and across devices, having this sort of testing ability is a good thing. Next the actual call to getPhoto shows that it is passed some parameters when called. The most important of which is the resultType, which we will give the CameraResultType.Uri parameter. Using the Uri type gives us a property called webPath, which we can use on our img tags since html img elements only recognize url strings and not file paths. After that you can see some fairly obvious code like useEffect, which is doing logging of what’s inside the photo object, and the camera button click handler, which again is only initializing the camera. But after this code is the JSX which contains an IonPage root container, another header and of course the IonContent, which has the button and an img tag that will receive the picture once clicked. Now that we’ve set everything up let’s do a run of the app. First we need to build the app again and make sure everything compiles so type the following

ionic build
ionic cap copy ios
ionic cap sync ios
ionic cap open ios

The commands above do the following. First we build the app, with ionic build. Next we copy the updated source files over to XCode. Then we also need to bring over any iOS specific plugins so that Capacitor can connect our web view to the relevant device services. We use sync for that. And finally we open the project. After executing these commands try and run the iOS XCode project on your device (I’ve already gone over how to setup the XCode project in Part I). Here’s a picture of the final screen with an incredible photo of my desk lamp!

The End (for Part II anyway)

Some of you who are not familiar with native coding might find the process a little arduous. Not only did we write code and add imports that were not necessarily obvious. We also had to execute command line calls to make sure that the native project build was always in sync with the web build, which also means we’ll have to do that for every platform we intend to support. However let’s think about the end result here. Without writing a lick of native code. That by the way, we would have otherwise had to write 3 or more times (iOS, Android, Desktop, Web). We managed to get access to the camera across device platforms with only a single code base and using Hooks coding style to boot. To me this is quite amazing.

This is the end of Part II. Source code can be found here, https://github.com/dharric/MyPhotos. In the next section we’ll make our camera screen a little prettier using Cards and some alignment of the image. And we’ll learn about the useStorage hook so that we can save our photos locally and then list them on our second tab with thumbnails.

Build Mobile iOS, Android, and Web Apps Using React and Ionic, Part I

Write Once Run Anywhere Is Alive And Well In Modern JavaScript Development

This article will be the first in a series introducing Ionic 4. And showing how you can keep your existing skill set for the web, to create attractive and fully functional mobile applications.

Originally, I had planned to use React Native for my mobile app. I’m a React developer so it seemed reasonable to assume it would be what I needed. But one of the things that annoyed me right away was the syntax. It was quite different from React for the web. In addition I found some frameworks that I liked don’t work in React Native. Things like standard CSS and React Router are not used in React Native development. So after trying it out I started shopping around for something better. I think Ionic is what I was looking for.


Some of you React devs might be thinking Ionic is an odd choice. Isn’t that the framework for Angular? So I’ll give a quick overview of what Ionic is, in its current v4 iteration. The latest version is a complete rewrite of the framework. It’s actually more like three distinct components than a single framework. At its core is something called Stencil. According to Ionic documentation Stencil is a compiler that emits standard Web Components. But it is also its own coding framework that allows creation of custom Web Components; with no dependency on external frameworks like React or Angular. This decision allows Stencil to be framework agnostic. Next up is the piece known as Ionic. Ionic React, Ionic Angular, Ionic Vue are basically wrappers around Stencil that allow you to code in your desired app framework and still emit the same Ionic components. Think of it like how TypeScript eventually gets converted into native JavaScript. So when using Ionic React you can continue to use SASS, React Router, Redux, etc without issue. It even supports Hooks! And then finally we have Capacitor. Capacitor is a runtime that allows the web view, that your app lives in, to communicate with your device API and access hardware and services (it also does the work of attaching your dev build to the native iOS or Android build system, XCode or Android Studio).


OK so all this sounds cool but what’s the big deal? What this all means is that now you can write your iOS, Android, Web or even Desktop App entirely using a web framework of your choosing. So if you are a React dev you can keep using your favorite npm packages and coding techniques to build mobile apps that look and feel just like native apps. No quasi CSS like object syntax; no HTML wanna-be tags. Just standard React in all its glory. So let’s get a feel for what Ionic development is like by building a small photo app and deploying onto an iPhone. First let’s install Ionic globally.

npm i -g ionic

Once you have Ionic installed globally we can use it like create-react-app to setup our initialized application. Let’s create our app by calling Ionic cli without any parameters.

ionic start MyPhotos
Select React as web framework

As the screenshot shows, the first thing to do is select which web framework you will be using to build your app (note Vue is currently still in beta). Let’s select React and then go to the next step. This step allows us to select which starter template to use to build our app. To allow for simple navigation in our app let’s select tabs.

Select tabs as template

Finally after hitting enter Ionic should start setting up your project. It has to download quite a few packages so this can take a while. Once it completes cd into the MyPhotos directory and type

ionic serve

You should see the app load in your browser, and if you use chrome’s debugger and switch to mobile mode you should see the below.

Tabs navigation based Ionic template

As you can see the built in template is quite attractive and has a theme fairly similar to other native iOS apps. Now let’s take a look at the contents of the MyPhotos directory and see how Ionic has laid out this project.

The layout is a pretty standard React-like folder structure. You have a src folder, index and App files, an npm packages.json file, and a public build folder. Also as is clear by the file extensions Ionic uses TypeScript by default, although this can be disabled. If you open the App file you should see the following.App.tsx file

import React from 'react';
import { Redirect, Route } from 'react-router-dom';
import {
IonApp,
IonIcon,
IonLabel,
IonRouterOutlet,
IonTabBar,
IonTabButton,
IonTabs
} from '@ionic/react';
import { IonReactRouter } from '@ionic/react-router';
import { apps, flash, send } from 'ionicons/icons';
import Tab1 from './pages/Tab1';
import Tab2 from './pages/Tab2';
import Tab3 from './pages/Tab3';
import Details from './pages/Details';
/* 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';
const App: React.FC = () => (
<IonApp>
<IonReactRouter>
<IonTabs>
<IonRouterOutlet>
<Route path="/tab1" component={Tab1} exact={true} />
<Route path="/tab2" component={Tab2} exact={true} />
<Route path="/tab2/details" component={Details} />
<Route path="/tab3" component={Tab3} />
<Route path="/" render={() => <Redirect to="/tab1" />} exact={true} />
</IonRouterOutlet>
<IonTabBar slot="bottom">
<IonTabButton tab="tab1" href="/tab1">
<IonIcon icon={flash} />
<IonLabel>Tab One</IonLabel>
</IonTabButton>
<IonTabButton tab="tab2" href="/tab2">
<IonIcon icon={apps} />
<IonLabel>Tab Two</IonLabel>
</IonTabButton>
<IonTabButton tab="tab3" href="/tab3">
<IonIcon icon={send} />
<IonLabel>Tab Three</IonLabel>
</IonTabButton>
</IonTabBar>
</IonTabs>
</IonReactRouter>
</IonApp>
);
export default App;
view raw Ionic Tabs App file hosted with ❤ by GitHub

Again Ionic fully supports hooks without additional updating. If you look at the code you notice the JSX starts with IonApp at the root. Every Ionic app must have the IonApp component at its base. After that you can see the IonReactRouter component. This is a wrapper around React Router that Ionic has created to allow for better integration with their framework, it’s comparable to the BrowserRouter. After that you can see the IonTabs component which will host the individual tabs involved in navigation. And finally you should see the IonRouterOutlet. This component is roughly equal to the Switch component in React Router.

Now that we have a feel for a basic Ionic app I need to explain how we go from this web app to an iOS or Android app. So as previously mentioned Ionic at its core uses only web technologies. There is a clean separation of concerns between Ionic the web framework and the native mobile pieces. So then the question is how do we build an app that works, on iPhone and Android, like a native app? This is where Capacitor comes in. Capacitor is the glue that ties your web app to native services. It wraps your code, so that it can function as a native app. Capacitor also provides the integration to our native development environment, for example XCode on Mac, so that our web app will be compiled into a native binary and can hook into all the native device capabilities that we want.

Note in order to build a native binary we need a computer running XCode. Unfortunately Apple does not provide XCode on non Mac computers so you’ll need a Mac to build your iOS app. However, I do want to be clear on this point, you can develop your app on any platform that allows for React web development. Only when it comes time to compile into an iOS binary and test hardware services will you need a Mac computer. Android fortunately does not have this limitation.

Let’s setup Capacitor now. First we start by enabling Capacitor in our existing application. Type

ionic integrations enable Capacitor

When it’s complete you should see something like this.

Now that we’ve enabled Capacitor in our app we’ll need to add the iOS integration. But before we do that make sure you’ve built your app at least once since Capacitor needs the build folder to exist in order to setup properly. Now type

ionic cap add ios

If you get asked to install the Capacitor cli, enter yes as you’ll need it to run commands. Once this setup completes we can open our iOS app in XCode. Type

ionic cap open ios

Once your XCode project opens you will need to attach your iPhone via usb and then select it in XCode as the device to use for testing (it is also possible to use their emulators, but final testing should always be done on a real device). Now once all this is done and you try and run your project from XCode you will see this error.

This error is indicating Apple’s requirement to associate a developer account with an application. Go back to your file explorer view on XCode and click on the root App icon. You should see this

As you can see on the darker right side of the screenshot, there are multiple tabs and you end up on the General tab first. Click on the Signing & Capabilities tab and you should see this.

On this tab you should see the Team drop down. Select it and choose your desired dev account. Obviously if you don’t have one you’ll have to create it. Once you select the proper team account it will automatically update your Signing Certificate and you’re ready to start your app!

Make sure your iPhone is connected and selected within XCode as the test device. Then click the play button on the upper left corner of XCode. You should see the app auto start up on your iPhone. It will look just like the web app that we started earlier in the web browser.


Great we built and started our first iOS mobile application using Ionic. This was quite a bit of reading and configuration so I’ll end Part I here. In the next post I’ll show how to update the tab navigation and start modifying the components to create our photo app screen.

Part II has been released.

As always if you like helping other devs try DzHaven.com

Introduction to the Apollo GraphQL React Hooks Library

A quick start guide to migrate from react-apollo-hooks to the official React hooks library provided by Apollo

This post is an introduction to @apollo/react-hooks. It is not intended to be exhaustive. However, since the syntax is quite similar to react-apollo-hooks, it should be enough to get you started.

Getting Started

I have a react hooks app called DzHaven that I built entirely using React Hooks, no classes and no HOC. However, at the time @apollo/react-hooks was not available, so I used Daniel Trojanowski’s excellent react-apollo-hooks library for all my client-side code.

It works great, but one thing that always bothered me was the fact that the useQuery calls would run at declaration — instead of the exact moment I wanted. So as I go through my experience I’ll show you how to avoid unnecessary execution of queries, as well as some other useful tidbits.

If you are moving from react-apollo-hooks, the first thing you should know is that you do not need to do the migration in one shot.

react-apollo-hooks and @apollo/react-hooks can live together in the same client app. I did my migration running both providers and I did not see any issues. I’ve included my index.tsx file below with all of my imports, and as you can see, both providers are there and work without issue.

import React from "react";
import ReactDOM from "react-dom";
import { BrowserRouter } from "react-router-dom";
import App from "./App";
import configureStore from "./shared/store/configureStore";
import registerServiceWorker from "./registerServiceWorker";
import { Provider } from "react-redux";
import ErrorBoundary from "./shared/components/ErrorBoundary";
import Modal from "react-modal";
import ApolloClient, { InMemoryCache } from "apollo-boost";
import { ApolloProvider } from "@apollo/react-hooks";
import { ApolloProvider as ApolloHooksProvider } from "react-apollo-hooks";
import "bootstrap/dist/css/bootstrap.min.css";
import "./index.scss";
const apolloClient = new ApolloClient({
uri: process.env.REACT_APP_SERVER_URL,
credentials: "include",
cache: new InMemoryCache()
});
const store = configureStore();
ReactDOM.render(
<Provider store={store}>
<BrowserRouter>
<ApolloProvider client={apolloClient}>
<ApolloHooksProvider client={apolloClient}>
<ErrorBoundary>{[<App key="App" />]}</ErrorBoundary>
</ApolloHooksProvider>
</ApolloProvider>
</BrowserRouter>
</Provider>,
document.getElementById("root") as HTMLElement
);
Modal.setAppElement("#root");
registerServiceWorker();
view raw index.tsx hosted with ❤ by GitHub

The next thing you need to know is which packages are required for your particular setup. If you plan on supporting only hook functions and will not use the old class-style components and HOC, you can install just the @apollo/react-hooks package.

However, If you need to support all three, you’ll have to install the full react-apollo package. Or if you need either the HOC or older style React components you’ll need to install @apollo/react-hoc or @apollo/react-components respectively.

You should note that if you use only the @apollo/react-hooks as opposed to the full react-apollo package, your bundle size will drop to just 5.1kb from 10.6kb.

So then if you have the bare bones hooks setup, your npm install command would be like below. As shown in the previous code sample, apollo-boost provides the ApolloClient, the InMemoryCache, and some other items to help set up the React client and connect to your Apollo server. I would compare it roughly to using create-react-app in terms of it being a pre-packaged library providing most of what you need to set up, without extra effort.

npm install @apollo/react-hooks apollo-boost

Some Examples

Let’s start with queries. The Apollo library contains the same useQuery hook as the react-apollo-hooks library. They’re called identically so I won’t go over that here. However, there is a new hook called useLazyQuery, which allows delayed execution of your query. Let’s take a look at the code below.

const [
execGetThreadData,
{ called, data: getThreadData, error, loading, refetch }
] = useLazyQuery(GetThread);
useEffect(() => {
if (called && loading) {
log("thread is loading");
} else if (error) {
log(error);
} else if (!called && !getThreadData && id) {
execGetThreadData({
variables: { id, incViewCount: true }
});
} else if (called && getThreadData) {
if (thread == getThreadData.getThread) {
refetch({ id, incViewCount: true });
} else {
setThread(getThreadData.getThread);
}
}
}, [getThreadData, id, userProfile]);
view raw useLazyQuery.tsx hosted with ❤ by GitHub

Starting from the top we can see that the output of the query is a bit different. The function execGetThreadData is defined first and this is the method used to call your query when you desire to do so. The name given is of course up to you, but I like prefixing each query caller with “exec”.

After that, you can see multiple properties similar to what existed before, but with the addition of the called property. Basically this property prevents unwanted calls from being accidentally made.

Now if we look at useEffect, starting on line 6, we can see that it looks quite familiar to what we would have done before. However starting on line 11, we can see that we check the called property to make sure that a call on this query was not done yet and then make the call execGetThreadData. This is a minor difference but something to be aware of. Continuing on, I will say that most scenarios will not include the code starting on line 16. However, I wanted to show that refetching data is still possible using the same syntax when required.

For useMutation, the syntax is a bit different. As shown below:

const [execPostThread] = useMutation<any, any>(PostThreadMutation, {
refetchQueries: [
{
query: QueryMe
}
]
});
useEffect(() => {
if (askToPostThread && allowPostThread) {
setAskToPostThread(false);
execPostThread({
variables: {
threadId: localThread !== null ? localThread.id : "0",
userId: userProfile ? userProfile!.id : "0",
title,
body,
videoUrl,
bounty,
categoryId: selectedCategory!.id
}
})
.then(({ data }) => {
if (data.postThread && data.postThread.id > 0) {
const dataPostThread = data.postThread;
let message = "Thread posted successfully";
if (postBtnLabel === "Edit") {
message = "Thread edited successfully";
}
setValidationSuccessMsg(message);
setLocalThread(dataPostThread);
setTitle(dataPostThread.title);
setBody(dataPostThread.body);
setBounty(dataPostThread.bounty);
setPostBtnLabel("Edit");
if (window.location.href.includes("/postthread")) {
setThreadUrl("/thread/" + dataPostThread.id);
} else {
setThreadUrl("");
}
}
})
.catch(err => {
setSummaryValidationErrors([cleanGraphQlError(err.message)]);
});
}
}, [askToPostThread, allowPostThread]);
view raw useMutation.tsx hosted with ❤ by GitHub

Starting from the top again you can see a function property called execPostThread. Later on inside of useEffect I call this function to execute a mutation. On line 2 I wanted to show that refetchQueries is still supported with the same syntax as react-apollo-hooks. Starting on line 13, I am using the older promises style syntax because the call is from within a useEffect, which doesn’t allow awaiting. However if one is able to await the call then the syntax looks familiar again.

const { data, errors } = await execPostThread({
variables: {
threadId: localThread !== null ? localThread.id : "0",
userId: userProfile ? userProfile!.id : "0",
title,
body,
videoUrl,
bounty,
categoryId: selectedCategory!.id
}
});
if(errors && errors.length) {
// do something about error
} else {
// success
}
view raw useMutation2.tsx hosted with ❤ by GitHub

Obviously there’s much more than these features in the library but I hope this quick introduction will show you that getting started with @apollo/react-hooks should not be a massive rewrite of your code if you’ve been using hooks already. Having said that, testing has changed somewhat from the way I was doing it with react-apollo-hooks. Let me know if you would like to see a write up on that topic. Hope this helps.

If you’re a typical dev you probably write code for multiple platforms. If you want to use GraphQL on SwiftUI try this article.

As always if you like helping other devs try DzHaven.com

Sharing Code In a Large JavaScript Project

Trials and tribulations sharing code in JavaScript

I built a fairly large app, called DzHaven, using Node and React, and I found myself with the issue that a lot of JavaScript developers find themselves in — difficulty reusing code. I have a fair amount of experience using C#, and it’s pretty easy to create a module through a separate project and then reference that assembly for code reuse. Obviously you can’t do that with JavaScript. Let’s see what is possible.

Attempt 1: Shared Local Project Folder

This seemed like a no brainer. Just create a folder where anything that can be shared among separate projects can live. Then I’ll just use a relative path to that folder from each project and that should do it.

I think you already know this did not work. All projects these days, Node or otherwise, are going to be NPM packages. These packages have a folder structures and configuration files expecting certain paths from within the project itself. It becomes exceedingly difficult and arduous to deal with relative sub-paths from each project. Something like this gets unwieldy very quickly.

{someroot}/../../../{some sub project}

Additionally a larger project can have multiple NPM projects to deal with, where each project has its own set of unique dependency versions that may not be compatible with each other. So whether it’s language differences between JavaScript versions or NPM package versions, things may not play nicely with all projects.

On top of all this, if the folder is moved or renamed, then everything of course stops working.

Attempt 2: Shared NPM Package

Oh I know I’ll just get a subscription to NPM and create private Node packages. The nice thing about this method is that you get a shared location to store your packages and have some control over versioning too.

Unfortunately, this does not work for the same reason as the first attempt. Since this is also an NPM package, the versions of certain NPM packages inside may conflict with other project dependencies. Additionally, there is the added annoyance of having to compile the project after any changes.

Advertisements

Attempt 3: Locally Shared Git Project Folders

Let me try and explain what this is. Most devs place their code into Git as a code repository for their project. Well, it’s also possible to create a shared folder as a repository in Git and clone it anywhere locally.

This means you could clone the shared project into the src folder of any parent project that may need its code. And using the .gitignore file, one could have this folder ignored by the parent project — so that it is not checked in when the parent is. Now if you have two projects you could clone your shared project as a subfolder locally into each project’s src folder. Something like this,

Project A/src/common and Project B/src/common.

Since the folder common is not an NPM package, you are free to use any package versions you like, as long as you’ve not placed any version-specific code into the folder (but that would defeat the purpose of using a shared code location now wouldn’t it). You are also free to use different versions of Typescript, which is what I’m doing. Also, since the folder exists inside of the parent project’s src folder there is no relative path hell to deal with.

Additionally, if a change is required for the common folder, that change can be made, pushed to Git, and then the other project can pull the change down. And vice versa. Or you can wait to pull on one of the projects until it has updated its own dependencies and can accept the changes.

This method is not perfect, as I cannot use projects that depend on NPM packages. But for my project, DzHaven, it’s a small problem that has not caused any major headaches. Hope this helps!

Advertisements

This site runs on wordpress

%d bloggers like this: