Turn your website into mobile application with react native
November 30, 2020
When I was converting progressive web apps or websites to mobile apps, I was wrapping them within webview using Java if Android and Swift for IOS or Flutter for both. After discovering Expo, the task became more easier following just a few steps, as long as it’s a cross-platform, not to mention the possibility of integrating additional native functionalities into your mobile app such as push notifications, in-app purchases, AdMob, geolocation, background tasks, and many more.
If you are not a React developer or if you cannot understand the process, just download the source code from GitHub, install expo, add your website link and skip to the build section.
Getting started
First, we should’ve Node.js installed, then we run the following command to install Expo CLI globally
npm install --global expo-cli
Then create new Expo project by running
expo init my-project
As mentioned previously, we are going to wrap our website within webview using react -native-webview package
expo install react-native-webview
Note: when we install packages in expo, we use expo install instead of npm install to ensure version c*ompatibility with current version of Expo SDK. Now, in our app.js file we will import webview and add our website link (https://expo.io/ for example)
import { WebView } from "react-native-webview";
export default function App(props) {
return <WebView source={{ uri: "https://expo.io/" }} />
}
Run expo start to test the application in development, you must have an Android emulator or IOS simulator installed, otherwise you can test in your device by installing Expo client in your device
If you notice, the website overlaps the top bar of the phone, luckily React Native has a predefined component for IOS devices to display content in the safe area, in Android, we should do it manually.
import { Platform, SafeAreaView } from "react-native";
import Constants from "expo-constants";
export default function App(props) {
return <SafeAreaView
style={{
flex: 1,
backgroundColor: "#FFFFFF",
}}
>
<View
style={{
height: Platform.OS === "android" ? Constants.statusBarHeight : 0,
backgroundColor: "#FFFFFF",
}}
></View>
<WebView source={{ uri: "https://expo.io/" }} />
</SafeAreaView>
}
Not the end! Let’s make it even more suitable by providing a back event listener, a Loader and a connectivity checker
Handling back state event
When we press the return button in Android devices, React Native will not change the current state of our website, so we have to add some logic to link the back event with the back state on our website.
import {useEffect, useState, useRef} from "react";
import { Platform, BackHandler, SafeAreaView, View } from "react-native";
import { WebView } from "react-native-webview";
import Constants from "expo-constants";
export default function App(props) {
const WEBVIEW = useRef()
const [backButtonEnabled, setBackButtonEnabled] = useState(false)
// Webview navigation state change listener
function onNavigationStateChange(navState) {
setBackButtonEnabled(navState.canGoBack)
};
useEffect(() => {
// Handle back event
function backHandler() {
if (backButtonEnabled) {
WEBVIEW.current.goBack();
return true;
}
};
// Subscribe to back state vent
BackHandler.addEventListener("hardwareBackPress", backHandler);
// Unsubscribe
return () => BackHandler.removeEventListener("hardwareBackPress", backHandler);
}, [backButtonEnabled])
return <SafeAreaView
style={{
flex: 1,
backgroundColor: "#FFFFFF",
}}
>
<View
style={{
height: Platform.OS === "android" ? Constants.statusBarHeight : 0,
backgroundColor: "#FFFFFF",
}}
></View>
<WebView
ref={WEBVIEW}
onNavigationStateChange={onNavigationStateChange}
source={{ uri: "https://www.chafikgharbi.com/" }}
/>
</SafeAreaView>
}
Connectivity and loader
Let’s go even further and put some extra features like checking connectivity and adding a splash screen before the website loads.
Install netinfo package:
npm install --save @react-native-community/netinfo
Create a listener with react useEffect hook, this will show “no connection” alert if there is no internet, you can ignore this part if your website is PWA (supports offline)
const [isConnected, setConnected] = useState(true)
useEffect(() => {
// Subscribe for net state
const netInfroSubscribe = NetInfo.addEventListener((state) => {
setConnected(state.isConnected)
if (!state.isConnected) {
alert("No connection");
}
});
// Clean up
return netInfroSubscribe
}, [])
We can check if our website is loaded using the onLoad property in webview.
const [loading, setLoading] = useState(true)
// Webview content loaded
function webViewLoaded() {
setLoading(false)
};
<WebView onLoad={webViewLoaded}/>
And here is the final code:
import React, { useEffect, useState, useRef } from "react";
import {
Platform,
BackHandler,
Dimensions,
SafeAreaView,
View,
Image,
} from "react-native";
import { WebView } from "react-native-webview";
import Constants from "expo-constants";
import NetInfo from "@react-native-community/netinfo";
const BACKGROUND_COLOR = "#FFFFFF";
const DEVICE_WIDTH = Dimensions.get("window").width;
const DEVICE_HEIGHT = Dimensions.get("window").height;
const ANDROID_BAR_HEIGHT = Platform.OS === "android" ? Constants.statusBarHeight : 0;
export default function App(props) {
const WEBVIEW = useRef()
const [loading, setLoading] = useState(true)
const [backButtonEnabled, setBackButtonEnabled] = useState(false)
const [isConnected, setConnected] = useState(true)
// Webview content loaded
function webViewLoaded() {
setLoading(false)
};
// Webview navigation state change
function onNavigationStateChange(navState) {
setBackButtonEnabled(navState.canGoBack)
};
useEffect(() => {
// Handle back event
function backHandler() {
if (backButtonEnabled) {
WEBVIEW.current.goBack();
return true;
}
};
// Subscribe to back state vent
BackHandler.addEventListener("hardwareBackPress", backHandler);
// Unsubscribe
return () => BackHandler.removeEventListener("hardwareBackPress", backHandler);
}, [backButtonEnabled])
useEffect(() => {
// Subscribe for net state
const netInfroSubscribe = NetInfo.addEventListener((state) => {
setConnected(state.isConnected)
if (!state.isConnected) {
alert("No connection");
}
});
// Clean up
return netInfroSubscribe
}, [])
return (
<SafeAreaView
style={{
flex: 1,
backgroundColor: BACKGROUND_COLOR,
}}
>
<View
style={{
height: ANDROID_BAR_HEIGHT,
backgroundColor: BACKGROUND_COLOR,
}}
></View>
{(loading || !isConnected) && (
<View
style={{
backgroundColor: BACKGROUND_COLOR,
position: "absolute",
top: 0,
left: 0,
zIndex: 10,
width: DEVICE_WIDTH,
height: DEVICE_HEIGHT + ANDROID_BAR_HEIGHT,
flex: 1,
alignItems: "center",
justifyContent: "center",
}}
>
<Image source={require("./assets/icon.png")}></Image>
</View>
)}
{isConnected && (
<WebView
onLoad={webViewLoaded}
ref={WEBVIEW}
useWebKit={true}
onNavigationStateChange={onNavigationStateChange}
source={{ uri: "https://expo.io/" }}
/>
)}
</SafeAreaView>
);
}
Building the app
Before publishing our application, we have to take a look at the configuration file app.json, I already talked about best practices to configure your app in my previous article.
Do not forget to modify the app default images such as the application icon, the notification icon, and the start screen.
The app build will be generated in the cloud, so no need to use Android Studio or Xcode. The process will take a few minutes.
expo build:android -t app-bundle
You can build the apk using expo build: android -t apk but the previous command is recommended by Google in order to compress the size of the application and generate apps for different versions because the app built with expo is relatively large and this is one of its disadvantages.
If you choose to build for IOS please follow thoroughly their documentation