React Native2024-11-18|8 min read

Complete Guide to Push Notifications in React Native (FCM + APNs)

Push notifications are one of the most requested features in mobile apps and one of the most error-prone to implement. Between Android's notification channels, iOS entitlements, background state handling, and the APNs/FCM split, there are a lot of moving parts. This guide covers the full setup from Firebase project creation to production-ready notification handling.

FCM vs APNs — What's the Difference?

APNs (Apple Push Notification service) is Apple's delivery infrastructure. FCM (Firebase Cloud Messaging) is Google's. On Android, FCM delivers directly. On iOS, FCM acts as a relay — your server sends to FCM, which forwards to APNs, which delivers to the device. Most React Native projects use FCM as a single endpoint for both platforms.

Setting Up Firebase

  1. Create a project at console.firebase.google.com.
  2. Download google-services.json (Android) and GoogleService-Info.plist (iOS).
  3. Place them at android/app/google-services.json and ios/MyApp/GoogleService-Info.plist.
npm install @react-native-firebase/app @react-native-firebase/messaging

In android/build.gradle, add the Google services classpath:

buildscript {
  dependencies {
    classpath 'com.google.gms:google-services:4.4.0'
  }
}

In android/app/build.gradle, apply the plugin at the bottom:

apply plugin: 'com.google.gms.google-services'

On iOS, add the Push Notifications capability in Xcode under Signing & Capabilities, then upload your APNs key or certificate to the Firebase console under Project Settings > Cloud Messaging.

Requesting Permissions

import messaging from '@react-native-firebase/messaging';
import { Platform, Alert } from 'react-native';

export async function requestNotificationPermission() {
  if (Platform.OS === 'ios') {
    const authStatus = await messaging().requestPermission();
    const enabled =
      authStatus === messaging.AuthorizationStatus.AUTHORIZED ||
      authStatus === messaging.AuthorizationStatus.PROVISIONAL;

    if (!enabled) {
      Alert.alert(
        'Notifications disabled',
        'Enable notifications in Settings to receive updates.',
      );
      return false;
    }
  }

  // Android 13+ requires runtime permission
  if (Platform.OS === 'android' && Platform.Version >= 33) {
    const { PermissionsAndroid } = require('react-native');
    const granted = await PermissionsAndroid.request(
      PermissionsAndroid.PERMISSIONS.POST_NOTIFICATIONS,
    );
    if (granted !== PermissionsAndroid.RESULTS.GRANTED) return false;
  }

  return true;
}

Getting and Storing the FCM Token

export async function registerDevice() {
  await messaging().registerDeviceForRemoteMessages();
  const token = await messaging().getToken();
  // Send token to your backend for storage
  await api.updateDeviceToken(token);

  // Refresh token when it changes
  return messaging().onTokenRefresh(async (newToken) => {
    await api.updateDeviceToken(newToken);
  });
}

Handling the Three Notification States

A notification can arrive when the app is in one of three states. Each requires different handling:

import messaging from '@react-native-firebase/messaging';

// 1. FOREGROUND — app is open and active
messaging().onMessage(async (remoteMessage) => {
  // FCM does NOT show a system notification in foreground automatically
  // Display your own in-app banner or use notifee for a local notification
  showInAppBanner(remoteMessage.notification);
});

// 2. BACKGROUND — notification tapped while app was in background
messaging().onNotificationOpenedApp((remoteMessage) => {
  navigateFromNotification(remoteMessage.data);
});

// 3. QUIT STATE — app was fully closed
messaging()
  .getInitialNotification()
  .then((remoteMessage) => {
    if (remoteMessage) navigateFromNotification(remoteMessage.data);
  });

// Background handler for data-only messages (must be outside component tree)
messaging().setBackgroundMessageHandler(async (remoteMessage) => {
  console.log('Background message:', remoteMessage);
});

Android Notification Channels

Android 8+ requires all notifications to be assigned to a channel. Users can silence individual channels in system settings. Use notifee to manage channels:

import notifee, { AndroidImportance } from '@notifee/react-native';

export async function createNotificationChannels() {
  await notifee.createChannel({
    id: 'alerts',
    name: 'Critical Alerts',
    importance: AndroidImportance.HIGH,
    sound: 'default',
    vibration: true,
  });

  await notifee.createChannel({
    id: 'marketing',
    name: 'Promotions',
    importance: AndroidImportance.LOW,
  });
}

Deep Linking from Notifications

Include a screen and params in the FCM data payload from your server:

{
  "data": {
    "screen": "OrderDetail",
    "orderId": "ORD-12345"
  }
}
function navigateFromNotification(data) {
  if (!data?.screen) return;
  // navigationRef is a ref attached to NavigationContainer
  navigationRef.current?.navigate(data.screen, { id: data.orderId });
}

Testing Push Notifications

  • Firebase Console: Cloud Messaging > Send test message > paste your FCM token.
  • Android CLI: adb shell am broadcast -a com.google.android.c2dm.intent.RECEIVE ...
  • iOS Simulator: xcrun simctl push <device-id> <bundle-id> payload.apns

Push notifications are one of those features where edge cases live in production. Test all three states — foreground, background, and quit — before every release. A notification that deep-links to a crashed screen is worse than no notification at all.