◉RORK LABJP
●GPT-6.1 SOL — Added to Rork's model menu (09-29) with Low, Medium and High effort; included on Pro and Max●SONNET 5.5 — Claude Sonnet 5.5 is selectable: strong at UI design and faster than Sonnet 5 (09-28)●SONNET 4.5 — Retires on the Claude API on 11/30, 27 days left. If your app calls it, check the migration target●SDK 58 — Moves to React Native 0.88. R8 is now on by default in Android release builds, and iOS prebuild generates a SceneDelegate●OBSERVE — EAS Observe now records native crashes too (SDK 57.0.21 or later, 10/07)●NEW — My App Open Ad Only Showed When the App…●GPT-6.1 SOL — Added to Rork's model menu (09-29) with Low, Medium and High effort; included on Pro and Max●SONNET 5.5 — Claude Sonnet 5.5 is selectable: strong at UI design and faster than Sonnet 5 (09-28)●SONNET 4.5 — Retires on the Claude API on 11/30, 27 days left. If your app calls it, check the migration target●SDK 58 — Moves to React Native 0.88. R8 is now on by default in Android release builds, and iOS prebuild generates a SceneDelegate●OBSERVE — EAS Observe now records native crashes too (SDK 57.0.21 or later, 10/07)●NEW — My App Open Ad Only Showed When the App…
Articles/Dev Tools
⬡ Dev Tools/2026-06-21Advanced

Your Notification Opens the App but Lands on Home — Routing Rork Apps by Launch State

How to make a notification tap reliably reach its target screen. We cover the three launch states — killed, background, foreground — and a pending-route design that never drops a tap that arrives before navigation is ready.

Rork611Expo234Notifications3Deep Linking6Routing

✦ Premium Article

On one app I run, I once sent a "your order has shipped" push notification from the server. Tapping it launched the app — but it just sat on the home screen and never opened the order detail. It worked perfectly on my test device, so the cause took a while to pin down.

The short version: when the app launched from a fully killed state via the tap, my navigation command fired before the navigator was mounted. The command was quietly ignored, with no error.

Almost every notification-routing headache comes down to this one gap: the moment of the tap versus the moment a screen can actually be drawn. Let me walk through a design that absorbs that gap, organized by launch state.

The three launch states you have to account for

When the user taps a notification, the way you receive that tap depends entirely on what state the app was in. Conflate these and you end up with code that only works in one of them.

Launch stateApp conditionWhere the tap comes from
killed (cold start)Process was terminatedgetLastNotificationResponseAsync()
backgroundSuspended in the backgroundResponse listener
foregroundVisible on screenResponse listener

A cold start is the tricky one: the tap that launched the app may never reach your listener. By the time the app boots and registers the listener, that event has already passed. That is exactly why you need to fetch the "last response" once at startup.

In the background and foreground states the app is already alive, so the listener fires reliably.

Put the routing info in the notification's data

What decides the destination is not the notification body — it's the data payload. Send a structured destination on every notification from the server.

// Notification payload the server sends (example)
{
  "to": "ExponentPushToken[xxxxxxxx]",
  "title": "Shipping update",
  "body": "Your order is on its way",
  "data": {
    "type": "order",
    "id": "A-10293"
  }
}

Structuring it as type and id makes it far easier to validate when you reassemble the route later. You could also drop a fully formed URL string into data, but using a server-supplied string directly as a destination is something I'd rather avoid — I'll explain why toward the end.

✦

Thank you for reading this far.

Continue Reading

What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.

WHAT YOU'LL LEARN
✦A pending-route design that absorbs the gap between the moment of the tap and the moment a screen can render
✦An implementation that reliably catches notification taps across all three launch states: killed, background, and foreground
✦How to wire an allowlist and a safe fallback so you never push the user to an unvalidated route
Secure payment via Stripe · Cancel anytime
✦

Unlock This Article

Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.

or
Unlock all articles with Membership →
Share

Thank You for Reading

Rork Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • ✦Copy-paste ready implementation code
  • ✦New advanced guides published daily
  • ✦$5/mo or $10 for lifetime access
View Membership →

Related Articles

⬡ Dev Tools2026-10-30
It runs in development but the iOS Release build dies on launch — checking whether a missing scheme is the cause
In an expo-router app, an app.json with no scheme can crash only the iOS Release build at launch. Why Debug and Android survive, the two ways to fix it, how to think about leaving the scheme out on purpose, and a pre-submission check.
⬡ Dev Tools2026-10-15
When EXPO_PUBLIC_ Values Read Fine in Dev but Come Back undefined in Production: Telling the Spellings Apart
The API URL reads fine while developing and turns undefined once the app is built. The cause was how process.env was written. I lined up babel-preset-expo's output for dev and production and tabulated which spellings get replaced and which are left behind.
⬡ Dev Tools2026-10-13
Before You Ship: Go Looking for Your Own Keys Inside the Exported Expo Bundle
A key can be missing from your source and still be sitting inside the finished app. I ran expo export on Expo SDK 57, searched the bundle with grep and a short Node script, and recorded which secrets show up and which don't.