Why Real-Time Notifications Matter

Real-time notifications are a cornerstone of modern mobile engagement. They keep users informed of time-sensitive updates—new messages, order confirmations, breaking news, or social interactions—without requiring them to open your app. By implementing push notifications, you can increase retention rates, reduce churn, and create a more responsive user experience. Firebase Cloud Messaging (FCM) is one of the most reliable and cost-effective solutions, offering a unified way to send notifications across Android, iOS, and web platforms. In this guide, you will learn step-by-step how to integrate FCM into your React Native project, from initial setup to sending targeted messages, with production-ready code and best practices.

Why Firebase Cloud Messaging?

FCM provides several advantages for React Native developers:

  • Cross-platform consistency – A single API works for Android and iOS, reducing code duplication.
  • Scalable infrastructure – Google’s infrastructure handles millions of messages with low latency.
  • Rich payloads – Send notification messages, data-only messages, or a combination of both.
  • Topic and device group messaging – Target specific user segments or broadcast to all users.
  • Free tier – FCM is free, with generous limits suitable for most apps.

When combined with @react-native-firebase/messaging, you get native-level performance and access to all FCM features without writing platform-specific code.

Prerequisites

Before you begin, ensure you have the following:

  • A React Native project (bare workflow recommended; Expo managed workflow requires additional steps).
  • A Firebase Console account.
  • Node.js and npm (or yarn) installed.
  • For Android: Android Studio, a physical device or emulator with Google Play Services.
  • For iOS: Xcode 14+, a physical device (simulator does not support push unless using a special build), and an Apple Developer account.

Setting Up Your Firebase Project

Create a Firebase Project

Go to the Firebase Console, click Add project, and follow the steps. Once your project is ready, add an Android app and an iOS app to it.

Add Your Android App

Register your Android app in Firebase with the package name (e.g., com.yourcompany.yourapp). Download the google-services.json file and place it in your React Native project at android/app/google-services.json. Next, update your android/build.gradle (project-level) and android/app/build.gradle as per React Native Firebase documentation:

// android/build.gradle (project-level)
dependencies {
    classpath("com.google.gms:google-services:4.4.0")
}
// android/app/build.gradle
apply plugin: 'com.google.gms.google-services'

Then, ensure you declare the default Firebase channel and notification icon in AndroidManifest.xml:

<meta-data android:name="com.google.firebase.messaging.default_notification_channel_id" android:value="default" />
<meta-data android:name="com.google.firebase.messaging.default_notification_icon" android:resource="@drawable/ic_notification" />

Add Your iOS App

In the Firebase Console, add an iOS app with your bundle identifier (e.g., com.yourcompany.yourapp). Download GoogleService-Info.plist and add it to your Xcode project. Open Xcode, right-click on your project, and choose Add Files to "YourProject". Ensure the file is added to the main target.

Next, enable push notifications capability: in Xcode, select your target → Signing & Capabilities+Push Notifications. Also, you must upload an APNs authentication key in the Firebase Console under Cloud MessagingAPNs Authentication Key, or use APNs certificates if you prefer.

Installing and Configuring the Messaging Package

First, install the core Firebase app module and the messaging module:

npm install @react-native-firebase/app @react-native-firebase/messaging

Then rebuild your app on each platform:

npx react-native run-android   # Android
cd ios && pod install && cd ..  # iOS (required after adding native modules)

After installation, initialize Firebase (if not auto-initialized) by adding this to your root component or entry file:

import { firebase } from '@react-native-firebase/app';

if (!firebase.apps.length) {
  firebase.initializeApp();
  // optionally pass config from environment variables
}

On iOS only, Firebase auto-initialization uses GoogleService-Info.plist; you usually don't need explicit initializeApp. However, for Android you may need it if you have multiple projects or want to control initialization order.

Requesting Notification Permissions

iOS requires explicit user permission. Android 13+ also requires runtime permission for notifications. The @react-native-firebase/messaging library handles this easily:

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

async function requestUserPermission() {
  const authStatus = await messaging().requestPermission();
  const enabled =
    authStatus === messaging.AuthorizationStatus.AUTHORIZED ||
    authStatus === messaging.AuthorizationStatus.PROVISIONAL;

  if (enabled) {
    console.log('Notification permission granted:', authStatus);
  } else {
    console.log('Notification permission denied');
    // Optionally show a dialog explaining why notifications are needed
  }
}

// Call on app start (only on iOS or Android 13+)
if (Platform.OS === 'ios' || (Platform.OS === 'android' && Platform.Version >= 33)) {
  requestUserPermission();
}

On Android 12 and below, no permission is required for local notifications, but FCM will still work. You may also want to check permission status later using messaging().hasPermission().

Handling Incoming Notifications

Foreground Messages

When your app is in the foreground, FCM delivers messages via the onMessage listener. By default, these are not displayed as system notifications—you must show them yourself (e.g., using @notifee/react-native for local notifications).

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

useEffect(() => {
  const unsubscribe = messaging().onMessage(async remoteMessage => {
    console.log('Foreground message:', remoteMessage);
    // Example: show a local notification using Notifee
    // await notifee.displayNotification(remoteMessage.notification);
  });

  return unsubscribe;
}, []);

Background Messages (iOS and Android)

When the app is in the background (paused or killed), FCM automatically displays a system notification if the payload contains a notification key. To handle data-only payloads in background, you need to register a background handler:

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

// Must be registered outside of any component
messaging().setBackgroundMessageHandler(async remoteMessage => {
  console.log('Background message:', remoteMessage);
  // Perform background operations (e.g., sync data, update badge)
});

This handler will be invoked even if the app is killed (on Android) or in the background (iOS). Note that iOS has strict background execution limits; keep the processing brief.

On App Opened from Notification

To handle a user tapping a notification, use onNotificationOpenedApp (for background start) and getInitialNotification (for killed state):

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

// When app is in background and notification is tapped
useEffect(() => {
  const unsubscribe = messaging().onNotificationOpenedApp(remoteMessage => {
    console.log('Notification opened app from background:', remoteMessage);
    // Navigate to specific screen based on data
  });

  return unsubscribe;
}, []);

// When app is killed and opened by notification
useEffect(() => {
  messaging().getInitialNotification().then(remoteMessage => {
    if (remoteMessage) {
      console.log('Notification opened app from killed state:', remoteMessage);
      // Navigate accordingly
    }
  });
}, []);

Device Token Management

Your server needs a device's FCM token to send targeted messages. Retrieve it after permission is granted:

const getToken = async () => {
  try {
    const token = await messaging().getToken();
    console.log('FCM Token:', token);
    // Send token to your backend and store it
    await saveTokenToServer(token);
  } catch (error) {
    console.error('Failed to get token:', error);
  }
};

// Call after permissions granted
if (enabled) {
  getToken();
}

Tokens can change (e.g., after app reinstall, clearing data, or on iOS when notification settings change). Listen for token refresh:

useEffect(() => {
  const unsubscribe = messaging().onTokenRefresh(token => {
    console.log('Token refreshed:', token);
    // Update your backend
  });
  return unsubscribe;
}, []);

On Android, token may also be fetched using messaging().getToken() at any time. Apple recommends using onTokenRefresh as the primary mechanism.

Sending Notifications

Using Firebase Console (for testing)

In the Firebase Console, go to Cloud MessagingSend your first message. Enter notification title and body, then under Target choose Single Device and paste the FCM token you retrieved. Set your app name and send. You should receive the notification on your device.

Google recommends using the HTTP v1 API over the legacy legacy API. It supports finer access control via OAuth2 and is more secure. Here's a Node.js example using a service account:

const admin = require('firebase-admin');
const serviceAccount = require('./path/to/serviceAccountKey.json');

admin.initializeApp({
  credential: admin.credential.cert(serviceAccount),
});

const message = {
  token: 'DEVICE_FCM_TOKEN',
  notification: {
    title: 'Hello!',
    body: 'This is a test notification from v1 API.',
  },
  // optional: android, apns config
  android: {
    notification: {
      channel_id: 'default',
      priority: 'high',
    },
  },
  apns: {
    payload: {
      aps: {
        alert: {
          title: 'Hello!',
          body: 'This is a test notification from v1 API.',
        },
        sound: 'default',
      },
    },
  },
};

admin.messaging().send(message)
  .then(response => {
    console.log('Message sent:', response);
  })
  .catch(error => {
    console.error('Error sending message:', error);
  });

Obtain a server key via Firebase Console → Project Settings → Service accounts; generate a new private key and download the JSON file. Keep it secure and never expose it client-side.

Data-Only Messages

To send a payload that does not trigger a system notification (useful for silent sync), omit the notification key and use only data:

{
  "token": "DEVICE_FCM_TOKEN",
  "data": {
    "type": "new_message",
    "message_id": "1234"
  }
}

These messages are delivered to both foreground and background handlers. On iOS, background data messages require content-available: 1 in the APNs payload and the app must have appropriate background modes enabled.

Advanced: Topic Subscriptions

Instead of sending to a single token, you can subscribe devices to topics and broadcast to all subscribers.

// Subscribe to topic
await messaging().subscribeToTopic('promotions');

// Unsubscribe
await messaging().unsubscribeFromTopic('promotions');

Then send a message to the topic from the server:

admin.messaging().send({
  topic: 'promotions',
  notification: { title: 'Big Sale!', body: '40% off everything.' },
});

Topics are public; anyone can subscribe. For targeted groups, use device groups or condition-based expressions (e.g., "'TopicA' in topics && ('TopicB' in topics)").

Best Practices

Define Notification Channels (Android)

Android 8+ requires notification channels. Create them in your app:

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

async function createChannel() {
  await notifee.createChannel({
    id: 'default',
    name: 'Default Notifications',
    importance: AndroidImportance.HIGH,
  });
}

Then specify the channel in your server payload under android.notification.channel_id.

Respect User Opt-In

Do not request permission on first launch without explaining the value. Show a custom dialog first: "We will notify you when you receive a new message." Then call requestPermission(). On iOS, you can also call requestPermission({provisional: true}) to present notifications quietly initially, then upgrade later.

Use Analytics to Optimize

Enable Firebase Analytics to track notification opens, conversions, and user engagement. Add the Analytics package:

npm install @react-native-firebase/analytics

Then log events when a notification is opened:

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

// Inside notification handler
await analytics().logEvent('notification_opened', {
  notification_title: remoteMessage.notification?.title,
});

Handle Token Expiration Gracefully

When a token becomes invalid (e.g., user uninstalls/reinstalls), your server will receive an error. Implement a mechanism to remove stale tokens from your database. For example, when sending returns "errorCode": "UNREGISTERED", delete the token.

Troubleshooting Common Issues

  • Notifications not received on iOS: Ensure APNs key/certificate is uploaded in Firebase Console and that your app bundle ID matches. Test on a physical device.
  • Foreground notifications not showing: By design, FCM doesn't display foreground notifications. Use a local notification library like notifee or react-native-push-notification to show them.
  • Token undefined on iOS simulator: Simulators cannot receive push notifications. Use a real device or a remote push simulator tool.
  • Android app crashes on startup after adding FCM: Check google-services.json location and ensure you have the correct Gradle dependency versions. Also, verify that your applicationId matches the package in Firebase.
  • Background data messages not delivered on iOS: Enable Remote notifications in Xcode capabilities and set the content-available: 1 flag in your server payload. Also, your background handler must complete quickly (iOS may throttle).

Conclusion

Integrating Firebase Cloud Messaging into your React Native app equips you with a powerful, scalable notification system that works seamlessly on both Android and iOS. By following the steps outlined—project setup, platform-specific configuration, permission handling, token management, and server-side sending—you can deliver real-time, engaging notifications to your users. Leverage topics for mass messaging, use data-only payloads for silent updates, and always respect user preferences. With proper implementation, FCM will become an invaluable asset for boosting user retention and driving app engagement.

For further reference, consult the official React Native Firebase Messaging documentation and the FCM documentation.