This post is a high-level technical overview of how we integrated React Native (RN) into Course Hero's flagship iOS app. Our first RN app, for our Textbook Solutions product, is now out in the wild.
The idea to implement RN came from a Course Hero internal hackathon project done by myself and Ernesto Rodriguez. We saw the opportunity to introduce Course Hero to this great technology, already in use at Shopify, Facebook, Instagram, Tesla, and more.
Though Course Hero currently uses React for web development, we also have separate mobile teams who maintain our mobile native apps. Using RN allowed web developers with good knowledge of React to apply their expertise towards building a mobile app. This flexibility allowed us to scale our textbook product to native platforms in order to give our customers a great experience.
Deep dive on the integration
RN Dependencies
When we started, we had a separate repository on GitLab: one for our web app, and another for our iOS app. We created a separate repository for the RN integration, which had the build file. There is no easy way of creating the two links, other than having them in a remote somewhere and fetching the build from a script inside the iOS repo or adding the RN inside the iOS repo. But we didn't want the iOS team needing to clone any RN dependencies, and this was our first iteration anyway.
We started by adding the RN dependencies to the iOS Podfile. We then forked the RN project to our Course Hero Github Repo and then used the source method to clone the RN project to our local ~/.cocoapods/repos/coursehero
dir. Now everyone who clones the iOS repo will automatically have the RN dependencies when doing pod install.
In Github, we made 0.63-stable our default branch. This helped us keep the RN project in sync with the Podfile. To change the default branch in GitHub: [repo] -> Settings -> Branches
# Podfile
def react_native_pods
source 'https://github.com/coursehero/react-native.git'
source 'https://github.com/CocoaPods/Specs.git'
rn_path = '~/.cocoapods/repos/coursehero'
# Default RN depencences
pod 'React', :path => "#{rn_path}/"
pod 'React-Core', :path => "#{rn_path}/"
pod 'React-Core/DevSupport', :path => "#{rn_path}/"
pod 'React-Core/RCTWebSocket', :path => "#{rn_path}/"
…
# 3rd party
pod 'glog', :podspec => "#{rn_path}/third-party-podspecs/glog.podspec"
# … all the other depencies that your project needs
end
def main_pods
# … additional pods
react_native_pods
end
abstract_target 'All Targets' do
target 'Course Hero' do
project 'Course Hero.xcodeproj'
main_pods
end
end
Our Podfile will start to look something like this - react_native_pods been the method that encapsulates all the RN dependencies
Intro to RCTRootView
Doing the integration between the two sides is fairly simple. In iOS, we can use the subclass RCTRootView from the UIView class, which we can use in any location of our iOS app.
Most of all the Swift and the Obj-c code below is under the CourseHero iOS folder. CourseHero/ReactNative/Textbooks/
// RNViewManager.swift
class RNViewManager: NSObject {
static let sharedObject = RNViewManager()
var bridge: RCTBridge?
// crating the bridge if is necesary, avoding creating multiple instances
func createBridgeIfIsNeeded() -> RCTBridge {
if bridge == nil {
bridge = RCTBridge.init(delegate: self, launchOptions: nil)
}
return bridge!
}
func viewForModule(_ moduleName: String, initialProperties: [String : Any]?) -> RCTRootView {
let viewBridge = self.createBridgeIfIsNeeded()
let sourceURL = Bundle.main.url(forResource: "main", withExtension: "jsbundle")
#if DEBUG
sourceURL = URL(string: "http://localhost:8081/index.bundle?platform=ios")
#endif
let rootView: RCTRootView = RCTRootView(
bundleURL: sourceURL,
bridge: viewBridge,
moduleName: moduleName, // the module name, this is the name of the React Native App
initialProperties: initialProperties
)
return rootView
}
}
RNViewManager is going to a swift reusable class
// CourseHeroHomeController
extension CourseHeroHomeController {
func openTextbookApp() {
let textbookRNView = RNViewManager.sharedObject.viewForModule(
"TextbookApp", // the RN app name
initialProperties: nil)
let reactNativeVC = UIViewController()
reactNativeVC.view = textbookRNView
// differnt settings for our need case
reactNativeVC.modalPresentationStyle = .fullScreen
self.present(reactNativeVC, animated: true, completion: nil)
}
}
Calling RNViewManager class
How do the two worlds communicate?
For the RN and native applications to communicate, we need a bridge - a way to send JSON data bidirectionally and asynchronously.
In our case, the RN app had a few modules that we needed to implement. From sending user information to sending callbacks and executing some business logic on the native side.
RN To Native
A key step in the process was creating a Native Module, which is a 3 step process.
The first step is to tell our native app about the RN bridge (we only need to perform this once), and then add the data below to the header.h file in our project. Note that there should only be one header file per project and it should conform to the standard naming convention, ProjectApp-Bridging-Header.h
// CourseHero-Bridging-Header.h
//...
#import <React/RCTBridgeModule.h>
#import <React/RCTViewManager.h>
#import <React/RCTUIManager.h>
#import <React/RCTBridge.h>
#import <React/RCTRootView.h>
Can be found in the Build Settings tab too
Next, we create our Modules. We started with TrackingModule.swift
which allowed us to access the Native code from the RN side and report some tracking metrics to our internal tracking service.
import Foundation
import React
@objc(RNTrackingModule)
class RNTrackingModule: NSObject {
@objc static func requiresMainQueueSetup() -> Bool {
// true will initialized the class on the main thread
// false will initialized the class on the background thread
return true
}
// all method that will need to be accessed by Obj-C
// needs to add the `@objc` directive
@objc func logEvent(_ eventName: String, withTrackInfo: [String: Any]) -> Void {
// log events to your tracking service
CHTrackingService.logEvent(eventName, withValues: withTrackInfo)
}
@objc
// constantsToExport: A native module can export constants that are immediately available to React Native at runtime.
// This is useful for communicating static data that would otherwise require a round-trip through the bridge.
// this data is on runtime, you won't get updated values.
func constantsToExport() -> [AnyHashable: Any]! {
return [
"inititalData": ["userId": 1],
]
}
}
Finally, we exposed the Swift class Module to RN by creating another file, typically the same name of the module above but with a .m
extension representing Objective-C. This is typically referred to as an RN Macro.
//
// RNTrackingModule.m
// Course Hero
//
// Created by Henry Arbolaez on 01/25/21.
// Copyright © 2021 Course Hero. All rights reserved.
//
#import <React/RCTBridgeModule.h>
// RCT_EXTERN_REMAP_MODULE allow to rename the exported module under a different name
// first arg is the name exposed to React Native
// second arg is the Swift Class
// third arg is the superclas
@interface RCT_EXTERN_REMAP_MODULE(TrackingModule, RNTrackingModule, NSObject)
RCT_EXTERN_METHOD(logEvent: (NSString *)eventName withTrackInfo:(NSDictionary *)withTrackInfo)
@end
Accessing the Swift module from React Native
With the native side set up, we moved to the RN project/App.js
file, where we imported NativeModules
from the react-native
package. Any module exported from the Obj-C Macros will be available using the NativeModules
object.
// App.js
import { NativeModules } from 'react-native'
// now we should have access to the logEvent and initialData
console.log(NativeModules.TrackingModule)
To recap, the process of creating a Native Module and exposing it to RN goes like this:
1. Create the Swift Module Class
2. Obj-C Macro which expose the Swift Module Class
3. NativeModules which is used in RN app, to access the module or methods exported from Objective-C
* @objc in the top of a swift method, is to export them to the Objective-C Class
* RCT_EXTERN_MODULE or RCT_EXPORT_MODULE (from objective-c code) - to export the module or methods to the RN
Native To React Native
When we instantiate RCTRootView
, we can pass data into the initialProperties
parameter. The data needs to be an NSDictionary
, which then gets converted to a JSON object that we can access in the root component.
let textbookRNView = RNViewManager.sharedObject.viewForModule(
"TextbookApp", // the RN app name
initialProperties: [ "currentUser": currentUser];
)
when we load the RN app, it adds a rootTag
, which allows us to identifier the RCTRootView
import React from 'react'
import { View, Image, Text } from 'react-native'
interface Props {
currentUser: User
rootTag: number
}
const App = ({ currentUser, rootTag }: Props) => {
return (
<View>
<Text>Hi, {currentUser.name}!</Text>
<Image source={{ uri: currentUser.profileUrl }} />
</View>
)
}
Destrcuring props, which have current
UserRCTRootView
exposes another way of sending messages by using appProperties
, which is helpful if you want to update the properties initialized in your RCTRootView
and trigger a rerender of the root component.
We didn't have a use case for using the RCTEventEmitter
subclass, but this is the preferred way of emitting some events to signal that something has changed to the RN side.
Iteration Speed
RN allowed us to build, integrate and deploy the textbook app to the existing iOS app in less than a month. While doing the integration, we took advantage of hot reloading which allowed us to see the changes being made in RN almost instantly, compared to >20 seconds that native code would typically take to build.
Summary
By putting just a little effort to integrate React Native into our application stack, we quickly realized the advantages it would bring to our organization. There may be cases where React Native is not the right choice, but for us, it works great for our Textbook Solutions product and we look forward to building others using this technology. We hope this summary helps you get started on your React Native integration journey.
Originally posted at Course Hero Engineering Blog
Top comments (0)