React Native Deep Linking and the Unhandled URL UI
A deep link that matches no route should not crash or blank the app. Catch it and show a friendly fallback that routes the user home.
TL;DR
Deep linking in React Native opens a specific screen from a URL, using a custom scheme or a universal link. The hard part is the unhandled case: a link that matches no route, arrives on a cold start, or is malformed. Handle it by reading the initial URL, listening for new ones, validating the path, and showing a graceful fallback screen instead of a blank view. Use expo-router or the Linking API, and start the UI from a free VP0 design.
Deep linking lets a URL open a specific screen in your app, but the feature people forget is the unhandled case: a link that matches no route, arrives malformed, or lands on a cold start. Handled badly, it shows a blank screen or crashes. VP0 is the free, AI-readable iOS design library builders start from for these screens, including the graceful fallback, so a bad link still lands somewhere sensible.
Who this is for
You are building a React Native or Expo app, maybe with Cursor or Claude Code, that opens links from email, notifications, or the web, and you want deep linking that never dead-ends the user. This is the pattern.
The two link types, and why universal links win
There are two ways into your app. A custom scheme like myapp:// is simple but only works when the app is installed and is easy to spoof. A universal link is a normal https URL that opens your app when installed and falls back to your website otherwise. Apple covers the setup in its universal links guide, which needs an associated domain entitlement and an apple-app-site-association file on your server. Prefer universal links for anything you send to real users.
Read the URL on both paths
A link reaches your app two ways, and you must wire both. The React Native Linking API gives you getInitialURL for a cold start and an event listener for links that arrive while the app runs. Route both through one handler. In Expo, expo-router linking maps paths to file-based routes for you, which removes most of the manual parsing, and the React Native navigation guide shows how the navigator fits in. Miss the cold-start path and a tap from quit state silently does nothing.
Map every link, including the ones that match nothing
The table below is the core of a resilient deep-link handler.
| Incoming link | What it should do | Failure to avoid |
|---|---|---|
| Known path, valid data | Open the target screen | Slow parse that blanks the screen |
| Known path, missing data | Open with a loading or empty state | Crash on a nil value |
| Unknown or malformed path | Show the unhandled-URL fallback | Blank screen or hard crash |
| Expired or revoked link | Explain and route home | Pretending it worked |
The unhandled-URL screen is the one most apps skip. It needs one honest sentence, that the link could not be opened, and one button that routes to a safe home. Speed matters here too: the BBC lost an extra 10% of users for every additional second its site took to load, and a deep link that hangs feels the same way. Build the fallback as a real screen, the same care a bolt and Expo routing fix brings to navigation, and reuse your standard layout, like the Reanimated bottom sheet template, so it feels native.
A worked example: a password reset link
Picture a password reset. The user taps a link in your email, a universal link to https://yourapp.com/reset with a token. If the app is installed, iOS opens it, your handler reads the /reset path and the token, validates the token with your server, and shows the reset screen. If the token has expired, you do not blank out: you show the unhandled-URL fallback explaining the link is no longer valid, with a button to request a fresh one. If the app is not installed, the same URL opens your website, which offers the App Store link. One link, every case covered, no dead ends. That difference, between a link that always lands somewhere sensible and one that sometimes shows a white screen, is what separates a deep-link feature that delights from one that fills your support inbox.
Common mistakes and fixes
- Only wiring the event listener. Read getInitialURL for cold starts too.
- No fallback screen. Treat the unhandled URL as a first-class route.
- Trusting link data. Validate the path and parameters before you navigate.
- Using only a custom scheme. Add universal links so a missing app still works.
- Blocking the UI while parsing. Show a loading state, never a blank view.
If your target screen is SwiftUI rather than React Native, the same fallback discipline applies, as in this language learning app in SwiftUI.
Key takeaways
- Prefer universal links over custom schemes for real-world links.
- Handle both getInitialURL and the listener so cold starts work.
- Make the unhandled URL a real fallback screen, never a blank view.
- Validate link data before routing, and start from a free VP0 design.
Frequently asked questions
The FAQ above covers handling deep links in React Native and Expo, what an unhandled URL is, schemes versus universal links, and why cold starts behave differently.
Frequently asked questions
How do I handle deep linking in React Native and Expo?
Register a URL scheme and, for https links, set up universal links with an associated domain. Read the launch URL with the Linking API getInitialURL for a cold start, and add an event listener for links that arrive while the app runs. Map the path to a route with expo-router or your navigator, and always handle the path that matches nothing. Start the UI from a free VP0 design so the routed screens look consistent.
What is an unhandled URL and how should the UI respond?
An unhandled URL is a deep link whose path matches no screen, or is malformed or expired. The wrong response is a blank screen or a crash. The right one is a clear fallback: a short message that the link could not be opened, plus a button that routes the user to a safe home screen. Treat the unhandled case as a first-class screen, not an afterthought.
What is the difference between a custom scheme and a universal link?
A custom scheme like myapp:// only opens your app if it is installed and is easy to spoof. A universal link is a normal https URL that opens your app when installed and your website otherwise, which is safer and survives a missing app. Universal links need an associated domain entitlement and an apple-app-site-association file on your server.
Why does my deep link work warm but not on a cold start?
Because a warm app catches the link through the event listener, while a cold start delivers it through getInitialURL, which is a separate code path. If you only wired the listener, a tap that launches the app from quit state is missed. Read getInitialURL on startup and also subscribe to the listener, then route through the same handler.
Part of the React Native & Expo: Mobile Frontend Architecture hub. Browse all VP0 topics →
Keep reading
Full-Stack React Native Expo + Supabase Template, Free
Want a full-stack React Native Expo + Supabase starter? Generate your own from a free design plus Supabase auth, database, and storage, with Claude Code or Cursor.
Port Vercel v0 Components to React Native and Expo
v0 outputs React web with Tailwind. Here is how to map its components into a React Native and Expo iOS app with NativeWind, plus the pitfalls to avoid.
Bolt.new React Router Errors in Expo? Swap the Router
Bolt.new app throwing React Router DOM errors when you move to Expo mobile? React Router is for the web. Replace it with Expo Router or React Navigation.
Property Management App UI in React Native
A free React Native pattern for a property management app: units and tenants, maintenance requests, lease documents, and rent through a certified provider.
AdMob Banner Template in React Native, Free
Add an AdMob banner to your React Native app the right way. A free template for clean placement, ATT consent, and ads that do not wreck the experience.
AI Interior Design Room Scanner UI, React Native Free
Build an AI interior design room scanner UI in React Native from a free template. Get the scan, generate, and before-after flow with Claude Code or Cursor.