React Native – Deep Linking and the URL as State

July 13, 20263 min readUpdated 8/24/2026

A phone app has URLs too. A link in an email, a push notification, a payment provider returning the customer after a redirect — all of them need to open your app at a particular screen, and all of them fail silently if you have not set it up.

Registering a scheme

The simplest form is a custom scheme, and it is one line of configuration:

const config: ExpoConfig = {
  name: 'StayHub Pizza',
  slug: 'pizza-react-native-mobile',
  version: '1.0.0',
  orientation: 'portrait',
  icon: './assets/icon.png',
  scheme: 'pizzaapp',
  userInterfaceStyle: 'light',

That makes pizzaapp://order/abc123 open the app on the order screen. Expo Router maps the path onto the same route tree the app already uses, so there is nothing else to declare — the folder structure is the link structure.

⚠️ A custom scheme only works if the app is installed. Tap pizzaapp://… without it and the browser reports an unknown protocol. That is fine for returning from a payment flow you started, and useless for a link you email to someone who might not have the app.

The grown-up version: https://lovemesomecoding.com/order/abc123 opens the app when it is installed and the website when it is not.

It costs more than a scheme because the OS verifies you own the domain. You serve a JSON file — apple-app-site-association for iOS, assetlinks.json for Android — over HTTPS from a fixed path, listing your app's identifier. The OS fetches it at install time. Expo generates the native manifest entries from associatedDomains and intentFilters in the config.

The verification step is where people get stuck, and the symptom is unhelpful: the link just opens the website, with no error anywhere. Both platforms have a validator; use it before assuming your code is wrong.

Coming back from a redirect

The most common reason to need any of this is not marketing. It is a third party sending the user back:

      urlScheme="pizzaapp"

That is the Stripe provider in the demo app. When a card needs 3D Secure, the customer leaves for their bank's page and this scheme is what brings them back. Get it wrong — or forget to match it to scheme in the config — and the app is simply never reopened, leaving a paid order the customer never sees confirmed. Lesson 19 covers the flow.

The URL as state

The habit worth keeping from the web, and it is not obvious that it applies here:

  const params = useLocalSearchParams<{ type?: string }>();
  const router = useRouter();
  const activeFilter: Filter = isFilter(params.type) ? params.type : 'ALL';

The menu's active filter lives in the route's query params rather than in component state. On a phone that still earns its keep for two reasons. A deep link — pizzaapp://menu?type=PIZZA — opens on the right tab already filtered. And the filter survives the screen being dropped from memory and rebuilt when the user returns to it, which a useState would not.

Note isFilter: a type guard rather than a cast. Params arrive as string | undefined, and pretending otherwise means a malformed link produces a screen in an impossible state.

      router.replace(filter === 'ALL' ? '/menu' : `/menu?type=${filter}`);

replace, not push — a filter change is not a place to go back to. Get this backwards and the back gesture walks through every filter the user tried.

What does not belong in the URL

Anything transient or private. Which modal is open, a half-typed form, an auth token. The test is whether you would be happy for someone to send that link to a friend.

export default function NotFoundRoute() {
  const router = useRouter();

+not-found.tsx is Expo Router's catch-all, and it matters more here than on the web. A website's old URLs are under your control; a deep link is out in the world in emails and notifications, and it can point at a screen a newer build removed. Without a catch-all the app opens to nothing and the user assumes it is broken.

npx uri-scheme open "pizzaapp://order/abc123" --ios
xcrun simctl openurl booted "pizzaapp://menu?type=PIZZA"
adb shell am start -W -a android.intent.action.VIEW -d "pizzaapp://menu"

Worth running early. Deep linking is the kind of feature that is configured once, assumed to work, and discovered broken by a customer.

What is next

Structuring a Real App — where all this code goes once there is more than a screen's worth.