Skip to content
BloGrove
mobile dev

React Native Push Notifications: The Architecture to Ship

Local vs remote push in React Native — how the payload flows, why push isn't a background worker, permission wrangling, and patterns users won't mute.

BBloGrove Editorial5 min read
React Native Push Notifications: The Architecture to Ship

Push notifications look simple from the app side: your server calls a provider, the provider wakes the OS, the OS shows a banner. But the mobile-specific details — permission flows, cold starts, foreground vs background handlers, and the "just collapsed into an RSS feed of deals" trap — are where most implementations fall apart. This guide walks the architecture that actually survives production.

Local vs remote push#

Two different things, confused constantly:

  • Local notification: scheduled and shown by the device itself. No network involved, works offline. Timers, reminders, calendar events.
  • Remote push (APNs/FCM): your server sends a message to the platform, which delivers it to the device. Used for anything time-sensitive or user-triggered on the backend: chat messages, order updates, breaking news.

Decide which kind you need before touching any library. If all your notifications originate from within the app, local notifications cover you with zero backend and zero credentials setup.

The payload flow for remote push#

Your server → FCM/APNs → OS push daemon → device

The message your server sends is a JSON payload. Two parts matter:

  • The OS-visible fields (title, body, sound, or data on Android) — rendered by the system without your JS running.
  • The data payload — arbitrary keys delivered to your app. On Android, data-only messages wake the app up; on iOS, content-available: 1 is required and there are rate limits.

A typical payload:

{
  "to": "device-token",
  "notification": {
    "title": "Your order shipped",
    "body": "Parcel KRB001 has left the hub."
  },
  "data": {
    "orderId": "KRB001",
    "type": "order_status"
  }
}

The data keys let your app navigate straight to a screen (deep-link to /order/KRB001) and let the server adjust behaviour without a new app release.

Permissions: the funnel you must measure#

iOS and Android both require opt-in, and the request itself is a conversion event. Pattern: ask for permission in context, not at first launch.

  • First launch: use the app, don't ask.
  • When the user performs the action that makes notifications valuable (they send a message, they place an order): ask.

Measurement matters. Permission acceptance rate is a dashboard metric. If it's below ~50%, the ask timing is wrong.

Android quirk: on Android 13+, permission is a runtime prompt (POST_NOTIFICATIONS); before that it was automatic. Handle the three reply values — authorized, denied, provisional (iOS) — and show a settings deep-link when denied.

Handling taps, foreground, and background#

Notification taps are the primary return path; you need three handlers wired consistently:

  1. Tap while app killed (cold start) — apps often miss this. The OS launches the app with the payload; read it from your getInitialNotification before assuming normal startup.
  2. Tap while app in background — fires the "notification response" listener.
  3. App in foreground — iOS does not show a banner by default; decide whether to present it. (Standard: yes, because the user glances at the lock screen.)

A compact state machine: capture the tap payload, navigate, and clear the captured value so a later cold start doesn't double-navigate.

The classic production bug: user taps a notification, app cold-starts, and the root screen renders instead of the target screen because the cold-start payload was never read.

The background-data trap#

"Can I send a silent notification and run code in the background?" For Android, data-only messages give you a short execution window (for work that must run despite background restrictions) but the OS may throttle or kill it depending on vendor/Doze. For iOS, content-available: 1 gives you ~30 seconds and Apple limits how often silent pushes are delivered.

If you need a guaranteed background job (syncing, refreshing a badge), push is not the tool. Use the platform's background task APIs with a sensible minimum interval instead.

Badges, channels, and the opt-out signal#

Badge count on iOS is set by the app, not the server — call setBadgeCount when the app is active, and reset it on open. Android notification channels group settings: messaging vs promotions. Grouping honestly is good product sense: if users can mute "promotions" but keep "messages," they keep the app installed.

The engage-again trap. Push is a retention lever, not a channel to exhaust. The moment notification volume makes the uninstall reach, the whole feature has cost the business more than it earned. Rate-limit and let users choose frequency.

The React Native stack, concretely#

Use a mature abstraction:

  • iOS/Android remote: @react-native-firebase/messaging (FCM) or push on its own
  • Local + rich UI: Notifee-style libraries handle channels, foreground presentation, and display payloads uniformly

Minimum wiring set:

  1. Request permission in context (see above)
  2. Subscribe device tokens and send them to your server (rotate tokens on reinstall)
  3. Register tap/foreground/background listeners before (or during) the mount path, guarding against double-registration after fast refresh
  4. Feed data.type + payload into your navigation function once

Common production gaps#

Gap Symptom Fix
No cold-start handling Taps working only when app is open Read initial notification on startup
Asking permission at launch Low opt-in rate Ask in context, after the valuable action
Foreground banner missing on iOS "push works on Android but not iOS" Present-manual for foreground iOS
Badge counts from server Stale/wrong badge numbers Set/clear locally
Sending to stale tokens Provider errors, deliverability drops Prune tokens on failure (410/NotRegistered)

Push is a small surface, but the failure modes are sneaky because platform behaviours differ. For the broader app-side story, React Native performance fundamentals covers why background work and rendering stay lean, and native, cross-platform, or PWA helps you decide whether push matters at all for your distribution model.

Enjoyed this article?

Share it with your network.

Share

Keep reading

React Native Performance: The Fundamentals That Fix 90% of Jank
mobile dev

React Native Performance: The Fundamentals That Fix 90% of Jank

Why React Native apps feel slow — the JS thread, list virtualization, image handling, and re-render control, plus a diagnostic workflow for real bottlenecks.

3 min read
Native, Cross-Platform, or PWA: Choosing Where Your App Lives
mobile dev

Native, Cross-Platform, or PWA: Choosing Where Your App Lives

A framework for choosing in 2026 — what native, cross-platform, and progressive web apps genuinely cost and deliver, matched to team and distribution needs.

4 min read