Easier React Native upgrades with rn-diff

The React Native environment is a strange world: This shiny framework allows JavaScript developers to easily create mobile apps with a native look-and-feel. Most of the resources (official documentation, examples, tutorials, blog posts, StackOverflow questions, feedback, …) cover the early stages of a project like initialization, setting up the navigation, building screens, having your first “whoa!”, etc. But when the time has come to maintain, industrialize or release a React Native project, resources become sparse and you can sometimes feel like you’re on your own. Upgrading React Native is one of the most difficult tasks you’ll face when working on a long life project. I’ll tell you why, and how I solved it.

The upgrading process

First of all, React Native is upgraded at a fast pace: one minor release every 2 or 3 weeks. Note that, according to semver (see bullet n°4), “minor” releases could introduce breaking changes… and they do !

Number of days elapsed since the previous release

To understand the following, let me explain how the upgrading process works under the hood. Except in cases when you integrated React Native into an existing app, you probably used the CLI to generate a blank app. This skeleton includes:

  • 2 JavaScript entry-points (one for each platform),
  • iOS stuff in the ios directory (the “host app” for the iOS platform),
  • Android stuff in the android directory (the “host app” for the Android platform),
  • Tools (the React Native packager, Flow, Buck, …).

Since #2298, the generation process uses Yeoman with 3 generators embedded in the local CLI (the CLI included in the React Native package, called by the global CLI which is a pretty empty shell).

The upgrading process is described here. With the exception of few versions requiring manual upgrades, it’s a one-step process consisting of running the command react-native upgrade. Underneath, it simply performs a new generation over your existing sources. It’s the way to upgrade any app generated with a Yeoman generator (isn’t it, Swiip ?).

Why do I care about the generator I used to initiate my project ?

Other Yeoman generators generate real source files used as the foundation of your project, intended for modifications afterward. But the React Native generators are slightly different. They generate 2 full native apps (the iOS and the Android ones) containing a lot of React Native related content inside (the native entry-point, references towards all the native modules and components, platform-specific tooling, etc.). From the point of view of a JavaScript developer, these files are not the sources, you’d inevitably tend to consider them as part of the framework, completely tied to the react-native package in your node_modules directory. You can see the react-native upgrade command as a way to keep your hosted apps in sync with the React Native version mentioned in your package.json file.

That being said, let’s see what Yeoman does when you rerun a generator. For each output file that already exists, it performs a comparison between the existing content and the new content (see the Yeoman Conflicter class). If it differs, it prompts the user with two options: replace with the new content or keep the existing one.

react-native upgrade command output

Do you see how this could be problematic? The upgrade command has absolutely no idea which version you are coming from. When a conflict occurs, there is no way to know what kind of change it is:

  • Changes that are coming up with the new React Native version, let’s call them “upstream changes”,
  • Changes made by developer at one time during the project life, let’s call them “project changes”.

Of course, a conflicted file could contain both. The only thing Yeoman can do is printing the diff. Deal with it!

A file containing project changes will be marked as “conflicted” for every React Native upgrade, whether the corresponding file in the host app had been updated between the 2 versions or not!

As a side note, keep in mind that despite the VCS semantics, we are not working with Git! The diff is computed in JavaScript with the diff NPM package. Yeoman is unable to perform an automatic merge.

Why is it such a pain ?

At this point, you should start to imagine how much of a hassle it is to upgrade React Native. Unfortunately, the list goes on.

I said that the ios and android directories could be seen as part of the framework itself. Well, that is not quite true: sooner or later, you have to open the black box and introduce project changes:

  • Native 3rd-party libraries required references in host apps (the react-native link command is here to avoid manual updates but not every library has support).
  • Native settings like device orientation, splashscreen, assets, …

It means that the more your project is growing, the more potential conflicts you will encounter during React Native upgrades.

Some conflicts are easy to solve (like config files, comments, …) but most of them take place in the iOS and Android host apps which are written in Objective-C and Java, with a pinch of platform-specific knowledges like Gradle build files (Android) written in Groovy or Xcode boilerplate (iOS). And you don’t master these languages, that’s the reason you are using React Native!

Oh, and speaking of Xcode boilerplate… it deserves a dedicated paragraph! That is the worst part: some files in the ios directory are not even supposed to be read by a human… The project.pbxproj file contains the whole configuration of the Xcode project. It is a huge file (700+ LoC) written in an obscure (and deprecated!) plist format used by NeXTSTEP. It contains hashes used as identifiers within the file (multiple occurrences) for every file or dependency and the result is that it’s very hard to read (I guess we could feel fortunate, it is not a binary file after all).

Typical conflicts in project.pbxproj while upgrading React Native

Miss a line or make a typo in there when resolving conflicts in this file and the host app is broken, your app won’t start and Xcode will fail to open the project. You are good to restart the process (i.e. revert the changes in this file and redo the upgrade command)!

Note that it’s not only a React Native problem, the project.pbxproj has been a pain for iOS developers for a long time despite some tools like mergepbx or xUnique.

I would finish with some practical issues. When starting a React Native upgrade, you have to warn your teammates to avoid modifying the ios or android directories until your upgrade is done and merged into master. No manual changes and no react-native link (you certainly don’t want additional conflicts due to collaborative work!). You also have to keep track of all the changes made by hand in the host apps since day one of the project (for example: add a big red label “MANUAL UPDATES” on every Pull Request concerned).

rn-diff to the rescue

Let’s think about improving the process. The main problem is the difficulty in knowing what exactly has changed in the version you want to use.

Given that it simply uses Yeoman under the hood, you can explore and track the source code to see where the generators lie and read their Git history.

I did that for you, it’s:

  • here (the Android host app)
  • here (the iOS host app)
  • and here (platform-agnostic stuff like config files)

It’s a good start, upstream changes are entirely displayed here, but it’s still a hard process to extract all the changes due to a specific React Native version. You have to track down every commit and its corresponding Pull Request to see if it’s been embedded in the version you want to upgrade. Moreover, there are a few surprises, like .flowconfig which is taken from the root directory.

We could do better than that… If the main point is to isolate upstream changes from the project ones, how about executing the upgrade command on a blank project, without project changes at all?

There is a great tool which aims to display changes in source code and it’s called git diff. And there is also a great service for dealing with public git repositories and it’s called Github.

Github is able to display a page with all the changes between 2 parent/child branches. So… it could be a good idea to upgrade React Native on a blank project, version after version, by creating a dedicated branch every time, isn’t it ?

That’s the whole point of rn-diff http://github.com/ncuillery/rn-diff. It contains a pristine React Native project generated with react-native init with a pretty old React Native version and upgraded separately with react-native upgrade.

Branch tree of rn-diff

All branches of this repo contains one commit which contains the output of the upgrade process. It is now very easy to see the changes from one version to another using the Github compare view:

Now, you can use the upgrade command backed by the corresponding rn-diff view. There are 3 scenarios when dealing with the files marked as “conflicted” by Yeoman:

  • The file doesn’t appear in the rn-diff view: it means this file hasn’t changed in the new version, you can skip it with a clear conscience!
  • The file presents a few changes in the rn-diff view: you should probably skip it (i.e. keep your changes) and report the React Native changes manually (a good example of this scenario: a very minor change in the project.pbxproj file introduced in 0.29.0).
  • Most of the changes can be found in the rn-diff view: they are upstream changes, you should probably overwrite the file and report the project changes.

I’ve saved the best for last. By just appending “.diff” to the compare view url, Github gives you a ready-to-use Git patch: https://github.com/ncuillery/rn-diff/compare/rn-0.31.0...rn-0.32.0.diff

Despite some caveats (see the usage guide), you can use it as a replacement of the react-native upgrade command by running git apply. By entering the git world, you get many advantages:

  • No more conflicts for files unconcerned by the new version,
  • Automatic merge,
  • git apply options (dry run, whitespace management, exclusion, …),
  • In case of real conflict, use your favorite merge tool !

I would like to warmly thank my teammate Dits kenny for his guidances and Ryan Kaskel, an early adopter of rn-diff, for his in-depth review of this article.