← All posts about JavaScript

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


Chafik Gharbi Web and mobile app developer interested in everything related to JavaScript Follow me on Twitter

© 2021 Chafik Gharbi