Strategies, tips, and gotchas for migrating a cross-platform mobile app from Cordova to React Native
Apache Cordova is a Web view centric cross-platform mobile development platform that interacts with native APIs via plugins that expose functionality via JavaScript. It features a core set of plugins that allow access to commonly used device features, such as the GPS, camera, and files. There is a vast number of other plugins (both proprietary and open source) that provide access to other device features, such as background notifications, or integration with third-party tools, such as Google Maps. Cordova apps are compiled and distributed just like any native app.
One of the biggest challenges with Cordova is UI development. As mentioned above, it is Web view centric, which means that the UI is built in HTML/CSS/JavaScript and runs inside of an embedded web view within the native app. The major advantage of this is the same as that of a web-app, cross-platform compatibility. You can develop a UI that will look, feel, and behave the same way on different devices from different families, running on different operating systems. Although this appears attractive, it is not without faults. For one, you may well end up facing the same challenges that any web developer faces. Your UI might look great in iOS, but for whatever reason, the CSS doesn’t work quite right in Android or Windows mobile. You can easily end up fighting with trying to get everything to look right on devices of different sizes with different resolutions and all of that. The other big drawback is performance related. You can’t expect your UI to look, feel, and perform as well as a native UI, not without great difficulty at least. There are UI frameworks available that help with this, such as Ionic, which offers a rich set of UI components that look and feel more like a native mobile app. However, it is tightly coupled with AngularJS, and if you want to use Ionic 2, that requires Angular 2. At that point, you should strongly consider moving to NativeScript, which provides deep integration with Angular 2 and is similar to React Native in that the UI is rendered as native components, rather than Web components in an embedded web view.
React Native, like Cordova, is a platform for building cross-platform mobile apps, however it differs greatly in that it is native centric. Rather than running HTML in an embedded webview, the UI is composed of React Native components, which are rendered as actual native components in the application. Let’s look at a simple example that illustrates this, the Image component.
First let’s look what’s being returned by the render function in Image.ios.js
If we look at what’s happening under the hood, we can see that RCTImageView actually extends the Objective-C class UIImageView, which is part of the native library UIKit.
And on the Android side, here is what's being returned in the render() function of Image.android.js
As you can see, the Android implementation is a bit more complex. Down at the bottom of the file we can see that RKImage is once again an RCTImageView.
And in fact, it's also a little trickier to find the native implementation. We can search the source for RCTImageView, and find it in ReactImageManager.java
Then if we trace our way back up the inheritance tree, we will find that ReactImageView extends GenericDraweeView, which can be found in another repo. This class extends DraweeView where we can finally see that deep down under the covers, it is using the native Android widget ImageView.
By being native centric, React Native eliminates many of the problems and limitations found in Cordova. For one, there is no need to find an additional framework that makes the UI look and feel like a native UI. You can simply use and style the React Native components as you see fit. Like Cordova, React Native features a core set of commonly used features, but unlike Cordova, this includes UI components, for things such as lists, maps, and modal views. Device features are accessed through a core set of APIs. Just like Cordova, there is a wide variety of open source and proprietary plugins and components available. You can usually find a plugin, module, or component that suits your needs. If not, you can write your own native code and integrate it as a NativeModule. This is similar to writing your own Cordova plugin, but it's a little easier and more intuitive, I would say, since there is no need to write your own bridging API in JavaScript.
There are a couple of drawbacks to consider when using React Native. For one, there is a learning curve to React JS. There is more to it than just JavaScript. However, I feel that in the context of React Native for mobile apps vs React JS for web apps, it's actually a little bit easer to get the hang of. But the biggest drawback, especially when comparing to Cordova, is cross-platform compatibility. Many external components and even core components are only available for one platform. You might end up using different components to achieve the same thing on different platforms. Fortunately, it is easy to write your code in a way that provides different implementations for different platforms. At the moment, iOS and Android are fully supported out of the box. Windows Universal Binary support is available via the react-native-windows plugin. Though I have not personally tried it, this seems like a good tutorial on getting started with React Native for Windows.
If you're currently using Cordova, you may find that as your app evolves and things become more complex, you keep running into some of the drawbacks mentioned above. You really wish you could rewrite it as a native app on multiple platforms, but have neither the time nor resources to do so. Maybe no one on your team knows Objective-C, Swift, or Java. Well, now you have options to deliver a truly native app without writing in the native language. React Native is one of those options, and so let's finally get down to it and talk about how to migrate a mobile app from Cordova to React Native!
For this, we are going to use the SuperfitTracker app, found here:
https://github.com/objectpartners/cordova-to-react-native
Preparing for Migration
Deconstruct the UI and identify its functional components
Try not to think about the implementation of your existing UI, but rather, break it down into functional components. This is a dropdown list of items, this is a date picker that formats the date a certain way, this is a modal dialog that displays information when the user clicks on this button. Focus on the function of each UI component and try to forget about the fact that it's a jQuery Mobile UI date picker, or an Ionic popup modal. Whatever it is, it will be replaced with React Native components.
Behold the UI of the Cordova SuperfitTracker app.
Now there are a couple UI features not obvious in the screenshot:
1. When you tap on the chart, a marker appears on the map at the coordinate (lat/long) for the corresponding distance along the x-axis of the chart
2. When the record button is pressed, a text label below the button indicates the status of the recording (whether it is currently recording or stopped)
For now, we only care about the functionality of the UI. We don't need to concern ourselves with the fact that the map shown is actually using the Google Maps SDK or that the chart is drawn using D3. We only care that it's a map with a line and a marker on it, and it's a chart with a single line plotting speed over distance.
Now that we understand the functional aspects of the UI, we can identify suitable React Native components to use in our React Native app. Below you will see some components in camelCase and others with hyphenated names. The former are built-in core React Native components and the latter are external, from the open source community. Let's map the functional components of the UI to these React Native components.
Navigation Bar => react-native-scrollable-tab-view
Activities List => ListView, Navigator
Activity Detail
- Map => react-native-maps
- Interactive Chart => TBD: react-native-svg, react-native-chart, WebView with D3 and react-native-webview-bridge
Record Activity
- Dropdown => react-native-picker
- Record Button => TouchableHighlight
I made my choices by evaluating the core components and comparing them with what I could find elsewhere. React Native has a Picker component, but the external react-native-picker component seems much easier to use and will get the job done without too much fuss. React Native also has a MapView component, but it is implemented for iOS only. I suspect it has to do with the fact that an API key is required to use Google Maps, even natively on Android. In any case, the react-native-maps plugin will do the trick, though it does require a Google Maps API key on the Android side. The iOS implementation uses the built-in Apple Maps. I decided to use react-native-scrollable-tab-view for the navigation because, although it is feature rich component, the features can be turned off and then it becomes a simple tab-based mechanism for navigating through the app. For the activities list, I added the built-in Navigator component, because the Cordova app doesn't have a way to get back to the activity list, other than clicking on "Activities" in the nav bar. In this case, in an app this simple, it's a relatively easy feature to implement, so I decided to add it.
Decisions, decisions...
Sometimes, there are no obvious solutions and further discussion and evaluation is warranted so that a decision can be made. Consider the chart. Drawing good charts on a mobile device is not always easy, unless you want to pay for it. Here is where we need to consider the current implementation in the Cordova app, which is D3 written in plain old JavaScript, rendered in a div within an HTML page. Because D3 requires a DOM, this makes things a little more complicated. We could use a React Native WebView, which would allow us to use D3 and render the chart on an HTML page. There are a couple of problems with this. For one, communication between a WebView and native components is not well supported, though it is possible through the react-native-webview-bridge plugin. If we were simply sending data to the WebView and drawing a chart, this would be an acceptable solution, even though we're not using the native graphics libraries to draw the chart. However, this chart is interactive, and the touch (or click) handlers would need to call back to the native components, which could get very messy. And besides, we want to keep things native and forget about those embedded web views.
The react-native-svg plugin uses native graphics libraries to render SVGs, and it is very comprehensive. It has similar capabilities to D3. And in fact, you need to build your chart as a composition of SVG nodes, but you are given React Native components to do so, which does make life easier. However, this will be very time consuming and we would even need to build things like the axes and tick marks by hand. Which begs the question, is this chart complex enough to even warrant the use of something as robust as D3 in the first place? Probably not.
Going back to the idea of not worrying about the implementation and instead thinking of only the functional components, all we need is a simple line chart that can be clicked on. There is a module that gets very close to what we need, react-native-chart. It draws simple line, bar, and pie charts using native graphics libraries. Bar charts have the ability to handle touches, but line charts do not. After examining the source code for the bar charts, it seemed fairly trivial to add touch handling capabilities to a line chart. In the end, I made the decision to fork this module and give line charts that capability. The tricky part would be writing the touch handler function, which is passed into the component.
The fork is here: https://github.com/SteveConnelly/react-native-chart
Identify APIs and external dependencies
Now is the time to actually consider the implementation of our existing Cordova app and figure out what APIs and dependencies we are going to need in our React Native app. Again, let's take the approach of mapping the functional components of the app to what we need.
Navigation Bar => No additional APIs needed
Activities List => react-native-sqlite-storage, react-native-swipeout (for deletes)
Activity Detail => No additional APIs needed
Record Activity => react-native-location (iOS only), React Native Geolocation API for other platforms
I considered the current Cordova implementation for each of these functional components when making my choices. The Cordova application uses SQLite to store data via a plugin, and in fact react-native-sqlite-storage is the React Native port of that very plugin. Score! This means that we should be able to take all of our persistence code and pull it straight over, as is.
We need a delete button. The Cordova app's implementation for this is clickable text with "[X]" as the delete button, and it offers no warning if you accidentally touch it. This is pretty lame. If I'm making a native app, I want it to have commonly seen features of a native app, like swiping left to delete. The react-native-swipeout component is just the ticket. This is another component that has a ton of features, but it's easy enough to only have it expose a delete button on swipe left, so I made the choice to go ahead and make this improvement to the app.
Finally we come to the record activity feature. UI wise, it's just a dumb ugly red button, that I take full responsibility for creating. Behind the scenes though, when that button is pressed, magic happens and your activity is tracked via the device's GPS. The Cordova app uses the core plugin cordova-plugin-geolocation, and its API is based on the W3 API Spec for Geolocation. That is important to note, because it is something to consider as we evaluate our options. There is a built-in Geolocation API in React Native that also is based on this same API Spec. So that will work nicely, as we can port our code straight over with little worry about having to modify it. However, like the Cordova plugin, this API does not allow for background usage of the device's location services. This kinda sucks for us, because it means that the user has to have the app open the entire time during the activity. It's time to make another improvement to the app. There are a few options out there to choose from. The one that looks the most attractive is react-native-background-geolocation. It seems very robust and would meet our needs, but the Android version is not free. That is a problem, and I once again wonder if needing Google API Keys has anything to do with it. Futhermore, the implementation seems to be different from the W3 Geolocation spec, so I decided to look elsewhere. react-native-location seems to fit the bill. It is a simple NativeModule that looks easy enough to use, and even though it doesn't follow the W3 spec, the location object seems to at least conform to the W3 spec of the Position interface, which is helpful to know. Because of this, we can reuse the function handlers for positions received from our Cordova app. But alas, no Android implementation. It's iOS only. I searched for other options that can do background location tracking on Android, but came up empty. Ultimately, I made the decision to end up with an improved iOS version that does background tracking, and a non-improved Android version that does not. However, the code will be written in such a way that if a good solution for Android is found in the future, it can easily be added.
Identify reusable code
This is the part that everyone is always concerned about when someone brings up the idea of migrating to a different platform or different tech stack. Can we reuse all of our code? Well, in this instance, the answer is no. You are not going to reuse that HTML, CSS, or JavaScript that interacts with the DOM. There is no spoon. And there is no DOM. You are not going to cheat by using the WebView component. What happens when we cheat, even with the intention that we'll fix it later? It never happens. It stays there in place, for all eternity. We all know this. If we're going to bother with going through a migration, we should do it right. Don't half ass it. Once again, be prepared to let go of your amazing Web-based UI creations.
There is good news though. We can reuse a lot of our existing code. Any well-encapsulated code that deals with business logic, like services, or code that deals with persistence, or authentication with an external server, or third-party API calls, can all be reused. So long as it isn't intertwined with the UI, we can use it. And if it is intertwined with the UI, there may be partial pieces that we can reuse or even complete pieces that we can reuse with slight modifications.
Consider the following function that populates the list of Activities in the Cordova app.
Pretty amazing right? Building HTML and click handlers on the fly from query results. It's just awesome. So now what do we do? There's so much UI stuff happening here. Answer? Get rid of it and never speak of it again!
Here's our implementation of the same function in React Native.
Now that our UI functionality is being handled by different React Native components, we end up having simpler JavaScript code. Here, the results of the query are simply put into an array and the dataSource that gets updated belongs to the ListView component which embodies the list of activities. And when that happens, the list on the screen is updated.
And the click and swipe handlers are defined in the renderRow function.
As for any custom Cordova plugins you've written, they can be left largely intact, though you will need to expose their functionality a little differently. The good news is that you can throw away the bridging code, including all of those cordova.exec statements.
Other things to be wary of during migration
Plugin configuration can be a bit painful at times. Some of the things you may encounter are:
iOS
- Manually linking libraries
- Adding or modifying entries in the plist file
- Enabling app capabilities
Android
- Adding dependencies to build configuration
- Adding permissions to Android Manifest
- Adding Google API keys for certain features
- Registering packages in the MainApplication class
I suggest that you keep your platform specific code in source control so that only one of your team members has to live through the pain.
Sharing code across platforms
You will want to share as much code between platforms as possible. There's an index.ios.js file and an index.ios.android file, and these are the entry points for your application on those respective platforms. Try to make them the same if you can, and simply import one component, from a file that is shared by both platforms. Like this:
In this case, both index.ios.js and index.android.js look exactly like what you see above, and the SuperfitTracker module is in ./app/index.js.
As discussed earlier, the iOS app gets the benefit of having background GPS location tracking capabilities. Fortunately we can use the same functions to handle the location response from the react-native-location NativeModule (iOS) and the React Native Geolocation API. All we need to do is invoke the correct API when starting and stopping the recording of an activity. We can do this with the help of the React Native Platform module. This example is from the RecordActivity component.
This is the only place in the SuperfitTracker app where there is any platform specific code. It's not that scary.
Now let's have a look at the final product!
Final thoughts on migrating from Cordova to React Native
You can end up with some messy files after the migration. You will end up creating new components that you hadn't thought about before migrating. It makes sense to refactor and to put each component into its own file.
Be sure to test your React Native app on all platforms. Be patient with Android.
And finally,
Migrating from Cordova to React Native is manageable if you...
- Break the UI down into components
- Identify the code that you can reuse
- Research and identify suitable plugins and components for the functionality that you need
- Have a little patience and a good attitude <img src="https://s.w.org/images/core/emoji/72x72/1f642.png" alt="