DEV Community

Cover image for The Complete Guide to Push Notifications in React Native (2026)
Angel Rose for RapidNative

Posted on Originally published at rapidnative.hashnode.dev

The Complete Guide to Push Notifications in React Native (2026)

TL;DR

  • Use expo-notifications in 2026, even in a bare React Native workflow. CNG makes the managed/bare distinction mostly irrelevant.
  • The token identifies a device plus install, not a user. Plan for rotation from day one.
  • On Android you must create a notification channel before requesting permission, or notifications silently never display.
  • Android 13+ needs the POST_NOTIFICATIONS runtime permission, and only if your target SDK is 33 or higher.
  • Never call the system permission prompt cold. It's one-shot. Put a screen you own in front of it.
  • react-native-push-notification (Zo0r) is dead. Don't start a new project on it.

Push notifications are the single most reliable way to bring a user back into a mobile app. Localytics data pegs day-90 retention at roughly 190 percent higher for apps that use them well, and Airship's 2024 benchmarks show median direct-open rates north of 4 percent across most verticals. Yet notifications are also the feature most React Native teams postpone the longest, because the surface spans two operating systems, two transport providers (APNs and FCM), a set of iOS entitlements that require an Apple Developer account, an Android notification-channel model that changed twice in the last five years, and at least three distinct app states you have to handle differently.

This guide is the version I wish I'd had when I first shipped push in a React Native app. It walks through the full stack (permissions, tokens, sending, receiving, deep linking, rich content, and testing) using the Expo Notifications SDK, which works for both Expo-managed and bare React Native projects on modern Expo SDK versions. Every code sample runs. Every gotcha is one I've actually hit in production.

What "push notifications" actually means

A push notification is a message your server hands to a push service (APNs for Apple, FCM for Google) which then delivers it to a specific device using a device-specific token. The device wakes up even if your app is killed, OS-level code decodes the payload, and either displays a system UI or fires an event into your app process.

Three things follow from that definition, and they cause most of the confusion:

  1. You cannot send a push directly from your React Native app to another user. You need a server.
  2. The token identifies a device plus app install, not a user. When a user reinstalls, logs into a second device, or clears app data, you get a new token. Tokens also rotate for other reasons and can be invalidated by the push service.
  3. What runs inside your app is only half the story. The other half is Apple's or Google's OS-level notification presentation logic, which you configure through payload keys, notification channels, and iOS entitlements.

Local notifications, the kind you schedule from inside the app without a server, use most of the same APIs but skip the token and the push service. This guide covers both, since real apps almost always need both.

The 2026 landscape: which library

There are three real options for React Native in 2026, and the honest recommendation is: use expo-notifications, even if you're not on Expo Go and even if you use a bare React Native workflow.

Library Best for Trade-offs
expo-notifications Almost every app First-class support in both Expo-managed and bare RN via CNG (Continuous Native Generation). Handles APNs and FCM transparently. Actively maintained.
@react-native-firebase/messaging Apps that already use other Firebase products (Firestore, Auth, Analytics) heavily Google-only transport story; you still need a separate iOS setup for APNs credentials.
react-native-notifications (Wix) Advanced native-side customization needs Smaller community; more manual native config.

The legacy react-native-push-notification package (Zo0r) is unmaintained and should not be used in a new project.

The rest of this guide uses expo-notifications. Because Expo now supports prebuild (npx expo prebuild) and CNG, you get the same experience whether your project is Expo-managed or bare: the config plugin generates the correct native code for both.

Installing and configuring

Assuming an Expo SDK 52+ project:

npx expo install expo-notifications expo-device expo-constants
Enter fullscreen mode Exit fullscreen mode

expo-device is used to skip token registration on simulators. Apple's push service does not deliver to the iOS simulator on Xcode versions below 14, and even on newer Xcode you need a paid developer account and the Simulator's push testing tools. expo-constants gives you access to easConfig.projectId, which the Expo Push Service needs to route tokens.

Add the config plugin in app.json:

{
  "expo": {
    "plugins": [
      [
        "expo-notifications",
        {
          "icon": "./assets/notification-icon.png",
          "color": "#111827",
          "sounds": ["./assets/notification-sound.wav"]
        }
      ]
    ],
    "ios": {
      "bundleIdentifier": "com.yourco.yourapp",
      "infoPlist": {
        "UIBackgroundModes": ["remote-notification"]
      }
    },
    "android": {
      "package": "com.yourco.yourapp",
      "googleServicesFile": "./google-services.json"
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Two things to know:

  • The notification icon must be a monochrome PNG with a transparent background. Android's icon guidelines are strict, and a full-color icon renders as a white square.
  • On iOS, you need to enable the Push Notifications capability and the Background Modes to Remote notifications capability in your Apple Developer account. If you're using EAS Build, expo-notifications and the UIBackgroundModes config above handle this automatically.

If you're standing up a new project rather than retrofitting an existing one, starting from a config that already builds saves an afternoon. RapidNative generates Expo projects with app.json, the bundle identifier, and the Android package name already in place, so you're editing a working config instead of assembling one from an empty file.

Requesting permission the right way

This is where a lot of implementations lose would-be opt-ins. Apple's system prompt is a one-shot event: if the user taps "Don't Allow," you cannot re-prompt from your app. They would have to go into Settings.

The pattern that works is a pre-permission screen: a screen you own, explaining the value, with a "Turn on notifications" button. Only when the user taps that button do you call the system API.

import * as Notifications from 'expo-notifications';
import * as Device from 'expo-device';
import Constants from 'expo-constants';
import { Platform } from 'react-native';

export async function registerForPushNotifications(): Promise<string | null> {
  if (!Device.isDevice) {
    console.warn('Push notifications require a physical device.');
    return null;
  }

  if (Platform.OS === 'android') {
    await Notifications.setNotificationChannelAsync('default', {
      name: 'default',
      importance: Notifications.AndroidImportance.MAX,
      vibrationPattern: [0, 250, 250, 250],
      lightColor: '#111827',
    });
  }

  const { status: existing } = await Notifications.getPermissionsAsync();
  let finalStatus = existing;

  if (existing !== 'granted') {
    const { status } = await Notifications.requestPermissionsAsync();
    finalStatus = status;
  }

  if (finalStatus !== 'granted') return null;

  const projectId = Constants.expoConfig?.extra?.eas?.projectId
    ?? Constants.easConfig?.projectId;

  const token = (await Notifications.getExpoPushTokenAsync({ projectId })).data;
  return token;
}
Enter fullscreen mode Exit fullscreen mode

A few details that matter:

  • Android 13+ (API 33) requires runtime permission via POST_NOTIFICATIONS. expo-notifications handles this for you when you call requestPermissionsAsync(), but only if your target SDK is 33 or higher.
  • On Android, you must create a notification channel before you request permission. Otherwise notifications will silently fail to display, even though your token is valid. Channels group notifications so users can mute categories independently in system settings.
  • You want the Expo push token, not the native APNs or FCM token, unless you're skipping Expo's push service. The Expo token is a single string of the form ExponentPushToken[xxxxxxxxxx] that Expo's service translates to either APNs or FCM at send time.

Where to go next

The setup above gets you a valid token and a permission prompt users actually accept. The next layer is the one that decides whether push works in production: sending from your server, handling the payload in all three app states (foreground, background, killed), routing a tap to the right screen, and testing all of it on real hardware.

What broke first when you shipped push? My money is on the Android channel. Drop yours in the comments.

Top comments (0)