Switching From React Native to Proper iOS and Android Deployment

Using a cross-platform framework like React Native to develop and deploy your mobile app can be a really tempting idea. The promise of being able to share the same code between both iOS and Android, using a small team of developers to build it with, is a premise that’s not easy to dismiss. It can be even more tempting if you think you might be able to use existing React web engineers on your team to build your mobile apps, too.

I recently worked with a team to deliver and maintain a real-world iOS and Android app using React Native. Here is why, after three years of using it, we decided to move away from React Native, and how I helped transition the team to proper Swift and Kotlin development.

Like Developing with Duct Tape

Developing a React Native app feels a lot like developing with duct tape. (photo from MythBusters Jr. and Science Channel)

When I started, the team had just begun using React Native to create a new app for iOS and Android. The cross-platform promise of React Native had been the major draw, having only a few developers on the team to develop it with. I became one of those developers, and for three years we had a vigorous cadence of maintaining and updating the app with new features every sprint.

From that work, I started to get a pretty good idea of how React Native really was at creating and maintaining a commercial mobile product. At first, it was looking good. Almost 90% of the same code (if you didn’t count the third-party dependencies) was being shared between both the iOS and Android versions. It seemed like a huge win.

As time went on, though, and new releases of the app marched out the door, the hidden challenges of React Native started to come to light.

A life of Co-dependency Dependencies

Third-party libraries are an incredibly helpful fact-of-life for web development. They allow you to quickly add new functionality and new features to your web application that bring real value to your app without spending any of your own resources developing and testing them. Plus, they are often written with the same JavaScript tools that you are using to write your web app with, making the libraries easy to modify and fix by your team if needed.

Relying on third-party dependencies is also much more manageable with a web app. You have complete control over the environment your app is running on, and you decide when and how that environment changes. Adding new features and deploying fixes are entirely under your control. Even better, they happen instantly for all your users as soon as you deploy them to your production server. All of your latest and greatest updates will be there waiting for them the next time they access your app through their web browsers.

Because of this, React for the web can be a wonderfully effective and controlled way to build and maintain a rich, complex web application. .

Absolutely none of this, however, can be said about React Native.

Although React Native is built around the same philosophies that React for the web is (a declarative, state-based approach of composing and build apps with a mixture of your own code and third-party libraries), that doesn’t mean it will be just as effective for building and maintaining a mobile app.

And as it turns out, it’s not.

That’s because, unlike React for the web, the environments your mobile app will be running in are vastly varied, disparate, and entirely out of your control.

You can’t simply test and deploy a new version of your app on one single, trusted production environment like you can with a React web app. There are hundreds of variations and configurations of devices your mobile app could be running on, especial with Android, all running various version of the Apple’s and Android’s SDKs. (For some idea of the number of Android smartphones out there, look here.)

Once you have your app built and tested on the devices you think your app will be running on most, you then have to upload it to the App Stores for review and approval. Only then, can you hope your users will actually download and update to your latest version.

This, of course, is a normal part of mobile development, and entirely manageable with normal mobile development practices. However, it starts to become particularly problematic when React Native is involved because of one of its core philosophies: relying on third-party libraries to construct your app.

In other words, code that is outside of your control.

React Native depends heavily on this philosophy. You end up having to add third-partly dependencies for almost everything, including the most basic mobile functionality that would have otherwise been available to you if you had built your app using Apple’s and Google’s native development tools.

For example, React Native has…

  • No screen navigation abilities. You have no elegant way, out of the box, to navigate screens. If you want your users to be able to drill down into different views within your app, and to be able to navigate back out of them, you’ll need to add a dependency. Or develop your own approach. I can’t think of any, but the most simple of apps, that wouldn’t require this in some form or another.

  • No tab navigation either. Tabs are another common UI experience that both iOS and Android provide, and a navigation experience that many apps rely on. If you want to have that kind of navigation in your React Native app, though, you’ll need to add a third-party dependency, or build it yourself.

  • No basic UI components like date or time pickers. You’ll need to add a third-party “community package” if your app requires a way for users to select dates and times. Or you’ll have to develop your own solution. Most apps often need this kind of UI. I can’t think of an app I’ve worked in the past nine or ten years that didn’t require some form of date or a time picker as part of the user experience.

  • No web views. If you need to incorporate any HTML or web content into your app, you’ll need another third party dependency.

  • No push and local notification support. Both iOS and Android implement notification alerts in very different ways, and you’ll need a third-party dependency to provide them in your app.

  • No map views. React Native provides no support for map views on either device.

  • Things like widgets, wearable device integration, camera support, and location services are definitely not supported by React Native. You’ll need third-party dependencies for each of these.

  • No local data storage. State is core to a React Native app, but surprisingly it doesn’t do it very elegantly. You needed a third-party library like Redux to help. And if you want to actually persist a user’s sate, that will require another third-party library as well.

It all starts to feel a bit like putting an app together with duct tape.

At the Whim of Stone Giants

The Stone Giants in Peter Jackson’s The Hobbit

There is a much bigger problem, though, with needing so many third-party dependencies. They break. And they break more often than you might think, creating real obstacles for you to deliver your own product.

This is hardly ever a problem with a React web app since you do control the environment your app is running in. You don’t ever have to update that environment, risking things breaking, if you don’t need to. Once your app is stable and running well, you can leave it that way for as long as you like. Of course, there are security updates and other server-related issues that might require updates, but it’s entirely up to you and your team’s priorities and schedules for when and how those updates might happen.

This is not the case with mobile apps. Apple and Google update and improve their devices, SDKs, and development tools constantly, and you have to keep up with them to keep your app running. We all want (and need) Apple and Google to keep doing this so that their tools, SDKs, and devices get better. This is an expected and important part of mobile development. But because React Native requires you to rely on so many third-party dependencies, it will often break your app in ways you have no control over, putting your fate in the hands of someone else’s code and someone else’s development schedules.

Peter Jackson’s (unwitting) metaphor for what a team goes through managing React Native dependencies.

It can be a bit like that scene in Peter Jackson’s The Hobbit, where Bilbo and the band of Durin’s Folk are trapped on a mountain ledge between fighting Stone Giants, watching the mountain crumble around them.

Some might argue that you can just fork the broken libraries and fix them yourself when that happens. Sure, that’s true. Usually. But then you’re in the business of learning and fixing someone else’s code for them, in addition to your own. We had one third-party library, in particular, that we had to fix for them four different times over the three years we used React Native to develop our app.

You also find yourself writing native code to fix those libraries, too, because unlike React web dependencies, most React Native libraries have been written in Swift, Kotlin, or even Objective-C and Java in order to add those native features you needed. Which, of course, defeats the whole “benefit” of not needing native developers on your team. It starts to make you wonder why you’re not just writing your app in native code, anyway, having code you actually do own and control.

Minefields Ahead

The one third-party dependency that can cause the most trouble, though, is React Native itself. That’s because React Native actually requires Xcode and Android Studio to build its React Native apps. It needs those native tools to compile and bundle an executable that you can submit to the App Stores.

Apple and Google update their tools often, requiring you to upgrade to their latest tools in order to submit your apps. Upgrading those tools can often break React Native itself, and you find yourself walking through a minefield of updating the React Native framework, along with any third-party libraries that may have broken along with it.

From our experience, performing a React Native update is no small task, and can have a major impact on delivering your app. Over the three years we used React Native, we were forced to upgrade React Native three times as well, and it always took a full sprint or more to tackle. This required multiple engineers, significant QA resources, and took away time and energy we would have rather spent building and improving actual features in the app.

This, alone, was the reason we began to really question whether or not React Native was a good solution moving forward. The efforts to keep all these dependencies updated and working well began to quickly shadow any efficiencies we were seeing from having shared code.

There were other factors to consider, as well…

React Web Developers Building React Native Apps

Our experience with React Native did show us that React web developers can adapt to developing React Native mobile apps. Often, quite well. Although it never turned out to be the big efficiencies we had hoped for.

React uses HTML’s DOM model for UI, where as React Native uses the device’s native view component model. Plus React uses standard CSS for styling and layout, where as React Native uses a custom subset that lacks any cascading abilities and requires a completely different syntax. All of this can take time to adjust to for those who have only ever developed front-end UI with HTML and CSS.

React web developers who had more of an aptitude for front-end web development, compared to those who were more inclined to the back-end tasks, did much better. It always took more time than we had expected, though, to ramp up any new web developers that were added to the React Native team.

This was certainly not a mark against React Native, but in spite of how it was initially sold as a benefit, it turned out to be a zero-sum gain.

Performance Issues

Adding a graph to our React Native app required two new dependencies, and the performance results were disappointing.

React Native performs surprising well considering what it’s built on. There are limits, though, to how well a React Native app can perform. Native Swift and Kotlin apps will almost always outperform React Native apps, especially for certain types of tasks.

Our mobile app, for example, required a simple graph to show some data changing over time. Again, we needed a third-party React Native library to even consider the feature. (Actually two. The graphing library itself, and an SVG drawing library that it depended on.) But because of React Native’s performance limits, the graph never performed as well as we had wanted. We had to scale back on some of the graph’s intended functionality like pinch-to-zoom, and even the amount of data it was rendering at any given time, just to get it to work. And it was still disappointing and clunky to use in its final scaled-back form.

The Development Tools

There is a lot to like about developing with JavaScript, especially for web development. I like how much JSON is part of its DNA, which makes writing things like API calls a whole lot of fun.

Plus JavaScript is everywhere these days, with tons of support in the community to draw from.

Which is why it surprises me there still aren’t better development tools available for it, yet. VS Code is fine for developing web applications, but it’s no mobile IDE. Especially when comparing it to Xcode and Android Studio.

And if you have to use Chrome to debug your mobile app, I’m pretty sure you’re doing it wrong.

Making the Switch

Putting the less-than-optimal development tools and slower performance aside, the reality of being so heavily tied to so much breakable code outside of our control (all of the third-party dependencies, and the React Native framework itself) started to become a real burden, with real consequences to the team’s ability to deliver effectively. We worried it was only going to get worse as the app’s features evolved.

After working through one last sprint of upgrading React Native and keeping all the third-party dependencies happy, we started to take a serious look at how we might take the existing React Native team and turn them into a Swift and Kotlin native development group.

The thing is, after you spend three years developing a React Native app, releasing new features sprint after sprint, you start to realize there is no magic to it. JavaScript is no easier to write than Swift or Kotlin is. In fact, JavaScript (and even TypeScript) can be more of a challenge with the limited tools available for it, and all of the quirks that come with the language. (The lack of type and compiler safety, for example, that Kotlin and Swift provide so much better.)

Eventually you start to wonder. If React Native needs Xcode and Android Studio anyway, why not just use those tools directly? Bypass all the headaches of co-dependency dependencies and just develop the app the way both Apple and Google had intended us to build it.

Using the Same React Native Team

With research and due diligence, it started to become pretty clear it was the right move to switch to proper native development, and something we could actually handle. I had basically been building up the skills and experience for that kind of transition my entire mobile development career. The biggest challenge was convincing everyone else, including the rest of the team. The myth of cross-platform frameworks like React Native had become so ingrained in the psyche of our entire industry, it was going to be a bit of a challenge to figure out how to present the other side of the discussion.

The plan for success, we felt, required putting three pieces in place to help convince everyone it would work…

  1. It would help if the native app had a minimal, manageable set of features we could initially transition into to. Luckily, the company was wanting to re-think the app we were working on, and a new Product Manager had already started to take on that change. We had a window of opportunity to control what the app could launch with.

  2. It would require someone on the team with a history of Swift and Kotlin experience to kick it off, and to help train the current team of React Native developers. Fortunately, I had been building native iOS and Android apps for eight years prior to joining the company.

  3. It would require a POC with some starter scaffolding code to show how it would be possible, and then to use that scaffolding as the actual code base for the team to transition into coding Swift and Kotlin with.

The Scaffolding Starter Code

The approach we took for creating the scaffolding was to design it as two-stack architecture.

The lower stack would consist of a native library, built and maintained for each platform, containing all the foundational code that all apps need. Things like an extendable UI library of controls, layout views, a core data and persistence model, common utility code, and foundational API networking and error-handling code.

The upper stack would be where all the app-specific feature code would live—the code that used the lower stack to provide the app’s unique functionality and feature set for the user.

Between the two stacks would be a common API interface that both the iOS and Android upper stack could use to access the abstracted lower-stack code with. As part of that, we would discourage accessing any of the native SDK APIs directly from the upper stack, adding new features to the lower stack with new common APIs to access them with instead. And because both Swift and Kotlin share a lot of the same syntax, it would become possible to cut and paste much of the code between iOS and Android, allowing you to maintain (with some team discipline) very similar code (albeit not “shared” code) between both devices.

Then, the key to ongoing success, would be to make sure the React Native engineers were trained on the common API interface between the upper and lower stacks, and for the team to become committed to making sure we all wrote the upper-stack code in parity between Swift and Kotlin as much as we could, using that lower-stack interface to keep it consistent and easier to maintain.

Over time, using code reviews and training, we hoped the practice would become second nature. And once the team got comfortable with the upper-stack Swift and Kotlin code, they could quickly start to write and maintain lower-stack enhancements, too.

The Results…

We developed the initial POC scaffolding, made our case to the team to switch, and put out an initial release of the app for both iOS and Android in only three months. Three months!

Since then, the transition has been (for the most part) a huge success. We’ve been iterating new builds and adding new features for over a year—all delivered on time, and all with the requirements that product and design teams had requested.

Proper Development Tools for Your Developers

It’s all about the tools. And the people. (image “borrowed” from offermanwoodshop.com)

It is so important that your developers have the right tools to do their jobs.

Apple and Google have both devoted an unimaginable amount of energy and resources, year after year, to give us those tools.

They aren’t perfect. They never will be. But I think you can argue they are the right tools for the job. Watching Apple engineers talk about Xcode and Swift (and now SwiftUI) at every WWDC is always an inspiration. Google engineers talk about their Android advancements with the same amount of passion. It’s to their benefit, after all, to make sure their development tools work well for both them and for us.

The reality is, as much as cross-platform frameworks like React Native (or Flutter, or Jetpack Compose) would like us all to forget, iOS and Android apps will always be better (and ultimately easier to build and maintain in the long term) using the native tools and languages that both Apple and Google have worked so hard to provide for us.

A Healthy Amount of Duct Tape

When developing directly with Xcode and Android Studio, you have immediate access to all the features you need in order to develop a rich, professional-grade mobile app with. You also have access to the latest-and-greatest implementations of those features. No need to add any third-party dependencies just to have the most basic functionality in your app, and no need to wait for those dependencies to catch up when those features evolve and change.

Red Green, the Duct Tape Handyman

That means whenever Apple and Google do update their SDKs and development tools with all the latest-and-greatest, the transition happens almost automatically for you. And it makes it infinitely easier to manage anything that does happen to break because all of the code is completely under your control.

Some third-party dependencies may still be helpful, of course, but you only need to add the few, highly specialized dependencies that can actually add extra value to your app, or improve your development process in some way. Using MixPanel for analytics or Auth0 for secure authentication, as an example, or using Optimizely for feature experimentation

The difference is that adding dependencies is entirely your choice, and not something forced upon you by your development platform.

Real Performance

Our native, high-performant graph for both iOS and Android, written in Swift and Kotlin. No dependencies required.

Once we switched to native Swift and Kotlin, we were able to implement graphs in the app with ease, and could make them function as the product and design teams had wanted them to behave.

Plus, we were able to write all of the code ourselves, without the need for a single third-party dependency.

But it’s not just the performance of high-demand features like data visualization. A native UI, in general, feels more responsive to a user’s gestures. You can worry far less about having to optimize UI performance, and focus much more on how to actually implement your app’s unique features.

Productive on Day One

Engineers are really smart, imaginative, adaptive people, when given the chance. We had expected some ramp-up time switching from JavaScript and React Native to Swift and Kotlin development, but with just a little help from our two-stack architecture, even the junior React Native developers on the team became productive Swift and Kotlin developers almost immediately, picking up tasks and building new features on the very first day. The senior React Native developers were even jumping in and making changes to the native lower-stack code just as quickly, and it only got better with each new release.

Our experience showed any lack of initial Swift and Kotlin knowledge by the engineers had very little impact on our ability to deliver, and it was easily managed and eliminated through code reviews and a little bit of training.

Mobile Development Done Right

Cross-platform solutions are an amazing feat of engineering. My early career, for example, developing multimedia CD-ROMs and educational software titles, was only possible because of Macromedia Director and its ability to generate both Mac and Windows stand-alone executables.

The native app developed in parity for iOS and Android, using Swift and Kotlin, with Xcode and Android Studio.

With the rich development tools that Apple and Google provide for us today, though, refined over decades to make them easier and easier to develop with, I honestly believe that cross-platform solutions are not only unnecessary, they can become a real hinderance.

The freedom and efficiencies you get from using the native tools directly, developing your apps the way they were intended to be developed, with all of your code under your control, and all of the latest device features at your command—all of that far outweighs any benefits you might get from having some shared code you control, with a whole lot of code you do not. All it takes is a little planning, a little technical management, and a little discipline—which are things every engineering team should have already.

Previous
Previous

Some of My All-Time Favorite Computer Books

Next
Next

First Impressions of SwiftUI from the React Native Trenches