While working on our Devbridge TeamApp, we were confronted with a dilemma. Our backend and the iOS version were continually updated by various developers, while the Android version’s codebase couldn't keep up. The result was that iOS users had access to features that Android users were missing. We realized that with our limited internal R&D team we didn't always have the necessary bandwidth to update three platforms and started looking at a complete rewrite of the app. And this time, we were going to go multiplatform.
We weren’t thrilled with the idea of a Web-App, which would only be there to act as an icon and a loader for a web page. It doesn’t offer the same native feel as a proper native application, and it doesn’t work at all without a network connection. We also wanted to have more functionality than what a web page could offer (like Health integration for iOS, photo upload directly from camera, et cetera). It seemed a much better idea to look into frameworks that could let us write applications for the most popular mobile platforms used in our company, and around the world - iOS and Android - using the same codebase.
Xamarin vs. React Native
Since we wanted a native application, we had two contenders lined up: Xamarin and React Native. At the time of writing this article, Xamarin is a well established tech (Not to mention the recent Microsoft acquisition can only mean great things). It is included with every Visual Studio version, and allows users to develop multi-platform applications using C#. Then, we have React Native: a relatively young, emerging, open source framework from Facebook that promises a “learn once, write everywhere” experience driven by JavaScript.
After making a proof of concept with each, we noticed that Xamarin only allows you to write the same kind of code that you would see while programming natively - similar calls, similar interface design tools. React Native took a completely different path toward app development - JavaScript code, CSS-like stylesheets and all-too-familiar, HTML-like tags for layout. They are implemented as JSX transforms in the Babel JavaScript compiler, which allow XML-like syntax for element structuring. Babel also allows us to use the latest ECMAScript features, such as async-await. As we have dealt with at least a little bit of web development, this looked very familiar, so we chose React Native… Because why not!
A crash course with React Native
The project started out from the basic initial template. After a week of getting up to speed with JavaScript and another week of development, we expanded it to contain a fully working login screen that took us to a blank page.
We loved the hot reloading capability of React Native, which let us modify application code without recompiling the application itself. Code is loaded from a local server while developing, and is packaged into the application with other resources on proper release.
Another great feature was that the same generic code applied perfectly to both the iOS and Android versions. React Native works by using JavaScript code to manage native views. A “View” in React Native is “UIView” in iOS, and a native “View” in Android at runtime. The maintainers of React Native are also working hard to bring native widgets to the framework. There are some elements that are unavailable to Android but that exist in iOS, like UIPicker (which got renamed to PickerIOS), or drawer layout from Android (which was renamed to DrawerLayoutAndroid). React Native allows platform detection in code, so it’s easy to write components that are similar in functionality but adapted to the respective operating systems. This means that we can use the same JavaScript files for the Android and iOS versions, with occasional ifs peppered throughout.
Layout in React Native is implemented with CSS-like stylesheets that allow you to specify border widths and margins, colors and fonts. The nicest thing about this is that elements can be laid out using a partial implementation of flex-box layout rules. That makes arranging elements in a row, spacing them out evenly or making them stretch proportionally a breeze. Basic component hierarchy is laid out with JSX tags, and style information is added by passing “style” props to elements. The props can be dynamic and generated at runtime. The same goes for the structure by using variables in the JSX tree.
Don't “React” to it all...
However, a problem became apparent as the project got more complicated: What do you do when the functionality that you require isn’t in React Native, can’t be made with given components, or requires the use of a library that was made for native development and not for React Native? The answer is native modules. Native modules are a way of bridging native code and React Native code. A component written in native code can integrate with the usual React Native layout. We had to write native modules that supported multipart form upload and photo operations.
In the native application, React Native looks like a simple view controller, so if you want to use another view controller from a library, you can push it over React Native and then pop back from it. We used this for photo previews for which we had already made view controllers in our past projects.
Even though we didn’t have Android development expertise, we were able to implement the views and components we needed by using a bit of Google-fu, and then we were back in the React Native code.
Keeping things neat with Flux
As the complexity increased, we needed to introduce some kind of code structure. JavaScript is great for its flexibility, but it offers no enforcement of inheritance or strict typing. Facebook not only provided us with an answer to multiplatform development, but also gave structure to our code with something called Flux. Flux is an application architecture that sits neatly with regular React and React Native application development. Instead of information flowing around the components, it now flows in one direction by employing user actions that use a central dispatcher (very similar to the Grand Central Dispatch in iOS) to notify stores and carry data. This way, almost all logic can be moved away to user actions, all data processing sits at the store, and the main component files are only responsible for setting up callbacks and properly displaying data retrieved from the stores.
React Native is heavily in development, and a new version is released every month. Constant updates bring new ported native components, improved custom ones (like the Navigator), bug fixes and performance improvements. This requires vigilant updates to code after the logic of the prebuilt components changes, or the native projects from which the application is built change structure. The changes required aren’t major, though, and don’t come too often, so it’s not too hard to keep up. There’s always plenty of documentation and warnings regarding new rules, and an upgrade assistant is available to check your native projects’ compatibility with the newest version. React Native doesn’t check its code at compile time, only at runtime, so an error and warning system was implemented. If an error occurs in JavaScript code, a red screen pops up with its description (though this only happens in development mode). Possible uncaught exceptions, possible internal changes and other miscellaneous issues are displayed in yellow warning screens that can be dismissed (however, note that the application is not guaranteed to work properly after the dismissal). Familiar console.log works nicely, too, with its output redirected to the usual device logs.
Since most of the application code is no longer running the native way, XCode and Android Studio debuggers don’t work, but that doesn’t mean debugging can’t be done. React Native usually works by using the JavaScriptCore engine to run the code. While developing, you can enable Chrome debugging and run the code on a local Chrome instance. This way, instead of JavaScriptCore, Google’s V8 JavaScript engine is used for the running application. After the linked debugger tab opens in Chrome, you can use developer tools to inspect JavaScript code from the app, put down breakpoints and go through code step by step. The whole project structure is accessible in JavaScript files.
Is React prime time ready?
React Native enables quick prototyping and a very high initial velocity. Basic features are relatively easy to implement. If needed, they can be extended with native code and native views, and also integrate well with other view controllers. Specifics are easy to pick up, and don’t stray too far from the React framework used in web development.
But with all the pros, there are some cons, too. It’s natural that JavaScript code is not as efficient for calculation-intensive tasks, and there is an overhead when JavaScript is controlling native elements. JavaScript has a single dedicated device thread, while native code is free to use whatever threads it wants. In performance, React Native stays behind an optimized native application.
That doesn’t mean React Native is hopeless, though. There are ways to bring it closer to native performance, like moving animations to native threads (which is currently in preliminary stages), optimizing JavaScript code, and moving processing-intensive tasks to native modules and separate threads. When JavaScript is left to deal with its intended tasks, it performs quite well, and the pros end up outweighing the cons.