Easier React Native upgrades with rn-diff
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 !
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:
- 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, …).
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 ?
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.
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!
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).
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)!
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.
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
- 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 !