Deep linking
In this guide we will set up our app to handle external URIs.
Deep-link integration
To handle incoming links, we need to handle 2 scenarios:
- If the app wasn't previously open, we need to set the initial state based on the link
- If the app was already open, we need to update the state to reflect the incoming link
To handle a deep link, we need to translate it to a valid navigation state. The library exports a getStateFromPath
utility to convert a URL to a state object.
For example, the path /rooms/chat?user=jane
will be translated to a state object like this:
{
routes: [
{
name: 'rooms',
state: {
routes: [
{
name: 'chat',
params: { user: 'jane' },
},
],
},
},
],
}
The useLinking
hook makes it easier to handle incoming links:
import { NavigationContainer, useLinking } from '@react-navigation/native';
function App() {
const ref = React.useRef();
const { getInitialState } = useLinking(ref, {
prefixes: ['https://mychat.com', 'mychat://'],
});
const [isReady, setIsReady] = React.useState(false);
const [initialState, setInitialState] = React.useState();
React.useEffect(() => {
Promise.race([
getInitialState(),
new Promise(resolve =>
// Timeout in 150ms if `getInitialState` doesn't resolve
// Workaround for https://github.com/facebook/react-native/issues/25675
setTimeout(resolve, 150)
)
])
.catch(e => {
console.error(e);
})
.then(state => {
if (state !== undefined) {
setInitialState(state);
}
setIsReady(true);
});
}, [getInitialState]);
if (!isReady) {
return null;
}
return (
<NavigationContainer initialState={initialState} ref={ref}>
{/* content */}
</NavigationContainer>
);
}
Note: The
getInitialState
function uses React Native'sLinking.getInitialUrl()
under the hood. Currently there seems to be bug (facebook/react-native#25675) which results in it never resolving on Android.
Often, directly translating path segments to route names may not be the expected behavior. For example, you might want to parse the path /feed/latest
to something like:
{
routes: [
{
name: 'Chat',
params: {
sort: 'latest',
},
},
];
}
You can specify a separate config
option to control how the deep link is parsed to suit your needs.
const { getInitialState } = useLinking(ref, {
prefixes: ['https://mychat.com', 'mychat://'],
config: {
Chat: 'feed/:sort',
},
});
Here Chat
is the name of the screen that handles this URL. The navigator will need to have a Chat
screen which handles a sort
param for the route:
You can also customize how params are parsed, for example, if you parse the path /item/42
as item/:id
, the param id
will be parsed as string by default. But you can customize it by passing a function:
{
Catalog: {
path: 'item/:id',
parse: {
id: Number,
},
},
}
This will result in something like:
const state = {
routes: [
{
name: 'Catalog',
params: { id: 42 },
},
],
};
It's important to note that the state object must match the hierarchy of nested navigators. Otherwise the state will be discarded.
Sometimes we'll have the target navigator nested in other navigators which aren't part of the deep link. For example, let's say our navigation structure looks this:
function Home() {
return (
<Tab.Navigator>
<Tab.Screen name="Profile" component={Profile} />
<Tab.Screen name="Notifications" component={Notifications} />
</Tab.Navigator>
);
}
function App() {
return (
<Stack.Navigator>
<Stack.Screen name="Home" component={Home} />
<Stack.Screen name="Settings" component={Settings} />
</Stack.Navigator>
);
}
Here we have a stack navigator in root, and inside the Home
screen of the root stack, we have a tab navigator with various screens. With this structure, let's say we want the path /users/:id
to go to the Profile
screen. We can express the nested config like so:
{
Home: {
screens: {
Profile: 'users/:id',
},
},
}
In this config, we specify that the Profile
screen should be resolved for the users/:id
pattern and it's nested inside the Home
screen. Then parsing users/jane
will result in the following state object:
const state = {
routes: [
{
name: 'Home',
state: {
routes: [
{
name: 'Profile',
params: { id: 'jane' },
},
],
},
},
],
};
Sometimes we want to ensure that a certain screen will always be present for the navigator in the state object. We can achieve it by specifying the initialRouteName
property for that navigator in the config. For the above example, if we want the Notifications
screen to be the initial route in Home
tab navigator, we should specify such config (Notifications
screen needn't be mentioned in screens
property):
{
Home: {
initialRouteName: 'Notifications',
screens: {
Profile: 'users/:id',
Notifications: 'notify/:user',
},
},
};
Then, the path /users/42
will resolve to the following state object:
const state = {
routes: [
{
name: 'Home',
state: {
index: 1,
routes: [
{
name: 'Notifications',
},
{
name: 'Profile',
params: { id: 'jane' },
},
],
},
},
],
};
Notice that we can't pass any params to the Notifications
screen if it isn't explicitly mentioned in the URL string, so the screen should implement handling lack of these params with e.g. providing default ones.
For some advanced cases, specifying the mapping may not be sufficient. You can implement your custom parser to address these cases using the getStateFromPath
option:
const { getInitialState } = useLinking(ref, {
prefixes: ['https://mychat.com', 'mychat://'],
config: {
Chat: 'feed/:sort',
},
getStateFromPath: (path, options) => {
// Implement your custom parser and return the navigation state here
},
});
Set up with Expo projects
First, you will want to specify a URL scheme for your app. This corresponds to the string before ://
in a URL, so if your scheme is mychat
then a link to your app would be mychat://
. The scheme only applies to standalone apps and you need to re-build the standalone app for the change to take effect. In the Expo client app you can deep link using exp://ADDRESS:PORT
where ADDRESS
is often 127.0.0.1
and PORT
is often 19000
- the URL is printed when you run expo start
. If you want to test with your custom scheme you will need to run expo build:ios -t simulator
or expo build:android
and install the resulting binaries in your emulators. You can register for a scheme in your app.json
by adding a string under the scheme key:
{
"expo": {
"scheme": "mychat"
}
}
URI Prefix
Next, let's configure our navigation container to extract the path from the app's incoming URI.
import { Linking } from 'expo';
const prefix = Linking.makeUrl('/');
function App() {
const ref = React.useRef();
const { getInitialState } = useLinking(ref, {
prefixes: [prefix],
});
const [isReady, setIsReady] = React.useState(false);
const [initialState, setInitialState] = React.useState();
React.useEffect(() => {
getInitialState()
.catch(() => {})
.then(state => {
if (state !== undefined) {
setInitialState(state);
}
setIsReady(true);
});
}, [getInitialState]);
if (!isReady) {
return null;
}
return (
<NavigationContainer initialState={initialState} ref={ref}>
{/* content */}
</NavigationContainer>
);
}
The reason that is necessary to use Expo.Linking.makeUrl
is that the scheme will differ depending on whether you're in the client app or in a standalone app.
Test deep linking on iOS
To test the URI on the simulator in the Expo client app, run the following:
xcrun simctl openurl booted [ put your URI prefix in here ]
# for example
xcrun simctl openurl booted exp://127.0.0.1:19000/--/chat/jane
Test deep linking on Android
To test the intent handling in the Expo client app on Android, run the following:
adb shell am start -W -a android.intent.action.VIEW -d "[ put your URI prefix in here ]" host.exp.exponent
# for example
adb shell am start -W -a android.intent.action.VIEW -d "exp://127.0.0.1:19000/--/chat/jane" host.exp.exponent
Change host.exp.exponent
to your app package name if you are testig on a standalone app.
Read the Expo linking guide for more information about how to configure linking in projects built with Expo.
react-native init
projects
Set up with iOS
Let's configure the native iOS app to open based on the mychat://
URI scheme.
You'll need to link RCTLinking
to your project by following the steps described here. To be able to listen to incoming app links, you'll need to add the following lines to SimpleApp/ios/SimpleApp/AppDelegate.m
.
If you're targeting iOS 9.x or newer:
// Add the header at the top of the file:
#import <React/RCTLinkingManager.h>
// Add this above `@end`:
- (BOOL)application:(UIApplication *)application
openURL:(NSURL *)url
options:(NSDictionary<UIApplicationOpenURLOptionsKey,id> *)options
{
return [RCTLinkingManager application:application openURL:url options:options];
}
If you're targeting iOS 8.x or older, you can use the following code instead:
// Add the header at the top of the file:
#import <React/RCTLinkingManager.h>
// Add this above `@end`:
- (BOOL)application:(UIApplication *)application openURL:(NSURL *)url
sourceApplication:(NSString *)sourceApplication annotation:(id)annotation
{
return [RCTLinkingManager application:application openURL:url
sourceApplication:sourceApplication annotation:annotation];
}
If your app is using Universal Links, you'll need to add the following code as well:
// Add this above `@end`:
- (BOOL)application:(UIApplication *)application continueUserActivity:(nonnull NSUserActivity *)userActivity
restorationHandler:(nonnull void (^)(NSArray<id<UIUserActivityRestoring>> * _Nullable))restorationHandler
{
return [RCTLinkingManager application:application
continueUserActivity:userActivity
restorationHandler:restorationHandler];
}
In Xcode, open the project at SimpleApp/ios/SimpleApp.xcodeproj
. Select the project in sidebar and navigate to the info tab. Scroll down to "URL Types" and add one. In the new URL type, set the identifier and the URL scheme to your desired URL scheme.
Now you can press play in Xcode, or re-build on the command line:
react-native run-ios
To test the URI on the simulator, run the following:
xcrun simctl openurl booted mychat://chat/jane
To test the URI on a real device, open Safari and type mychat://chat/jane
.
Android
To configure the external linking in Android, you can create a new intent in the manifest.
In SimpleApp/android/app/src/main/AndroidManifest.xml
, do these following adjustments:
- Set
launchMode
ofMainActivity
tosingleTask
in order to receive intent on existingMainActivity
. It is useful if you want to perform navigation using deep link you have been registered - details - Add the new
intent-filter
inside theMainActivity
entry with aVIEW
type action:
<activity
android:name=".MainActivity"
android:launchMode="singleTask">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="mychat" />
</intent-filter>
</activity>
Now, re-install the app:
react-native run-android
To test the intent handling in Android, run the following:
adb shell am start -W -a android.intent.action.VIEW -d "mychat://chat/jane" com.simpleapp
Hybrid iOS Applications (Skip for RN only projects)
If you're using React Navigation within a hybrid app - an iOS app that has both Swift/ObjC and React Native parts - you may be missing the RCTLinkingIOS
subspec in your Podfile, which is installed by default in new RN projects. To add this, ensure your Podfile looks like the following:
pod 'React', :path => '../node_modules/react-native', :subspecs => [
. . . // other subspecs
'RCTLinkingIOS',
. . .
]