Upgrading React Native Projects in Practice

We suggest to start with a fresh project with all updated dependencies, fix all compilation issues, and then compare with the existing project to do the update. Read on how we ended up there, and how to apply this technique properly.

The Problem: React Native is .... native

If you look at a React Native project, most code resides as JavaScript or Typescript in the src/ folder. Then, there is also an android/ and ios/ folder containing the "boilerplate" for both these platforms. Because react native packages often contain native code (after all, they usually wrap native functionality of the platforms and make it available via JavaScript), these projects contain quite a bit of foreign Objective C or Java code from all the dependencies.

Additionally, if you have a project running for a long time, it happens quite often that you need to tweak the Android and/or iOS build scripts here and there – as explained in the docs of the respective NPM packages. This usually leads to the fact that React Native applications become more non-standard over time, i.e. they deviate from the kickstarted React Native package when development started. In addition, also the kickstart develops further - so that's an additional source of changes.

The Build System on iOS and Android

Android uses a (complex but standard) build setup based on gradle which is working quite reliably usually - and where a comparatively short, human-readable build script is responsible for everything what happens.

iOS, in contrast, uses the xcode build system, which stores everything in a huuuge file - located in ios/[YourApp].xcodeproj/project.pbxproj. In our app which we will upgrade, this file is 739 lines long - and it contains references to many files, lots of UUIDs, and additionally the exact build instructions in some weird way. It's not easily possible to modify this file by hand; so usually I open xcode and do it there, hoping for the best.

Because iOS has no dependency management built into the platform, a system called cocoapods is used to manage the native dependencies and the build process of these. This is already a huge improvement to the react native world on iOS - gone are the days of manual linking where you had to follow long lists of ToDos for the installation process on iOS. The main entry point to Cocoapods is the ios/Podfile, which is what you usually tweak for updates. After modifications, you need to run cd ios/; pod install (or npx pod-install which does the same) to update the xcode project files. BTW - we check in the full ios/Pods/ folder in Git to have reproducible builds across multiple machines.

The usual recommended upgrade path: Down the rabbit hole

Our app has currently about 80 dependencies listed in package.json, which is pretty standard for React Native apps after a while. Android is usually less of a problem to upgrade because the build system works better, so let's focus on iOS first. 

Usually, people recommend to run react-native upgrade or using the online upgrade helper - and then manually apply the patch. For me, this approach has practically never worked - after applying the patch, I usually got various build errors. Then, I tried to figure out what package caused the problem, and tried to upgrade this package to the newest version. After upgrading the package and running npx pod-install, I re-tried this process quite a few times. For me, this did not lead to a working app, because the patches and workarounds which might have been useful a year ago would stay in the project, and cause problems today.

As an example, a year ago, React Native was not yet equipped to run on M1 Macs - and for me, after an upgrade on an M1 mac, I got various problems related to the processor architecture.

Then my usual process was googling the error message, trying to make sense of it, and flip some obscure xcode settings (or create an empty Swift file in the project, or ......). However, this did not really solve the issues, but only made problems worse.

A new approach: Starting from scratch

As the usual approach did not result in a good state, I explored another idea which worked way better:

  1. Start from scratch using npx react-native init AwesomeProject
  2. Ensure this generated project compiles and starts (by running react-native run-ios).
  3. Add all dependencies from package.json in the newest versions, by calling yarn add [packagename]. Then, run npx pod-install and again try compiling and running.

I usually did this in batches of 3-4 NPM packages - so I was able to detect and fix errors quickly (and to my surprise, everything worked nicely and I did not run into any problems compared to the other way).

At this point, we essentially have boilerplate JavaScript code, running with all the native dependencies (though they are not used).

Now, I could have copied the JS to the new project, and hope for the best, but I did the opposite way: I compared all xCode settings via diff -u ios/[original].xcodeproj/project.pbxproj ../[working]/ios/[working].xcodeproj/project.pbxproj to get an idea what xCode settings needed adjustments. This way, I was able to get back to a fully working and nicely running setup again.

... and interestingly, this approach took way less time than the usual update procedure :)

I'd be interested in how you approach React Native upgrades? Do you have other tricks and tips to share? Let me know on Twitter!