React Navigation Deep Linking: Diagnose and Fix Common Failures in v6
When a deep link fails in a React Navigation v6 app, the issue often lies in OS‑level scheme registration, linking configuration, or initial URL handling. This guide walks through a diagnostic table, ordered checks, and concrete fixes to get your URLs working reliably on iOS and Android.
22 Nov 2025, 01:53 UTC

What’s the problem?
When a user taps a URL like myapp://profile/123, the app should open the Profile screen and pass 123 as a parameter. In many projects this doesn’t happen – the URL is ignored, the app stays on the home screen, or the screen opens without the expected props. The root cause is usually a mis‑configured deep‑linking setup in React Navigation v6.
Why it matters
Deep linking is a core part of mobile UX: marketing emails, push notifications, or external web pages can bring a user directly to the relevant content. A broken link can frustrate users and undermine trust in the app.
Diagnostic table
| Symptom | Likely Cause | Check |
|---|---|---|
| URL never opens the app | Scheme not registered on the OS | Verify Info.plist (iOS) or AndroidManifest.xml (Android) |
| App opens but shows wrong screen | Linking config missing or wrong prefixes | Inspect linking prop on the navigator |
| Screen opens but props are undefined | Parse function mis‑configured or route name mismatch | Check route names and parse logic |
| Initial link lost when app is cold‑started | Initial URL not awaited before navigator mounts | Confirm Linking.getInitialURL() handling |
Ordered checks and fixes
1️⃣ Verify OS‑level scheme registration
On iOS, the URL scheme must appear in Info.plist under CFBundleURLTypes. On Android, add a data android:scheme="myapp" element inside an intent-filter for the MAIN activity.
// iOS (Info.plist)
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLSchemes</key>
<array>
<string>myapp</string>
</array>
</dict>
</array>
// Android (AndroidManifest.xml)
<activity android:name=".MainActivity" …>
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
<data android:scheme="myapp" />
</intent-filter>
</activity>
Run npx react-native run-ios or npx react-native run-android to confirm the app launches from a browser click. If the OS refuses to open the app, the scheme is missing.
2️⃣ Ensure the linking prop is present and correct
React Navigation v6 expects a linking object that maps prefixes to a route configuration. A common mistake is to omit the prefixes array or use an outdated structure.
const linking = {
prefixes: ['myapp://', 'https://myapp.com'],
config: {
screens: {
Home: 'home',
Profile: {
path: 'profile/:id',
parse: { id: (value) => Number(value) },
},
},
},
};
export default function App() {
return (
<NavigationContainer linking={linking} theme={DefaultTheme}>
<RootStackNavigator />
</NavigationContainer>
);
}
Check that every screen referenced in path exists in the navigator hierarchy. Mismatched names trigger “no route found” errors.
3️⃣ Handle parameters with parse or serialize
When a screen expects a numeric ID, the default query parser returns a string. Use the parse key to cast it.
parse: { id: (value) => parseInt(value, 10) }
If you omit parse, the screen receives { id: '123' }. Verify by adding console.log(route.params) inside the screen.
4️⃣ Await the initial URL before mounting the navigator
When the app starts from a cold state, Linking.getInitialURL() resolves asynchronously. If the navigator mounts before the promise resolves, the link is lost. Wrap the navigator in a state that waits for the URL.
function App() {
const [initialUrl, setInitialUrl] = React.useState(null);
React.useEffect(() => {
const fetchUrl = async () => {
const url = await Linking.getInitialURL();
setInitialUrl(url);
};
fetchUrl();
}, []);
if (initialUrl === null) return null; // or a splash screen
return (
<NavigationContainer linking={linking} initialState={initialUrl}>
<RootStackNavigator />
</NavigationContainer>
);
}
In Expo, the same pattern applies, but you can also enable expo-linking debug mode with expo start --dev-client to see resolved paths in the console.
5️⃣ Test on both platforms and states
- Foreground: open a link while the app is running.
- Background: press the home button, then open a link.
- Cold start: uninstall the app, then open a link.
Use npx uri-scheme open myapp://profile/123 (iOS) or adb shell am start -W -a android.intent.action.VIEW -d "myapp://profile/123" com.myapp (Android) to trigger the link from a terminal.
Escalation criteria
- If the app opens but shows the wrong screen after confirming the scheme, the
linkingconfig is likely mis‑structured. Upgrade to the latest React Navigation and re‑apply the config. - Persistent “no route found” errors indicate a mismatch between the path and the navigator hierarchy. Re‑examine screen names and nested navigators.
- If parameters are consistently undefined, double‑check the
parsefunctions and the way the screen readsroute.params. - When the link works in Expo but not in a bare workflow, verify that the
app.jsonscheme entry is present and that native code is correctly linked.
Practical checklists
- OS scheme? Search
Info.plistorAndroidManifest.xmlformyapp. - Linking prop? Confirm
prefixesandconfig.screensmatch your routes. - Parse functions? Log
route.paramsin each screen. - Initial URL? Add
console.log('Initial URL', url)aftergetInitialURL(). - Run
expo start --dev-clientand watch the console for “Resolved URL” messages.
Conclusion
Deep linking failures usually boil down to one of three things: the OS never hands the URL to the app, the linking configuration is wrong, or the app doesn’t wait for the initial URL. By following the ordered checks above and using the provided code snippets, you can isolate the problem quickly and restore a smooth deep‑linking experience.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.