heycar
Published in

heycar

From React Native to Native

Photo by Kolleen Gladden on Unsplash

At heycar we have been working with React Native since the beginning and like every tool it has its ups and downs. Today I will be explaining one of the downs that we faced and how we overcame this problem using native modules.

React Native provides us with the ability to communicate with the native Android and iOS code using native modules. Generally this is not required for straightforward applications, especially if it’s a new, up and coming application. The most common scenario for dealing with the native code would be when a specific dependency is needed to be added to the project and even then it’s usually very simple and explained thoroughly in the documentation of the dependency. When there is good documentation, it doesn’t include a lot of changes outside of Podfile or build.gradle.

There are still some cases where we needed to or preferred to disregard third party libraries and have our own implementations. One of the most common issues that we see is unstable or abandoned libraries. This can possibly be ignored for widespread and stable libraries, but not for smaller ones.

One of the cases that we opted for such an implementation was the in-app review API. While this has some libraries around which support it, we faced some bugs even with such a small implementation. On top of this, the realization of the effect that even small libraries have on our app bundle size was important. Even if a library is very small, it takes up more space than one line of native code, that is for certain.

Implementing our own native code also requires the maintenance of that code, but in my experience, this is better than depending on a library that might become unstable with some SDK update. Everyone seems to have a higher disregard for third party implementations, which makes it harder to catch bugs in them and even harder to fix these bugs, especially if the library is not even maintained anymore. Then a whole new search for a decent implementation would begin.

Another case we chose to use native implementation was about performance. If you push React Native’s buttons, this can lead to some serious performance issues. That’s why performance should be one of the highest priorities while developing a React Native application. One example for this is, when we decided that our beautiful fullscreen image gallery should allow users to change its orientation.

Unlike its peers, React Native renders everything in a very specific way. As well as native APIs, it also requests native components and depending on the response it gets, it renders things. You can check out more details about React Native’s core components and their corresponding views on the React Native website.

React Native Bridge

The initial attempt was simply just enabling the orientation capability only when the fullscreen gallery is active since we didn’t want the rest of the application to rotate as well. To put it simple, this made the whole thing unusable, when the user tried to rotate it would just stutter for 5 seconds and show the background content and become chaotic for a while and then finally move into the correct place, which was really not working for us. Keep in mind that the heycar application is a big application with so many components inside it, on top of that, our image gallery was deep inside the routes and not on the main screen where there would have been fewer components to render.

We tried a lot of combinations to make it work, but there were close-but-not-satisfying results. React Native uses one Activity and one main class to render everything inside it, which makes it really hard to find a way around such cases.

Finally we decided to implement native image galleries for Android and iOS separately. The logic was rather simple, we used the React Native bridge to start a completely new Activity that’s isolated from what is already being used and sent the necessary values as parameters to this new .

When we did this, our gallery looked and performed like this:

Compared to having 5 second stutters, this looked like a miracle. Now all that’s left was connecting this back to the Javascript code.

An issue that represents itself at this point is communicating back to React Native about what is happening in the newly opened activity. Although this can easily be overcome with startActivityForResult for Android (more below about we didn’t use this) and simply by passing the callback function to the new activity on iOS.

At this point, you might be wondering why deal with such a messy way of implementing the callbacks instead of just doing whatever is needed on the native side. As this was the initial wish for us as well, it was sadly not possible. React Native itself and most libraries that are used on React Native do not actually expose their native API on the native side as they are not intended to be used there. This means we would have to actually implement any library that we want to use on the native side as well, which means, more implementations, bigger bundle size and more tech debt. So by any means, we wanted to avoid this.

For iOS it was relatively simpler to do this. We had to create bridge functions that trigger emitters in React Native, pass these bridge functions into the new fullscreen gallery activity and just trigger the functions from there.

Here’s how this goes in order. First, we create listeners for native event emitters or just listeners for native code to trigger them.

type NativeCallback = (event: GallerySwipeEmitterData) => void;export const subscribeFullscreenSwipe = (callback: NativeCallback) => eventEmitter.addListener('gallerySwipeEmitter', callback);

Now we have our listener with the callback and the identifying name gallerySwipeEmitter.

After this, comes the bridge function that uses the same identifying name.

- (void)gallerySwipeEvent: (NSInteger) index
{
[self sendEventWithName:@"gallerySwipeEmitter" body:@{@"index": @(index)}];
}

And another bridge function that actually triggers the navigation to the new Activity.

RCT_EXPORT_METHOD(navigate: (NSArray *)urls: (NSInteger) initialIndex: (NSString *) videoRequestText)
{
[
[[ImageGalleryManager alloc] init]
navigateWithUrls:urls
initialIndex:initialIndex
videoRequestText: videoRequestText
imageGallery: self
];
}

As you may have noticed, the self is also passed here. This means the gallerySwipeEvent function will also be reachable for us.

Now that we also have that in place, the only thing that’s left is the navigation and for that we use a manager in Swift called ImageGalleryManager and it has one method that takes every parameter from the above navigate method and triggers the navigation controller to navigate to the new page.

let imgVC = storyboard.instantiateViewController(withIdentifier: "ImageGalleryViewController") as! ImageGalleryViewControllerimgVC.onSwipe = imageGallery.gallerySwipeEventappDelegate.navigationController.pushViewController(imgVC, animated: true)

This way we could pass the callback function to the new screen and actually simultaneously call the swipe event whenever we want, which would trigger the very first listener subscribeFullscreenSwipe.

For Android, unfortunately we couldn’t simply pass a method like that, but things turned out to be even simpler. The first plausible way we could think of was using startActivityForResult. The problem with this is, we cannot immediately communicate to React Native about what is happening on the new activity and if the user actually kills the application while they are on that activity, this data would simply be lost.

As a solution to this, we went to the MainApplication of our Android project. React Native runs on MainActivity, but this MainActivity is also wrapped by MainApplication, which actually carries a ReactContext. This magical context gives us access to all the capabilities of the react context including emitting. Having our image gallery activity also under MainApplication, it was only a matter of getting the react context

reactContext = (application as MainApplication)
.reactNativeHost
.reactInstanceManager
.currentReactContext

and then emitting the event we want:

reactContext
?.getJSModule(
DeviceEventManagerModule.RCTDeviceEventEmitter::class.java
)
?.emit("gallerySwipeEmitter", payload)

With that, all our connections back to React Native were also established.

When I think about whether this was worth it, I can easily say that it is, especially when I put user experience into consideration, this seemed like a must in this case. Of course, it also comes with its own luggage, a team may not always have people who can maintain native code and it also splits the workload. If these risks are things that your team can take on, then sometimes adding sprinkles of native code helps with the improvement of the application.