Underlying dependencies can trip you up, and probably will.

This post is meant to be read in the context of its parent post "Lessons Learned on a React-Native Project". If you've stumbled on this particular page randomly, I'd recommend jumping over to that post and starting from there.

Oof. This was, by far, the most painful part of my experiences with React-Native. God, where to start.

In React-Native, there are five main dependency concerns you need to worry about:

  1. Yarn/NPM
    • This serves as the entry point for your 3rd party libraries. You'll add the dependencies via yarn add or npm install and it'll resolve any other dependencies it needs from an NPM perspective.
  2. Linking (via the react-native link command)
    • This is the glue that binds the relevant NPM package to the native iOS and Android projects, adding various references and setting up the correct dependency resolution concerns for that platform. So things like adding extra libraries in XCode, Gradle file changes or Pod file changes.
  3. Gradle
    • This is where the native Android components of the above Yarn/NPM dependencies get defined.
  4. XCode
    • This is where the native iOS components of the above Yarn/NPM dependencies get defined.
  5. CocoaPods
    • And, because we're in iOS land, an extra layer of dependency pain. Sometimes, the libraries you're integrating don't actually contain the native code it's trying to call, and this is where CocoaPods comes in. This is basically Nuget (of .NET fame), but for Swift and ObjectiveC. This means that the library you're trying to integrate is actually just bridging to the respective CocoaPods library. You'll get to know your Podfile intimately :)
Yarn/NPM

There isn't much to say here. Quite frankly, this is the most straightforward (!) part of the process.

Linking

Linking in React-Native is a one-time thing, performed per-library when needed. When react-native link [your-library] is executed within the project, the linker goes off and does its best to add the various bits and pieces to each of the respective platforms project files.

I found linking in React-Native to be troublesome. Each library defines its own linking reference (which is what is performed when react-native link [your-library] is executed from the CLI). From what I've seen this is purely doing text manipulation on the relevant files, as opposed to using APIs on the Gradle or XCode processes to add references safely. This can lead to some really crazy problems. Oh yes, I used bold rather than italics, so you know it's serious!

Some things to note with regards to linking:

  1. Linking is not idempotent. If you re-run the command (targeting the specific library), you'll do the exact same thing to your files again, with no respect to what may have been done previously. So you'll have duplicate entries across your Gradle Files, your XCode projects, and your Pod file. Bam... build failures everywhere.
  2. Sometimes your Pod file will get entries added in a place you don't expect. One thing to be wary of in particular is which target the Pod file entry has been entered into. I had a situation where it'd unintentionally added it into a test target, and I spent longer than I'm proud of trying to figure out why the libraries wouldn't build. Once I moved the entry outside of the test target, voila! Build succeeded.

My main point of advice with regards to linking is this: If you find your link command isn't working the way you expect, or (as I found a few times) the relevant library's linker is broken somehow, follow the manual linking instructions. Seriously, there is nothing wrong with doing this. Thankfully, most libraries contain these instructions too :)

React-Native also has some doco on this, see here for iOS, and here for Android. Source control is also your friend here, if things get too out of hand.

Gradle

Ahhh Gradle.

Gradle is its own beast, with its own flaws, quirks and entire site dedicated to its usage.

The main files concerned with Gradle are:

  1. At the root of the android directory:
    • build.gradle - Used to define the high-level dependencies required by the project (so these are things like Android-specific libraries, and class-paths, defining which version of the build tools are to be used when actually building the solution.
    • settings.gradle - This is where the Android dependencies for your libraries get defined (usually referenced by relative paths to your node-modules folder). Here is also where your app (as in the underlying Java entry points) is defined in your application.
    • gradle.properties - These are project-wide Gradle settings, covering things like AAPT2, Build cache and NDK usage. We had to use enableAapt2 set to false for our project, because some of our underlying dependencies weren't compatible with this.
  2. android -> app
    • build.gradle - Wait... Another build.gradle? Yes, that's right! I'm not sure why these need to be named the exact same thing as the file in the above directory, but... here we are. This file is where SDK versions, application identifiers, and the actual dependencies are defined. If you can think of settings.gradle as the definition of your dependencies, build.gradle would be the actual call of these dependencies when building. I think. To be honest, I'm still a bit hazy on how Gradle hangs together.
  3. android -> gradle -> wrapper
    • gradle-wrapper.properties - This is what defines the particular version of Gradle that you intend to use. In CI pipelines, this package will actually be downloaded on the fly each time (I think).

One thing I found quite frustrating in dealing with Gradle is why are these files scattered all over the place? Perhaps someone with actual native Android/Gradle knowledge can answer that, but I really wish these files were all in android -> gradle, for the sake of simplicity (with some naming changes to the particular files to make the entire process significantly simpler and cleaner).

The main issues I found in dealing with Gradle were;

  1. The Android Build Tools (usually installed around the same time as you're installing the SDKs) versions are extremely confusing. There was a lot of trial and error trying to get just the right version, as things didn't always quite line up how I'd expect. You also need to make sure that the SDK version you're referencing in your Gradle file is consistent with what is installed locally, and on your CI pipeline.
  2. Your CI Pipeline will likely have a very different environment than your local build environment. This may seem obvious, but I felt in Gradle this was especially painful. Successful builds locally (that also run on device and simulator!), don't necessarily map to successful builds in the CI pipeline. You may need to spend some considerable time troubleshooting what on earth is going on.
  3. The dependencies, in terms of SDK versions, you may have defined mean nothing to your underlying dependencies for your 3rd party libraries. You may be targeting API 25, but that random camera library you've dragged in is targeting API 21 for some reason, and all of a sudden you get either weird build issues, or straight up failing builds (especially on your CI pipeline... I don't understand why, but the local environments are so much more forgiving when building!). Eventually I stumbled on a way to force all your libraries to the same SDK version you've defined:
// This goes in your top-level build.gradle file, in a separate section to the `allprojects` section you'll have. 
// This does not get nested in any other sections.
subprojects {  
    afterEvaluate {project ->
        if (project.hasProperty("android")) {
            android {
                compileSdkVersion 27
                buildToolsVersion "27.0.3"
                project.archivesBaseName = "AnApplication"
                configurations.all {
                    resolutionStrategy.force "com.android.support:appcompat-v7:${rootProject.ext.supportLibVersion}"
                    resolutionStrategy.force "com.android.support:customtabs:${rootProject.ext.supportLibVersion}"
                    resolutionStrategy.force "com.android.support:animated-vector-drawable:${rootProject.ext.supportLibVersion}"
                    resolutionStrategy.force "com.android.support:support-media-compat:${rootProject.ext.supportLibVersion}"
                    resolutionStrategy.force "com.android.support:support-v4:${rootProject.ext.supportLibVersion}"
                }
            }
        }
    }
}
ext {  
    buildToolsVersion = "27.0.3"
    minSdkVersion = 16
    compileSdkVersion = 27
    targetSdkVersion = 27
    supportLibVersion = "27.1.1"
}

The specifics of what you may need to define will probably vary from what I've shown above, but it'll be a good starting point for you. The build errors you'll get will point you in the direction of the libraries you need to add resolutionStrategy.force entries for.

XCode/CocoaPods

Ive combined the last two in this section because they're closely related. I came into this project knowing nothing about XCode, and as it turns out XCode is... complicated. My main point of reference for IDEs is Visual Studio, and while its far from perfect, I feel like VS makes more sense in terms of how it lays things out, defines the various bits and pieces going into a solution and how it manages external references. XCode (in my Opinion™) doesn't really make much sense. To get to certain things like schemes, or even build settings and build phases, it ended up being a non-intuitive process to get there, especially if you're trying to decipher the various tiny icons scattered around XCode. Of course, take this with a boulder-sized grain of salt as I've got significantly more experience dealing with Visual Studio :)

The one thing I will say though is the "Help" menu in XCode is excellent. Search for a term and it'll actually pop and highlight the menu you're looking for!

How XCode manages dependencies is kind of scary, and that's before we've started talking about CocoaPods. In XCode, external dependencies are managed by adding the xcodeproj for the given dependency directly into the project, under the Libraries folder in the Project Navigator view.

Then, you should be able to reference the relevant .h/.m (header and implementation files respectively) in your AppDelegate.m, where most of the things like callbacks will be handled. Annoyingly, there are multiple ways to declare header imports in ObjectiveC (I think? I'm still hazy on how this works tbh), so if you're integrating a library and when you build it starts complaining that it cant find your headers/files that are clearly actually there, I'd look at changing the particular way the header in question is imported.

The next part of complication in this story is the CocoaPods file (referred to as the Pod file). As previously mentioned, this is where some dependencies that get imported are bridging into an existing ObjectiveC library. If you're confused why we're dealing with the Libraries and this Podfile, its because Libraries are imports where you have the source locally. Think of Libraries like adding an existing project reference to another project in the same solution in Visual Studio.

Here is where things can get a little wild.

For React-Native specifically, if you get into a situation where you need to declare React-Native in your Podfile, you'll need to keep a couple of things in mind:

  1. You need to manually specify the submodules of React-Native you're interested in. I found examples of this annoyingly difficult to track down at first, but as we'll find out later, reading the documentation always helps.
  2. Sometimes, you'll get an extra React target (which contains declarations and implementations of the React-Native submodules) added to your project, despite the fact it already has the individual Library entries for the React Native submodules. Then when you go to archive, it'll throw an error saying duplicate symbols! You can add a post-processing script to your Podfile to remove the target on the fly:
// this goes at the bottom of your Podfile
post_install do |installer_representation|  
    installer_representation.pods_project.targets.each do |target|
        if target.name == "React"
            target.remove_from_project
        end
    end
end  

This is a known issue, and this was the only clean way I found that helped.

Things I don't know how to categorize

Another thing I'd like to mention, and I'm still really confused by this, is that it looks like the build process undertaken by the React-Native CLI, versus the build process taken by the native IDEs is different. Different to the point where sometimes you'll get failures from the IDEs, but not when building using the React-Native CLI. This really baffles me, and I'm still not sure if it's something I've done along the way to break something, but it is definitely a thing. So yeah, if you're in XCode or Android Studio, and things aren't working, I'd jump over to the CLI and try your luck there.

Also, sometimes the repositories you encounter will contain an example application (usually extremely basic). I encountered situations where the example didn't actually line up the with documentation provided by the repo! So if you hit an issue and you know you've followed the doco 100%, I'd clone down the repo and poke around the examples provided. Usually you'll find something different (Pod files were especially guilty of this), and if you make your changes more in line with the example app, you're more likely to have success.

Closely related to this, is you may run into a situation where multiple libraries clash with each other in terms of setup and linking. Something to realize when setting up a new project is the instructions you're following are done in isolation of other libraries. So you may need to tweak things slightly, depending on your configuration. Sadly, I don't have specific technical guidance in this area as it can vary so wildly across the different libraries. Our main pain point was modifying the AppDelegate.m file in the iOS library, and we spent quite a bit of time tweaking it in order to get the various bits and pieces working together nicely. So if you're having to do something like this, I'd recommend taking a look at the documentation for the methods that the library is asking you to modify or introduce. These are normally override methods based on well-known APIs in the ObjectiveC space, so you should be able to track something down.

In general I'd advise if you find a library that does what you want, and you don't need to do any linking, I'd go with that. It'll save you some potential pain.

So as you can see there is a lot of danger when it comes to dependency management. Now that I've got more awareness of these native ecosystems and IDEs I'm more confident in troubleshooting issues when they occur, but if you're going in brand new these are things you will need to be wary of.

Next Section -> "The React-Native dev experience is designed with Mac in mind, not Windows"