Migrating Incrementally from a React App to React Native: Part 1 of 2

How to get Started and Web View Gotchas

Hugo Dozois-C.
Voo Blog
7 min readMay 9, 2016

--

Tldr: The first part is about getting started with a full project that works on iOS and Android. The second part will be about breaking down an existing React app into views and being able to do transition animations between React Native and React views.

I've been working with React for almost two years now. The ecosystem has evolved a lot since then, especially with the appearance of React Native. Starting a project in React Native feels good: the development cycle is fast, everything is pretty much pre-configured for you, including hot-reloading and ES2015 compiling. The problem is you can't always build from the ground up. In our case we have an app that is currently all made in React. We use Cordova as a wrapper to enable usage of native functionalities like push notifications, address book, etc. It gets you a long way, but at some point you end up spending a lot of time rebuilding things that exist natively. Namely: infinite scroller, transition animations, and even list views with sliding action buttons.

To fix these issues there are a few possible paths:

  • Rebuild your app from scratch in pure native code (for both iOS and Android), something which is both time consuming and means you will have to stop pushing out releases for a while.
  • Migrate to React Native directly, but this is also time consuming since the app we have right now is already working and not every piece needs to be migrated urgently.
  • Integrate your Cordova web view in a native application, then migrate piece by piece. The incremental migration here is a step in the good direction, but you have to learn native languages to move on.
  • Use React Native WebViews to "host" your React code and be able to communicate with them. This way you can do a partial migration (that is, on a per-view basis) while you move to a cross-platform native solution.

This last solution is really appealing. The WebView is a component that can be used pretty easily in React Native. There is no configuration or list of steps required to make it work; you import it, write a `<WebView />` tag and that’s it! The only problem is that it doesn't support two-way communication out of the box. That means you can't pass data back and forth between the web view context and the React Native context.

Luckily, a component called React Native WebView Bridge has been created and can be used to address that. Now that we have a solution there are two main issues left:

  1. Have the whole project set up so that we can have WebViews that will load a JavaScript bundle (both in dev & prod), be able to send and receive messages back and forth, and document all the gotchas. (Part 1)
  2. Add build tools to automate the generation of the HTML files, split an existing React app in a way that allows to migrate one view at a time and transition animations between React Native views an WebViews. (Part 2)

Building the Prototype

Lets get started! The file structure is the following:

├── components
│ └── SuperWebView.js
├── index.android.js
├── index.ios.js
├── package.json
└── webviews
├── script.js
└── view.html

Creating a Basic HTML File and Loading a Script

We need a basic WebView template in HTML. That file will load a simple JavaScript script.

<html>
<head>
<title>ReactNative WebViews iOS: Part 1</title>
<meta http-equiv="content-type" content="text/html; charset=utf-8">
<meta name="viewport" content="initial-scale=1, minimum-scale=1, maximum-scale=1, user-scalable=0, height=device-height">
<script src="script.js"></script>
</head>
<body>
<div id="message"></div>
<div>It works! Awesome</div>
</body>
</html>

Fairly simple, right? Nope, this will run just fine in the iOS simulator but won't run on Android. That brings us to the first quirk:

In iOS the script tags needs to be placed in the head to load properly. In Android they need to be place at the end of the body.

To solve this problem, we need to create two views instead of one. view.ios.html with the script tag in the head, and view.android.html with the script tag at the end of the body.

Now we need a simple script that will be able to receive and send data back over the bridge. I made a simple implementation that modifies the page when it receives message and sends another one back. You can view it here.

Loading the WebView from React Native

Now, using react-native-webview-bridge, let's put everything together and run it! This component is placed in ./components

import React, { Component } from 'react';
import { View, Text, StyleSheet } from 'react-native';
import WebViewBridge from ‘react-native-webview-bridge’;export default class SuperWebView extends Component {
constructor(...args) {
super(...args);
this.state = { text: 'Hello World' };
this.onLoad = this.onLoad.bind(this);
this.onBridgeMessage = this.onBridgeMessage.bind(this);
}
onLoad() {
setTimeout(() => this.refs.webviewbridge.sendToBridge(
prepareForBridge({ text: 'Hello From React-Native' })
), 2000);
}
onBridgeMessage(msg) {
const message = JSON.parse(msg);
this.setState({ text: message.text});
}
render() {
return (
<View style={styles.container}>
<View style={[ styles.child, styles.nativePart ]}>
<Text>Text Received: {this.state.text}</Text>
</View>
<WebViewBridge ref="webviewbridge"
style={styles.child}
onLoad={this.onLoad}
onBridgeMessage={this.onBridgeMessage}
source={require('../webviews/view.html')}
scalesPageToFit={false}
scrollEnabled={false}
javaScriptEnabled={true}
/>
</View>
);
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
},
child: {
flex: 1,
},
nativePart: {
paddingTop: 40,
},
});

One thing to notice right away is the "javaScriptEnabled={true}". The second quirk to remember is:

This is not needed at all for iOS, but for Android, in web views, JavaScript is disabled by default and needs to be enabled manually.

Other than that, the component is fairly simple. In order to show interaction, it's a simple view split in two parts, native and web view. On load, it sends a message to the web view.

If you run this in the simulator in dev mode (using react-native run-ios), it should run without a hitch. After about two seconds, you will see that the web view received a message and after two more seconds the text in the native section will update. Awesome!

Adapting for the Production Environment

Great, now I can just run it on my device and I'm ready to push to prod?

Not so fast.

If you try to run it on your device using the bundled version, you will notice that it won't work at all. That is due to the way view.html is loaded. To make it work in production, a few more steps are needed.

First, we need to add all the resources to the build in Xcode. To do that, right click the folder bundle and click "Add Files to <Project Name>"

Add files to Project

Select the webviews folder, and make sure that you are creating a folder reference, without copying the items. We want them to be linked so we can change them easily without having to re-copy every time.

Create a reference without copying

You should now see the folder listed in your bundle.

Next, we need to change the way we import the view so that it's done using a path relative to the device filesystem and a URI.

Using the NativeModules module SourceCode, we can retrieve the full script URL. From this all we have to do is retrieve the folder path. Note that since we have a different HTML file for Android and for iOS, we need two different URIs in prod. We need to explicitly load the correct file, which is view.ios.html for iOS. To achieve this goal, we create two small files prodViewUri.ios.js, and prodView.Uri.android.js. The only thing they do is export the correct file name.

// prodViewUri.ios.js
module.exports = 'webviews/view.ios.html';
// inside SuperWebView.js
import { SourceCode } from 'NativeModules';
const scriptURL = SourceCode.scriptURL;
let VIEW;
if (scriptURL.indexOf('file://') > -1) {
// In prod
// Extract the full path
const _bundleSourcePath = scriptURL.substring(7, scriptURL.lastIndexOf('/') + 1);
const name = require('./prodViewUri');
VIEW = { uri: `${_bundleSourcePath}${name}` };
} else {
// In dev
VIEW = require('../webviews/view.html');
}

And then we change the render function to use VIEW as the source:

<WebViewBridge ref=”webviewbridge”
style={styles.child}
onLoad={this.onLoad}
onBridgeMessage={this.onBridgeMessage}
source={VIEW}
// ...

Making Sure Messages Get Sent Properly

Now that we have everything working, we can just start sending messages back and forth, right?

Well, yes and no. There is one last thing to consider. To be able to send data back and forth, the only way is to inject a script with the values we want in the page. Injecting a script means the message needs to be escaped properly. There is currently an open issue about that bug https://github.com/alinz/react-native-webview-bridge/issues/80.

To work around it, all we have to do is to make sure we escape single-quotes (or just replace them for something else and replace them back from the web view).

There are plenty of small libraries we can use to do that. In my case I decided to prepare the string with a very simple function:

// SuperWebView.js
function prepareForBridge(message) {
return JSON.stringify(message).replace(/'/g, '__@@__');
}
//script.js
function parseFromBridge(message) {
return JSON.parse(message.replace(/__@@__/g, `'`));
}

Everything is now ready! We now have a fully functional example both on iOS and Android for both production and development!

File structure at the end:

├── components
│ ├── SuperWebView.js
│ ├── prodViewUri.android.js
│ └── prodViewUri.ios.js
├── index.android.js
├── index.ios.js
├── package.json
└── webviews
├── script.js
├── view.android.html
└── view.ios.html

A repository containing the full example is available here: https://github.com/dozoisch/RNWebViewMedium/tree/part1

Web View Gotchas

  1. Script tags for the web views need to be placed in the head for iOS and in the body for Android.
  2. In Android, JavaScript is disabled by default in the web views, we need to enable it with `javaScriptEnabled={true}`
  3. In prod, files needs to be included in the bundle, and need to be loaded from the filesystem. To do that we add them in Xcode and make sure we load them using a filesystem URI.
  4. Bridge injection doesn't support single-quotes well. We need to escape them or replace them.

--

--

Hugo Dozois-C.
Voo Blog

Technology enthusiast, into the React ecosystem.