Heading image for post: Tips and Tricks from Integrating React Native with Existing Mobile Apps

Tips and Tricks from Integrating React Native with Existing Mobile Apps

Profile picture of Gabriel Reis

A lot of clients come to Hashrocket with an existing mobile app and they expect us to deliver new features as soon as possible. With an easy integration, native performance, support for the main platforms and great development experience, React Native has proved to be the right choice for maintaining existing mobile apps.

This blog post describes some of the tips and tricks that we face when we integrate React Native with existing apps. The examples are mostly for iOS but they apply for Android on most of the cases.

Introducing RCTRootView

The integration between React Native and native code is straightforward. For iOS, React Native exposes a UIView subclass called RCTRootView that can be embedded in any part of your app. Here is a simple example that adds a RCTRootView as a subview of a UIViewController's view:

-(void)viewDidLoad {

  ...

  NSURL *jsUrl = [NSURL URLWithString:@"http://localhost:8081/index.ios.bundle?platform=ios"];

  self.reactView = [[RCTRootView alloc] 
      initWithBundleURL:jsUrl
      moduleName:@"SampleApp"
      initialProperties:nil
      launchOptions:nil];

  [self.view addSubview:self.reactView];
}

To read more about setting up the integration with your existing app, just follow the official docs.

Now we need to learn how we can send messages between native code and React Native and vice versa.

Sending messages from native code to React

There are basically two ways to send messages from native code to a React view: using properties or event dispatcher.

Using properties

React components talk to each other through properties (props) and you can use the same approach to send messages from native code to a react view. All RCTRootView have a property called appProperties where you can set a dictionary that can be used on the React component:

self.reactview.appProperties = @{@"status": @"success"};

Every time the appProperties is changed React will re-render the root component:

render() {
  return <Text>{this.props.status}</Text>
}

But be careful with boolean values. Let say you have a pull-to-refresh feature on a regular UITableView and one of the cells is rendered by React. So the first time you pull to refresh the list, the React cell should also refresh and you set a reload prop to YES:

-(void)onRefresh {
  self.reactView.appProperties = @{@"reload": @YES};
}

And on the React component you check if the reload prop is set when the component receives new props and reload the data:

componentWillReceiveProps(nextProps) {
  if (nextProps.reload) {
    this._reloadData();
  }
}

So the first time this happens it works fine, but the second time you pull to refresh the list nothing is reloaded. This happens because you are setting the same boolean value for the same prop @{@"reload": @YES}. React will not propagate the props if they haven't changed. So one way to solve this issue is just setting a timestamp for the prop instead of a boolean value:

-(void)onRefresh {
  self.reactView.appProperties = @{
    @"reload": @(CFAbsoluteTimeGetCurrent())
  };
}

Using Event Dispatcher

The other way to communicate to a React component is using the event dispatcher. All bridges from a RCTRootView have a reference to a global event dispatcher where you can dispatch events:

[self.reactView.bridge.eventDispatcher 
             sendDeviceEventWithName:@"statusDidChange"
             body:@{@"status": status}];

And on your react component just add a listener to the event:

componentWillMount() {
  this._listener = DeviceEventEmitter.addListener("statusDidChange", (event) => {
    console.log(event.status);
  });
}

And don't forget to remove the listener when the component unmounts:

componentWillUnmount() {
  this._listener.remove();
}

You could also use the Subscribable mixin that automatically removes the listeners when the component will unmount.

*It looks like sendDeviceEventWithName is deprecated now in favor of a more generic method called sendEvent.

Sending messages from React to native code

The RCTRootView has no interface to receive messages from the React component but the way to achieve this is writing native modules.

A native module is a class that defines methods that can be exposed to the javascript code. They are singletons, meaning they are shared between multiple React view instances. On the example below we can write a generic native module that uses NSNotificationCenter to broadcast messages from React to the rest of the native app:

@interface NotificationsModule : NSObject<RCTBridgeModule>
@end

@implementation NotificationsModule

RCT_EXPORT_MODULE();

RCT_EXPORT_METHOD(sendNotification:(NSString*)notification params:(NSDictionary*)params) {
  [[NSNotificationCenter defaultCenter] 
      postNotificationName:event 
      object:nil 
      userInfo:params];
}

@end

And on your React component you can just call the same method that was exported:

import { NativeModules } from 'react-native';
const NotificationsModule = NativeModules.NotificationsModule;

render() {
  return <TouchableOpacity onPress={this._onPress} />
}

_onPress() {
  const params = { rootTag: this.props.rootTag, userId: this.props.userId };
  NotificationsModule.sendNotification("openUserScreen", params);
}

Note that we are setting the prop rootTag on the notification params. All React root components have a property called rootTag that identifies the React View RCTRootView. In some cases you only need to handle notifications from a specific RCTRootView and comparing the rootTag is the way to do this. In order to access the rootTag from the RCTRootView you will have to include the category UIView+React.h that will expose the method reactTag:

#import "UIView+React.h"

-(void)viewDidLoad {
  [[NSNotificationCenter defaultCenter] 
      addObserver:self 
      selector:@selector(didReceiveOpenUserNotification) 
      name:@"openUserScreen"
      object:nil];
}

-(void)didReceiveOpenUserNotification:(NSNotification *)notification {
  NSDictionary *params = notification.userInfo;
  if ([self.reactView.reactTag isEqualToNumber:params.rootTag]) {
    [self openUserController:params.userId];
  }
}

Custom native views as components

Another nice thing is that you can bring to React Native any existing custom UIView that you wrote. You just need to create a new class that inherits from RCTViewManager, then implement the view method to return the instance of your custom view and then expose the properties you want to be available on the React component:

@interface GoCoderManager : RCTViewManager
@end

@implementation GoCoderManager

RCT_EXPORT_MODULE(GoCoder)

-(UIView *)view {
  return [GoCoderView new];
}

RCT_EXPORT_VIEW_PROPERTY(initialCamera, NSString);
RCT_EXPORT_VIEW_PROPERTY(onImageCaptured, RCTBubblingEventBlock);

@end

Note that the second property exported in the example is a type of RCTBubblingEventBlock. This is the way you can send events from native custom views to the parent component that is using it.

To use this custom component inside React:

import {
  requireNativeComponent,
} from 'react-native';

const GoCoder = requireNativeComponent('GoCoder', null);

class ParentComponent extends Component {
  render() {
    return (
      <View>
        <GoCoder initialCamera="back" onImageCaptured={() => {
            alert("Image captured!")
        }} />
      </View>
    );
  }
}

To see more about this integration read the official docs.

Touchable React views inside scrollable native views

Let's say you have a UITableView filling the whole screen and one of the cells is a React Native view. You may see some strange behaviors now. When you are scrolling down the table and the first touch happens inside the React view, it will fire that event as a regular touch event to the React view causing your app to go to another screen instead of scrolling down the page.

One way to fix this issue is implementing the method scrollViewDidScroll from UIScrollViewDelegate and call cancelTouches on the React View:

- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
  [self.reactView cancelTouches];
}

Component Size

You don't need to specify the width and height for your components. React Native uses flexbox to style the views and calculates everything for you. But sometimes you need to know the exact height of a component. One example is when you have a UITableView and one of the cells is a React view. On iOS you have to specify the height of each cell so you need to know the height of the component. To get this data you will have to set onLayout prop on your component:

render() {
  return (
    <View onLayout={this._onLayout)}>
      ...
    </View>
  );
}

_onLayout(event) {
  const { height } = event.nativeEvent.layout;
  const params = { rootTag: this.props.rootTag, height };
  NotificationsModule.sendNotification("componentHeightDidChange", params);
}

Detecting orientation changes

You can detect the orientation of your device using the onLayout prop that was mentioned above on the root component of your app:

render() {
  return (
    <View onLayout={this._onLayout}>
      ...
    </View>
  );
}

_onLayout(event) {
  const { width, height } = event.nativeEvent.layout;
  const orientation = width > height ? 'landscape' : 'portrait';
  this.setState({orientation});
}

Relative units

You should not be writing conditions on your code to support all the different available screen sizes. One way to avoid this conditions is using relative units based on the device's width.

There is this library react-native-extended-stylesheet that adds extra features to the default Stylesheet class, including rem support. On the example below the rem is set to the device width divided by 32. We've chosen 32 because it makes 1rem on the iPhone 5 to be equivalent to 10 pixels (base-10 number system is always easy for us).

EStyleSheet.build({
  rem: Dimensions.get('window').width / 32,
});

const styles = EStyleSheet.create({
  text: {
    fontSize: '1.4rem'
  }
});

It's worth mentioning that this approach may not work properly if your component needs to support landscape and portrait mode.

Globally update default component props

On your index.ios.js you can get any component class and override the default prop. One example that we had to set was the property allowFontScaling to false because just few screens were supported.

import { Text } from 'react-native';

Text.defaultProps.allowFontScaling = false;

NavigationExperimental is a new way to write navigation logic that decouples the state from the component. The NavigationExperimental has a RFC that changed a lot over the past months and depending on the version you started using it may have changed completely. Our advice is to avoid using experimental features until they are mature enough or try to update React Native every time a new release comes up (every 2 weeks) so you are not that behind the latest changes on the specification.

JS errors in release builds

Different from development builds that show a big red error screen, your app will crash in production if a JS error happens. The good thing is that if you are using a service to track errors like Crashlytics or NewRelic, you will get a nice error message and you will be able to identify on your JS code what caused the error.

Overflow hidden/visible

There is a difference on how iOS and Android deal with the overflow property. The overflow: hidden on Android doesn't look like it has any effect. So our advice is to try to avoid using it until this is fully supported.

More to cover

There are a lot more to talk about the React and React Native ecosystem that we want to cover on other blog posts:

  • testing: jest, snapshot tests, shallow rendering
  • redux: middlewares
  • graphql, relay and apollo client
  • fastlane integration

More posts about Mobile react-native

  • Adobe logo
  • Barnes and noble logo
  • Aetna logo
  • Vanderbilt university logo
  • Ericsson logo

We're proud to have launched hundreds of products for clients such as LensRentals.com, Engine Yard, Verisign, ParkWhiz, and Regions Bank, to name a few.

Let's talk about your project