How to implement deep links with Expo Router
19 Feb 2025
If you're wanting to introduce attribution based deeplinking into your app using Expo router and struggling to find a solution to bypass the default link handling you will find the answer here.
Expo Router
18 months ago I was bootstrapping a new React Native app for my company and I decided to take a punt on using Expo Router as an alternative to React Navigation.
At the time it felt very raw, but because we were already using Next.js and the rest of our team had limited React Native experience I thought that having our mobile app structure closely mimic our website would make the transition smoother for web devs.
I found the web-like routing transform the way I thought about mobile development. By treating each page as a url type structure you have to think more closely about what data is passed and where it is fetched from. Combining this approach with react-query lead to an incredibly simple project structure with no need for much client side logic at all.
I also really enjoy the nested file structure for things like authenticated routes and tab layouts. What would have been difficult or quite manual with navigation is solved by simply the placement of a file. If you want something to happen after login i.e your deeplink handler, you just nest it within the authenticated layout.
Branch
Because Expo Router was (and still is) so new, I came across a few things that just weren't possible or scarcely documented, one of which was deeplinking. In my case I ran into a blocker with Branch.io (side note, the branch docs are terrible).
By default, Expo Router handles linking in your app for you, by virtue of the file structure, just like a website. This is great, because you don't have to actively think about this for each page like you would with React Navigation. However when it comes to using things like branch links, this gets in the way.
Branch links are formatted with a custom domain and a path which is usually a random string. To get these to work with any app you need to configure the domains. This is easy enough following the Expo docs for Android App Links and iOS Universal Links.
Unfortunately, Expo Router then tries to automatically handle the path, and when it is a random branch string this will lead to a not found error.
The fix
After lurking in the Expo discord for months I finally discovered a hidden doc on overriding native intents. The solution suggested is far from ideal however, because you probably don't have the required context in this file to make a decision on where to send the user. The answer is as easy as the following:
export async function redirectSystemPath() {
return '/';
}
That's it! This will essentially void the Expo routing functionality and pass it off into your app. From here you are free to handle Branch links like in a regular react-native app. As a bonus, you can place this hook within your authenticated route layout so it only gets hit after the user is logged in!
import Constants from 'expo-constants';
import { Href, router } from 'expo-router';
import { useEffect } from 'react';
import branch, { BranchParams } from 'react-native-branch';
function handleDeeplink(params: BranchParams | undefined) {
if (!params) {
return null;
}
if (params['+non_branch_link']) {
const link = params['+non_branch_link'] as string;
const { pathname, protocol } = new URL(link);
if (
Constants.expoConfig?.scheme &&
protocol.startsWith(Constants.expoConfig?.scheme as string)
) {
const match = link.match(
new RegExp(`^${Constants.expoConfig.scheme}://(.*)`),
);
if (match) router.navigate(match[1] as Href<string>);
return;
}
if (pathname) {
router.navigate(pathname as Href<string>);
return;
}
}
if (!params['+clicked_branch_link']) {
return null;
}
if (params['$deeplink_path']) {
router.navigate(params['$deeplink_path'] as Href<string>);
}
}
// Used instead of +native-intent.ts
export function useDeeplinkObserver() {
useEffect(() => {
branch.getLatestReferringParams().then((params) => {
handleDeeplink(params);
});
branch.subscribe(({ error, params }) => {
handleDeeplink(params);
});
}, []);
}
Note that depending on what you want to acheive you may need the getLatestReferringParams
as well as the subscribe
method. The subscribe
method will be triggered when the app is already open and the user clicks a branch link, whereas the getLatestReferringParams
will be triggered when the app is opened from a branch link.
This code subscribes to deeplink events using the branch SDK and handles them. Branch returns an object to say whether the deeplink is from branch or not. If the link contains the url scheme configured for Expo then we extract the path from it. Otherwise we assume it's a normal router link and we navigate to that. If it is a branch link then we use the $deeplink_path query param and route to that.
Hopefully as time goes on this will be better documented, but I am happy to have finally found the solution to getting this working. I don't think I ever want to go back to React Navigation, let alone just for the sake of deeplinks!
Loading...