Photo de Tim Gouw sur Unsplash

How to build an Android app, an iOS app and a web app with React Native

Axel de Sainte Marie

--

One of the dilemma most of the development’s teams will face is to be able to reduce delays and costs while building an app that would run on mobile devices and web browsers.

As a mobile developer, I’ve always been interested in finding a solution to be able to use the code I write for my mobile apps, to build a web app. By curiosity, I’ve been looking around for solutions without concrete project to apply them. So all my work on that subject was pure theory. Until Lili’s project happened…

The Lili’s project

Soon after Lili’s mobile app first version development’s end — it was published on Septembre 22, 2020 — we wanted to build a webapp either.

Lili’s mobile app’s code was written with React Native, so our first thought was to write the web app with ReactJS. But that still means having 2 different code sources. At the same time, Flutter was growing fast. As we wanted to be sure to choose the best solution, we decided to study a bit Flutter. Flutter’s demos shows apps running perfectly on all devices, so it was worth the shot to try it. The best way to study Flutter, was to build a project with it. So we did our first beta version of Lili’s webapp with Flutter.

To be objective, we realized that both solutions had their pros and cons. Flutter was a good solution but not the “miracle” we secretly expected to find. Regarding those facts, there was one major argument in favor of React Native, which was our experience with it. If talking only about myself, I had, at this time, 3 years of history with React Native versus 3 months in Flutter. So we knew we had to choose the React environment.

Once that choice made, we wanted to find a solution to be able to share as much code as possible between mobile and web. And while poking around, looking for solutions, I found the following interview on Microsoft Developer Youtube channel driven by Christopher Harrison. He talked with Robin Heinze about the use of React Native Web with mobile and web app.

Robin Heinze explaining how she handled code sharing between mobile and web projects using React Native Web

What triggered my attention during this talk was the way Robin Heinze managed to share code between React Native and ReactJS project using symbolic links. And that small detail was the spark needed to fire up our new version which would be written in React Native for mobile and web.

So now let’s talk about the way we are building our app for iOS, Android and Web browsers.

Let’s build a project for iOS, Android and Web browsers

So let’s create a small project to demonstrate the way we create Lili’s apps.

Here is a pre-requisite for apps I build. For mobile navigation, I like to use React-Native-Navigation from Wix. Coming from the “Native development world”, it is really handy for me to work with it. You can find back the agility of native navigation. Right now, React-Native-Navigation is compatible with React-Native 0.71.11, so we will build our project with this version. As the Wix’s team is doing a great job maintening it, I’m sure the next release compatible with 0.72 is just around the corner !

So first, let’s build the mobile app.

mkdir demo_app_web
cd demo_app_web
npx react-native@0.71.11 init onthebeach_mobile_app - version 0.71.11

So this will create my mobile app with React Native version 0.71.11. Once create, we can of course launch it with the following command.

cd onthebeach_mobile_app
yarn ios
yarn android
iOS Default React Native app

Before building the web app, let’s change a bit our mobile app. I will create a splashscreen with OnTheBeach.dev’s colors.

So first let’s create my mobile project structure, that will be more relevant once the web project will be created.

I will have a folder called “app”, at the root of my project. Inside this folder I will have a folder called “shared” and an other one called “specific”. As you will understand, all our code that will be shared between our mobile app and our web app will be in the first folder.

I am already working in the folder “onthebeach_mobile_app”, so let’s create the folders I described before, and the code folders. I will also create the Splashscreen.tsx file that will handle my Splashscreen’s code.

mkdir app
cd app
mkdir shared
mkdir specific
cd shared
mkdir components
cd components
mkdir Splashscreen
cd Splashscreen
touch Splashscreen.tsx

Let’s edit the Splashscreen.tsx file with a background color, a title, a logo and a footer displaying the version on the app.

import React from "react";

import {
Dimensions,
Image,
Platform,
Text,
View
} from "react-native";

interface SplashscrenProps {

}

const Splashscreen = (props: SplashscrenProps) => {

return (
<View style={{flex:1, justifyContent:"center", alignItems:"center", backgroundColor:"#F56565"}}>
<Image source={{uri:"https://onthebeach.dev/images/OtbLogo.png"}} style={{width:100, height:100}} resizeMode="contain" />
<Text style={{ color:"#FFF", fontSize:28, marginTop:10 }}>{"OnTheBeach.dev"}</Text>
<View style={{position:"absolute", bottom:0, width:Dimensions.get("window").width, alignItems:"center"}}>
<Text style={{ color:"#FFF", fontSize:14, paddingBottom:10 }}>{"Version "+Platform.OS+" - Demo"}</Text>
</View>
</View>
)

}

export default Splashscreen;

And here is our Splashscreen !

OnTheBeach.dev Splashscreen

As you can see the code Platform.OS return the value ios. This will allow us to confirm that the code is working well once on the web version.

Now is the time to create our web app. As I said at the beginning of this post, I tried different things that did not work well. Then I saw Robin Heinze’s demo, and I just copy her way of doing it. I will write it done below, but all the credits of this goes to her.

So as she does, we will not create a React app with react-cli or other tools. We will just create the files and install the libraries we need. We will start back in our route folder demo_app_web.

cd ..
mkdir onthebeach_web_app
cd onthebeach_web_app
touch index.html index.tsx tsconfig.json

So this will create the main files of our project. Then, let’s set up our project.

yarn init

This command will ask you to fill information, about your project to create your package.json file with it. Here are the one I gave four our example. (If you never done that before, know that when you leave the answer empty, it will take the default value in the brackets if there is one).

[question name (onthebeach_web_app):
[question version (1.0.0):
[question description: OnTheBeach Web App written with React Native
[question entry point (index.js):index.tsx
[question repository url:
[question author: Axel de Sainte Marie
[question license (MIT):
[question private: true

Then let’s install the dependencies.

yarn add -D parcel-bundler typescript

A lot of you may use WebPack usually. I discovered Parcel with this project, and it just works out of the box. Way simpler than WebPack.

yarn add react react-dom

So now that everything is configured, let’s edit our files for our project to work.

Let’s start with index.html

<html>
<body>
<div id="root"></div>
<script src="./index.tsx"></script>
</body>
</html>

Then index.tsx

import React from "react";
import { render } from "react-dom";

const App = () => {
return (
<div>
<p>This is my web app build with React</p>
</div>
)
}

render(<App />, document.getElementById("root"));

and tsconfig.json

{
"compilerOptions": {
"jsx": "react",
"lib": ["es6", "dom"],
"target": "esnext",
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
"moduleResolution": "node",
"noEmit": true,
"skipLibCheck": true
},
"exclude": ["./node_modules"]
}

Now the content of the web app is ready. Let’s set it up to work with Parcel. This is very simple, just add the Parcel’s script to your package.json file, and you will be good to go.

{
"name": "onthebeach_web_app",
"version": "1.0.0",
"description": "OnTheBeach Web App written with React Native",
"main": "index.tsx",
"author": "Axel de Sainte Marie",
"license": "MIT",
"private": true,
"devDependencies": {
"parcel-bundler": "^1.12.5",
"typescript": "^5.1.6"
},
"dependencies": {
"@types/react-native": "^0.72.2",
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"scripts": {
"start": "parcel serve ./index.html",
"build": "parcel build ./index.html"
}
}

Now, just launch the web app

yarn start
Simple React App

Now that this part is working, let’s make the magic happen, and display our React Native splashscreen in our web app.

To be able to use React Native code inside a web app, we will use React-Native-Web. So let’s add it to our project, and let’s change a couple of things to it.

yarn add react-native-web @types/react-native

Once done, you can edit your package.json file, and add an alias for react-native.

{
"name": "onthebeach_web_app",
"version": "1.0.0",
"description": "OnTheBeach Web App written with React Native",
"main": "index.tsx",
"author": "Axel de Sainte Marie",
"license": "MIT",
"private": true,
"devDependencies": {
"parcel-bundler": "^1.12.5",
"typescript": "^5.1.6"
},
"dependencies": {
"@types/react-native": "^0.72.2",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-native-web": "^0.19.6"
},
"scripts": {
"start": "parcel serve ./index.html",
"build": "parcel build ./index.html"
},
"alias": {
"react-native": "react-native-web"
}
}

The magic happens now. Insite your web root folder (onthebeach_web_app), create a folder to store your project code. I will follow Robin Heinze’s way of doing it and called it “src”. It is very common for web project and also allow you to quickly differentiate your mobile “app” folder from your web “src” folder. Then you will create a symbolic link to access your the code written inside your mobile “app/shared” folder in your web “src” folder.

mkdir src
cd src
ln -s ../../onthebeach_mobile_app/app/shared shared

In your IDE, you should now see the “shared” folder.

Now let’s edit our “index.tsx” file so that it will call our Splashscreen‘s code.

import React from "react";
import { render } from "react-dom";
import Splashscreen from "./src/shared/components/Splashscreen/Splashscreen";

render(<Splashscreen />, document.getElementById("root"));

And here is your web app

So the good thing is that the React Native code works on the web side. But we don’t have the design we would like. In that exemple, the styling attribute “flex:1" does not fill the screen.

A first idea would be to use the height of the window to make it work. Let’s try that.

import React, { useEffect, useState } from "react";

import {
Dimensions,
Image,
Platform,
Text,
View
} from "react-native";

interface SplashscrenProps {

}

const Splashscreen = (props: SplashscrenProps) => {
return (
<View style={{flex:1, justifyContent:"center", alignItems:"center", backgroundColor:"#F56565", height:Dimensions.get("window").height}}>
<Image source={{uri:"https://onthebeach.dev/images/OtbLogo.png"}} style={{width:100, height:100}} resizeMode="contain" />
<Text style={{ color:"#FFF", fontSize:28, marginTop:10 }}>{"OnTheBeach.dev"}</Text>
<View style={{position:"absolute", bottom:0, width:Dimensions.get("window").width, alignItems:"center"}}>
<Text style={{ color:"#FFF", fontSize:14, paddingBottom:10 }}>{"Version "+Platform.OS+" - Demo"}</Text>
</View>
</View>
)

}

export default Splashscreen;

Here is the result when I hit the refresh button of the browser.

Nice, isn’t it ? Sure, but here is what happen when I increase the browser’s height.

No so nice anymore … The screen keeps the old value of the window height. A quick solution is to use percentage instead of dimensions.

import React, { useEffect, useState } from "react";

import {
Dimensions,
Image,
Platform,
Text,
View
} from "react-native";

interface SplashscrenProps {

}

const Splashscreen = (props: SplashscrenProps) => {
return (
<View style={{flex:1, justifyContent:"center", alignItems:"center", backgroundColor:"#F56565", height:"100%"}}>
<Image source={{uri:"https://onthebeach.dev/images/OtbLogo.png"}} style={{width:100, height:100}} resizeMode="contain" />
<Text style={{ color:"#FFF", fontSize:28, marginTop:10 }}>{"OnTheBeach.dev"}</Text>
<View style={{position:"absolute", bottom:0, width:Dimensions.get("window").width, alignItems:"center"}}>
<Text style={{ color:"#FFF", fontSize:14, paddingBottom:10 }}>{"Version "+Platform.OS+" - Demo"}</Text>
</View>
</View>
)

}

export default Splashscreen;

Now our screen can dynamically adjust its height.

So where can we go from here ?

Now that we have shared the code of a single page between our mobile app and our web app, we have to think about the whole app.

The main question is to know which code can be shared between both platforms, and which one has to be specific for each platform.

For each library that we will add, we have to check if it works for both mobile and web, or not. react-native-navigation is specific to mobile, so it can not be used for the web navigation. We will have navigation method in each specific folder. Whereas — for example — react-native-event-listeners is pure JS, and can be used in the shared section of the code.

Some React Native component will work on both platform, but will not be optimal for a web app. Let’s take the example of ScrollViews.

ScrollViews are used in mobile app, as a view will not naturally scroll down if its content layout height if bigger than the window height. Whereas, on the web, the scroll is automatically functional if the content go beyond the bottom the screen. If you add a vertical ScrollView in a web app, you will have two concurrents scrolls : the one naturally created and the one from the React Native ScrollView. This will cause undesired behavior. This is an exemple of behavior that have to be studied carefully while creating your views.

After a year in production, we can tell that this solution is working for Lili. We continue to improve our code, and make the web app better, as we find new tricks every day. So if you want to code a multi-platform project, and you are already familiar with React Native, give it a try, and you will soon be able to work quickly and gain time on maintaining your projects.

--

--

Axel de Sainte Marie

Show Runner de projets mobiles. Développeur React Native et passionné par les challenges du monde mobile.