Navigation and Deep Linking with React Native

by Betty Tran

Mobile deep links are an essential part of a mobile application’s business strategy. They allow us to link to specific screens in a mobile application by using custom URL schemas. To implement deep links on the Android platform, we create intent filters to allow Android to route intents that have matching URLs to the application runtime. For iOS, we define the custom URL schema and listen for incoming URLs in the app delegate. It’s possible to create a custom module for React Native which wraps this native functionality, but fortunately React Native now ships with the APIs we need for deep linking. This article provides an overview of how to get deep links working in a React Native application for both Android and iOS platforms. We will also cover the basics of routing URLs and handling screen transitions.

The accompanying example application code can be found here. Note that at the time of writing this article, the current stable version of React Native is 0.26.

Setting up the navigation

In this example, we will use React’s Navigator component to manage the routes and transitions in the app. For non-trivial apps, you may want to look into React’s new NavigatorExperimental component instead as it leverages Redux style navigation logic. 

We can use React’s Navigator component to manage the routes and transitions in the app. First make sure the component is imported:

import {
  Navigator, // Add Navigator here to import the Navigator component
  StyleSheet,
  Platform,
  Text,
  View,
  ToolbarAndroid,
  SegmentedControlIOS,
  Linking
} from 'react-native';

Next, we need to define the route objects. In this example, are two routes, “home” and “account”. You can also add custom properties to each route so that they are accessible later when we want to render the screen associated to the route.

const App = React.createClass({
  ...
  getInitialState() {
    return {
      routes: {
        home: {
          title: 'Home',
          component: HomeView
        },
        account: {
          title: 'My Account',
          component: AccountView
        }
      }
    };
  },

The Navigator component is then returned in the render function:

render() {
  return (
    <Navigator
      ref={component => this._navigator = component}
      navigationBar={this.getNav()}
      initialRoute={this.state.routes.home}
      renderScene={(route, navigator) => 
        <route.component {...route.props} navigator={navigator} />}
    />
  );
},

To retain a reference to the navigator component, a function can be passed to the ref prop. The function receives the navigator object as a parameter allowing us to set a reference to it for later use. This is important because we need to use the navigator object for screen transitions outside the scope of the navigator component. 

A navigation bar that persists across all the screens can be provided using the navigationBar prop. A function is used here to return either a ToolbarAndroid or SegmentedControlIOS React component depending on which platform the code is running on. There are two screens, we can transition to using the navigation bar here.

getNav() {
  if (Platform.OS === 'ios') {
    return ( <
      SegmentedControlIOS values = {
        [this.state.routes.home.title, this.state.routes.account.title]
      }
      onValueChange = {
        value => {
          const route = value === 'Home' ? this.state.routes.home : this.state.routes.account;
          this._navigator.replace(route);
        }
      }
      />
    );
  } else {
    return ( <
      ToolbarAndroid style = {
        styles.toolbar
      }
      actions = {
        [{
            title: this.state.routes.home.title,
            show: 'always'
          },
          {
            title: this.state.routes.account.title,
            show: 'always'
          }
        ]
      }
      onActionSelected = {
        index => {
          const route = index === 0 ? this.state.routes.home : this.state.routes.account;
          this._navigator.replace(route);
        }
      }
      />
    );
  }
}

The initialRoute prop lets the navigator component know which screen to start with when the component is rendered and the renderScene prop accepts a function for figuring out and rendering a scene for a given route.

With the Navigator component in place, we can transition forwards to a new screen by calling the navigator’s replace function and passing a route object as a parameter. Since we set a reference to the navigator object earlier, we can access it as follows:

this._navigator.replace(route);

We can also use this._navigator.push here if we want to retain a stack of views for back button functionality. Since we are using a toolbar/segmented controls and do not have back button functionality in this example, we can go ahead and use replace.

Notice that the parameters passed to onActionSelected and onValueChange are 0 and ‘Home` respectively. This is due to the differences in the ToolbarAndroid and SegmentedControlIOS components. The argument passed to the method from the ToolbarAndroid component is an integer denoting the position of the button. A string with the button value is passed for the SegmentedControlIOS component.

Now that we have a simple application that can transition between two screens, we can dive into how we can link directly to each screen using deep links.

Android

Defining a custom URL

In order to allow deep linking to the content in Android, we need to add intent filters to respond to action requests from other applications. Intent filters are specified in your android manifest located in your React Native project at /android/app/src/main/java/com/[your app]/AndroidManifest.xml. Here is the modified manifest with the intent filter added to the main activity:

<activity android:name=".MainActivity" android:label="@string/app_name" android:configChanges="keyboard|keyboardHidden|orientation|screenSize">
  <intent-filter>
    <action android:name="android.intent.action.MAIN" />
    <category android:name="android.intent.category.LAUNCHER" />
  </intent-filter>
  <intent-filter android:label="filter_react_native">
    <action android:name="android.intent.action.VIEW" />
    <category android:name="android.intent.category.DEFAULT" />
    <category android:name="android.intent.category.BROWSABLE" />
    <data android:scheme="deeplink" android:host="home" />
  </intent-filter>
</activity>

The <data> tag specifies the URL scheme which resolves to the activity in which the intent filter has been added. In this example the intent filter accepts URIs that begin with deeplink://home. More than one URI scheme can be specified using additional <data> tags. That’s it for the native side of things, all that’s left to do is the implementation on the JavaScript side.

Receiving the intent data in React

React’s Linking API, allows us to access any incoming intents. In the componentWillMount() function, we retrieve the incoming intent data and figure out which screen to render. Building on the navigation example above, we can add the following componentDidMount() function:

componentDidMount() {
  const url = Linking.getInitialURL().then(url => {
    if (url) {
      const route = url.replace(/.*?:\/\//g, "");
      this._navigator.replace(this.state.routes[route]);
    }
  });
}

The getIntialURL() function returns the URL that started the activity. Here we are using a string replace function to get the part of the URL string after deeplink://. this.navigator.replace is called to transition to the requested screen.

Using the Android Debug Bridge (ADB), we can test the deep links via the command line.

$ adb shell am start
        -W -a android.intent.action.VIEW
        -d deeplink://home(the url scheme) com.deeplinkexample(the app package name)

Alternatively, open a web browser on the device and type in the URL scheme. The application should launch and open the requested screen.

IOS

Defining a custom URL

In iOS, we register the URL scheme in the Info.plist file which can be found in the React Native project at /ios/[project name]/Info.plist. The url scheme defined in the example is deeplink://. This allows the application to be launched externally using the url.

<key>CFBundleURLTypes</key>
<array>
  <dict>
    <key>CFBundleTypeRole</key>
    <string>Editor</string>
    <key>CFBundleURLSchemes</key>
    <array>
      <string>deeplink</string>
    </array>
  </dict>
</array>

We also need to add a few extra lines of code to AppDelegate.m to listen for incoming links to the application when it has already been launched and is running.

#import "RCTLinkingManager.h"
- (BOOL)application:(UIApplication *)application openURL:(NSURL *)url
  sourceApplication:(NSString *)sourceApplication annotation:(id)annotation
{
  return [RCTLinkingManager application:application openURL:url sourceApplication:sourceApplication annotation:annotation];
}

 If we try to compile the project in XCode at this point we will get a RCTLinkingManager not found error. In order to use the LinkingIOS library, we need to manually link to the library in XCode.  There are detailed instructions in the official documentation for how to do this. The library we need to link is named RCTLinking and header search path we need to add is:

$(SRCROOT)/../node_modules/react-native/Libraries

xcode screenshot

Receiving the URL in React

React’s Linking API provides an event listener for incoming links. Note that the event listeners are only available for IOS. We want to add the event listener when the component mounts and ensure that it is removed when the component is un-mounted to avoid any memory leaks. When an incoming link is received, the handleDeepLink function is executed which calls this.navigator.replace to transition to the requested screen.

componentDidMount() {
  Linking.addEventListener('url', this.handleDeepLink);
},
componentWillUnmount() {
  Linking.removeEventListener('url', this.handleDeepLink);
},
handleDeepLink(e) {
  const route = e.url.replace(/.*?:\/\//g, "");
  this._navigator.replace(this.state.routes[route]);
}

Test the deep link by visiting the URL in a web browser on the device.

Summary

In this article we covered how to enable deep links and screen transitions in a React Native application. Although the deep link implementation involves a small amount of work using native Android/iOS code, the available React Native APIs allow us to easily bridge the gap between native and javascript code, and leverage the JavaScript side to write the logic that figures out where the deep links should go. It’s worth noting that this article only serves as an example. For more complex real world applications, the NavigatorExperimental component should be considered as a more robust solution. We hope you have as much fun trying out mobile deep linking as we did!

newsletter-bot