Deep Linking Basics
10 examples to wire custom schemes, universal links, and Expo config - the foundation every SDK 57 app needs before routing users from email, SMS, QR codes, or push notifications into the right screen.
Search across all documentation pages
10 examples to wire custom schemes, universal links, and Expo config - the foundation every SDK 57 app needs before routing users from email, SMS, QR codes, or push notifications into the right screen.
Scaffold an Expo app with file-based routing and install the linking primitives:
npx create-expo-app@latest DeepLinkBasics --template tabs
cd DeepLinkBasics
npx expo install expo-linking expo-routerConfirm the SDK pin:
{
"dependencies": {
"expo": "~57.0.4",
"react": "19.2.3",
"react-native": "0.86.0",
"expo-router": "~6.0.0"
}
}Tooling: These examples target Expo SDK 57 (
expo~57.0.4), React Native 0.86.0, and React 19.2.3.
Mobile deep links arrive in two shapes. Pick based on whether you need verified HTTPS routing or a quick dev-only path.
┌──────────────────────────────────────────────────────────────────────────┐
│ Type │ Example URL │ Opens without prompt │
├───────────────────┼────────────────────────────────┼──────────────────────┤
│ Custom scheme │ myapp://orders/42 │ Yes (if app installed) │
│ Universal/App Link│ https://shop.example.com/o/42 │ Yes (when verified) │
│ Web fallback │ https://shop.example.com/o/42 │ Browser (no app) │
└──────────────────────────────────────────────────────────────────────────┘scheme in app.config.ts and handle paths in codeRelated: iOS Universal Links -
apple-app-site-associationsetup | Android App Links -assetlinks.jsonverification
The scheme property tells iOS and Android which URL prefix belongs to your app.
// app.config.ts
import type { ExpoConfig } from "expo/config";
const config: ExpoConfig = {
name: "ShopApp",
slug: "shop-app",
scheme: "shopapp",
ios: {
bundleIdentifier: "com.example.shopapp",
},
android: {
package: "com.example.shopapp",
},
};
export default config;shopapp) produces URLs like shopapp://orders/42scheme: ["shopapp", "com.example.shopapp"]scheme - OTA updates cannot add new URL handlersRelated: ../expo-rules/navigation-and-routing-rules/navigation-and-routing-rules.md - scheme and domain rules for review
expo-linking generates URLs that respect your configured scheme and optional path prefix.
import * as Linking from "expo-linking";
const orderUrl = Linking.createURL("orders/42", {
queryParams: { ref: "email" },
});
// Dev client / production: shopapp://orders/42?ref=emailcreateURL prepends the app scheme and normalizes slashes - prefer it over string concatenationisTripleSlashed: true only when integrating with legacy Android intent filterscreateURL so dev and prod stay consistentorderUrl in __DEV__ to copy into Maestro flows and marketing spreadsheetsRelated: expo-linking API - parsing inbound URLs and event listeners
When a link opens your app, parse it into structured parts before navigating.
import * as Linking from "expo-linking";
function handleDeepLink(url: string) {
const parsed = Linking.parse(url);
// shopapp://orders/42?ref=email
// parsed.path → "orders/42"
// parsed.queryParams → { ref: "email" }
// parsed.hostname → null (custom scheme)
const segments = (parsed.path ?? "").split("/").filter(Boolean);
const [screen, id] = segments;
if (screen === "orders" && id) {
// router.push(`/orders/${id}`);
}
}Linking.parse handles both custom schemes and https:// URLs - hostname is set for universal linkspath before splitting - Android and iOS sometimes differ on trailing slashesRelated: ../expo-router/expo-router-basics/expo-router-basics.md - file paths become URL segments
Warm-start links arrive while JavaScript is already running - subscribe in a root layout.
import * as Linking from "expo-linking";
import { useEffect } from "react";
export function useDeepLinkListener(onUrl: (url: string) => void) {
useEffect(() => {
const subscription = Linking.addEventListener("url", ({ url }) => {
onUrl(url);
});
return () => subscription.remove();
}, [onUrl]);
}addEventListener("url") fires when the user taps a link and the app is foregrounded or backgroundedsubscription.remove() in the effect cleanup - leaks cause duplicate navigations after fast refreshRelated: expo-linking API - cold-start vs warm-start handling
Cold-start links open the app from a killed state - read the initial URL once before rendering routes.
import * as Linking from "expo-linking";
import { useEffect, useState } from "react";
export function useInitialDeepLink() {
const [initialUrl, setInitialUrl] = useState<string | null>(null);
const [ready, setReady] = useState(false);
useEffect(() => {
Linking.getInitialURL()
.then((url) => setInitialUrl(url))
.finally(() => setReady(true));
}, []);
return { initialUrl, ready };
}getInitialURL returns the link that launched the app, or null if opened from the home screenready - navigating before Router mounts drops the initial URLgetInitialURL once at startup - subsequent opens use the event listenerscheme is configured - custom hooks are for side effectsRelated: expo-linking API - complete cold + warm bootstrap pattern
Expo Router turns your app/ directory into a URL tree - deep links map to screens without manual wiring.
app/
(tabs)/
index.tsx → /
orders/
index.tsx → /orders
[id].tsx → /orders/:id
product/
[slug].tsx → /product/:slug// app/orders/[id].tsx
import { useLocalSearchParams } from "expo-router";
import { Text, View } from "react-native";
export default function OrderDetailScreen() {
const { id } = useLocalSearchParams<{ id: string }>();
return (
<View>
<Text>Order {id}</Text>
</View>
);
}[id].tsx) become URL params - shopapp://orders/42 lands on OrderDetailScreen(tabs) do not appear in the URL - keeps tab URLs cleanid with Zod at the screen boundary - malformed deep links should show a fallback, not crashexperiments.typedRoutes) catch invalid href at compile timeRelated: ../expo-router/link-and-href/link-and-href.md -
Linkandhrefobjects
HTTPS deep links need a domain association - add associatedDomains (iOS) and intentFilters (Android) via config plugins or app.config.ts.
// app.config.ts
const config = {
expo: {
name: "ShopApp",
slug: "shop-app",
scheme: "shopapp",
ios: {
bundleIdentifier: "com.example.shopapp",
associatedDomains: ["applinks:shop.example.com"],
},
android: {
package: "com.example.shopapp",
intentFilters: [
{
action: "VIEW",
autoVerify: true,
data: [
{
scheme: "https",
host: "shop.example.com",
pathPrefix: "/orders",
},
],
category: ["BROWSABLE", "DEFAULT"],
},
],
},
},
};
export default config;applinks: prefix on iOS tells the system to fetch apple-app-site-association from your CDNautoVerify: true on Android triggers Digital Asset Links verification at install time/orders, /product, not the entire marketing site root unless intendedRelated: iOS Universal Links - AASA file format and CDN headers
Use npx uri-scheme to fire links into simulators and connected devices without rebuilding marketing pages.
# iOS Simulator
npx uri-scheme open "shopapp://orders/42?ref=cli" --ios
# Android emulator / device
npx uri-scheme open "shopapp://orders/42?ref=cli" --androiduri-scheme globally or use npx - no app code changes requiredxcrun simctl openurl with https:// - see Deep Link Testing MatrixRelated: Deep Link Testing Matrix - simulator, device, and CI coverage
Deep links should degrade to your marketing site or app store - never a blank error page.
<!-- Hosted at https://shop.example.com/orders/42 -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Order 42 - ShopApp</title>
<meta name="apple-itunes-app" content="app-id=123456789, app-argument=https://shop.example.com/orders/42" />
<script>
const isIOS = /iPhone|iPad|iPod/i.test(navigator.userAgent);
const isAndroid = /Android/i.test(navigator.userAgent);
if (isIOS || isAndroid) {
window.location = "shopapp://orders/42";
setTimeout(() => {
window.location = isIOS
? "https://apps.apple.com/app/id123456789"
: "https://play.google.com/store/apps/details?id=com.example.shopapp";
}, 1500);
}
</script>
</head>
<body>
<p>Opening ShopApp… <a href="https://shop.example.com/orders/42">View in browser</a></p>
</body>
</html>apple-itunes-app) improve iOS web-to-app handoff when universal links are not yet verified/orders/42 should mean the same entity on both surfacesRelated: Deferred Deep Links - attribution after fresh install
Use expo-linking path prefixes when multiple build flavors share one scheme but different route roots.
import Constants from "expo-constants";
import * as Linking from "expo-linking";
const prefix = Constants.expoConfig?.extra?.linkingPrefix ?? "";
export function appPath(route: string) {
return Linking.createURL(`${prefix}${route}`);
}
// Staging extra: linkingPrefix = "staging/"
// → shopapp://staging/orders/42linking config with prefixes in expo-router advanced setups - keep one source of truthRelated: Push Notification Deep Links - embedding paths in notification payloads
exp:// scheme - your shopapp:// links open the dev client or a standalone build, not Expo Go.expo-dev-client or preview builds with the real bundle ID before release.Linking.parse is for analytics, legacy React Navigation setups, or non-route URLs (e.g. shopapp://reset-password?token=…).assetlinks.json hosting.http:// instead of https:// - verification requires TLS.Stack versions: This page was written for React 19.2.3, React Native 0.86.0, and Expo SDK 57 (
expo~57.0.4).
Reviewed by Chris St. John·Last updated Jul 16, 2026