Skip to content

Instantly share code, notes, and snippets.

@alloy
Created October 29, 2019 13:34
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save alloy/c63e8658c3527521d8409bac11f52d23 to your computer and use it in GitHub Desktop.
Save alloy/c63e8658c3527521d8409bac11f52d23 to your computer and use it in GitHub Desktop.

React Native Link

Who am I?

  • Eloy Durán
  • @alloy
  • Author of CocoaPods
  • Engineer at Artsy, where we’ve been using React Native for 3.5 years in our brownfield app

What will you learn from this presentation?

  • What native package ‘linking’ is
  • How to use it for iOS and Android platforms

What is it and what problems does it solve?

First, let’s talk about ‘linking’ outside of the context of React Native.

Perhaps you may have thought it was related to npm link / yarn link; however, beyond both making dependencies whose source exists on a local file-system available to a project, these tools are meant to allow one to work on those dependencies and take their name from ‘symbolic link’, the manner in which they are made available to an application. In case you were not thinking this–never mind me, let’s carry on.

Instead, the origins lie with tooling thats exist in native tool-chains, such as those used for languages like C, the term ‘linking’ refers to building up a working executable from various compiled modules–referred to as ‘object files’, that contain the compiled ‘object code’ corresponding to input source files. These can either be ‘statically’ linked, which basically just copies over the ‘object code’ into the executable; or they can be ‘dynamically’ linked, meaning the ‘object code’ is provided as a separate binary to the executable and loaded into memory at runtime. Why you would choose one over the other is beyond the scope of this presentation, but suffice it to say that for a native dependency’s code to be available to the application it needs to be linked in either of these manners.

‘Linking’ in the context of React Native refers to this process, but in reality only covers configuring your project for the actual linker machinery to be able to do its work at build time. This is the part that the rest of this presentation is concerned about.

How was this done historically (for iOS)?

Now that we understand what linking refers to in the context of React Native, let’s take a look at how this was done over the years in an iOS project.

Manual

As taken from the React Native documentation, a native package is expected to come with a Xcode project that instructs Xcode how to build a static library out of the package’s native source.

  1. The user adds a reference to the package’s Xcode project to their own (app) Xcode project;
  2. after which they are able to select the package’s static library from the list of libraries their application should link against;
  3. and finally, where necessary, update build settings of the app Xcode project required by the dependency, such as including the correct C++ standard library.
  4. If, at runtime, you are only using the package through JavaScript then you are done now. If you also need to use the package’s code from your own native code, you will need to setup header search paths so you can include the package’s headers in your source files.
  5. If the package has any native dependencies of its own, you need to repeat the above steps for those dependencies.

Automated

An automated version of the exact same approach has existed for a while now, through the link command of the react-native command-line tool, in that it will actually edit your Xcode project on-disk. One limitation with this approach was that the tool doesn’t know about native transitive dependencies, and as such in those cases you would still have to perform the manual approach for those dependencies.

What problems exist with this approach?

  1. In an ideal scenario, you would have to perform 2 steps to manually integrate a single native package, at worst including additional build settings and multiplied by the number of native transitive dependencies the native package has. Even in the automated case, the inconsistency in handling native transitive dependencies isn’t the greatest developer-experience.
  2. Needing to understand the build settings of Xcode and its underlying tooling. This isn’t necessarily a bad thing, things can and will break and it’s good to know how things work under the hood, but having to do this each time even when on the happy path isn’t ideal.
  3. Other than bookkeeping, there’s no easy way to track which build settings originated from what dependency, which becomes a maintenance burden when you want to upgrade or remove dependencies.

Alternative and the future of React Native linking

This is where relying on dependency managers comes in. Respective ecosystems already have dependency management solutions that are able to deal with all of the above, so in the spirit of standing on the shoulders of giants, let’s leverage those domain specific tools instead!

In the case of iOS, the popular option is CocoaPods–which given a manifest that describes a project’s dependencies, similarly to a package.json file, will perform all of the aforementioned work and do so in a maintainable manner. For instance, it is able to resolve and install transitive dependencies like any other dependency, it encapsulates dependency specific build settings in xcconfig files, and does so with minimal one-time changes to your app’s Xcode project.

Seeing as sooner or later most people will have to deal with linking native packages and the different ways in which you had to maintain your app’s Xcode project, the React Native maintainers decided to make utilizing CocoaPods the default since version 0.60. (Gradle, for Android apps, was already the default.)

So, great–now there’s a single place where you manage your native dependencies! But given this standardization, we can take it one step further and automate the process. 🐢s all the way down!

Auto-linking

Enter auto-linking, which is a new addition to the react-native command-line tool that discovers packages that have native requirements and offloads the responsibility of configuring the app project to the dependency manager for the respective platform.

For iOS, this means that adding a native package boils down to:

  1. Install the package, e.g. yarn add lottie-react-native.
  2. Run pod install.
  3. There’s no step 3.

What this does under the hood, is that the CocoaPods manifest, the Podfile, includes a React Native specific helper, which on pod install will ask the react-native command-line tool for a list of all the native packages and their corresponding podspec files–which are the files that describe to CocoaPods how to build the library. It then dynamically includes those dependencies in its set of dependencies to resolve, as if they were all explicitly specified in the manifest.

For instance, for the lottie-react-native example, this is what the helper will invoke:

$ npx --quiet react-native config

…and this is [part of] what react-native reports back to CocoaPods:

{
  "dependencies": {
    "lottie-react-native": {
      "root": "/path/to/project/node_modules/lottie-react-native",
      "name": "lottie-react-native",
      "platforms": {
        "ios": {
          "sourceDir": "/path/to/project/node_modules/lottie-react-native/src/ios",
          "folder": "/path/to/project/node_modules/lottie-react-native",
          "pbxprojPath": "/path/to/project/node_modules/lottie-react-native/src/ios/LottieReactNative.xcodeproj/project.pbxproj",
          "podfile": null,
          "podspecPath": "/path/to/project/node_modules/lottie-react-native/lottie-react-native.podspec",
          "projectPath": "/path/to/project/node_modules/lottie-react-native/src/ios/LottieReactNative.xcodeproj",
          "projectName": "LottieReactNative.xcodeproj",
          "libraryFolder": "Libraries",
          "sharedLibraries": [],
          "plist": [],
          "scriptPhases": []
        },
        "android": {
          "sourceDir": "/path/to/project/node_modules/lottie-react-native/src/android",
          "folder": "/path/to/project/node_modules/lottie-react-native",
          "packageImportPath": "import com.airbnb.android.react.lottie.LottiePackage;",
          "packageInstance": "new LottiePackage()"
        }
      },
      "assets": [],
      "hooks": {},
      "params": []
    }
  }
}

Given this, the helper will read the podspec:

Pod::Spec.new do |s|
  s.name         = "lottie-react-native"
  s.source_files  = "src/ios/**/*.{h,m,swift}"
  s.dependency "lottie-ios", "~> 3.1.3"
end

…and dynamically add the native dependency, as if it were specified like so:

pod "lottie-react-native", :path => "../node_modules/lottie-react-native"

This will in turn resolve the dependency, its transitive dependencies, and setup the Xcode projects accordingly:

$ pod install
Detected React Native module pod for lottie-react-native
Analyzing dependencies
Downloading dependencies
Installing lottie-ios (3.1.3)
Installing lottie-react-native (3.2.1)

Demo iOS

Demo Android

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment