React Native

Push Notifications in React Native with Firebase Cloud Messaging

Introduction

Push notifications keep users engaged by delivering timely updates even when an app is not active. In mobile development, Firebase Cloud Messaging (FCM) is one of the most reliable solutions for sending notifications across Android and iOS. When combined with React Native, it enables cross-platform push notifications with a single backend setup. This comprehensive guide covers everything from initial setup to production-ready notification handling, including platform-specific configurations, message types, deep linking, and best practices for secure and reliable delivery.

Understanding Push Notification Architecture

Before implementing push notifications, it’s essential to understand how they flow through the system:

┌─────────────┐     ┌─────────────┐     ┌─────────────┐     ┌─────────────┐
│    Your     │     │   Firebase  │     │   APNs /    │     │   Mobile    │
│   Backend   │     │     FCM     │     │   Android   │     │   Device    │
└──────┬──────┘     └──────┬──────┘     └──────┬──────┘     └──────┬──────┘
       │                   │                   │                   │
       │ 1. Send message   │                   │                   │
       │    with token     │                   │                   │
       │──────────────────>│                   │                   │
       │                   │                   │                   │
       │                   │ 2. Route to       │                   │
       │                   │    platform       │                   │
       │                   │──────────────────>│                   │
       │                   │                   │                   │
       │                   │                   │ 3. Deliver        │
       │                   │                   │    notification   │
       │                   │                   │──────────────────>│
       │                   │                   │                   │
       │                   │                   │                   │ 4. User
       │                   │                   │                   │    interacts
       │                   │                   │                   │
       │ 5. Analytics /    │                   │                   │
       │    delivery status│                   │                   │
       │<──────────────────│                   │                   │

Project Setup

Install Required Dependencies

# Install React Native Firebase core and messaging
npm install @react-native-firebase/app @react-native-firebase/messaging

# For local notifications (to show notifications when app is in foreground)
npm install @notifee/react-native

# iOS pod install
cd ios && pod install && cd ..

Create Firebase Project

  1. Go to Firebase Console
  2. Create a new project or select existing one
  3. Add Android and iOS apps to your project
  4. Download configuration files (google-services.json and GoogleService-Info.plist)

Android Configuration

// android/build.gradle
buildscript {
    dependencies {
        // Add Google Services plugin
        classpath 'com.google.gms:google-services:4.4.0'
    }
}

// android/app/build.gradle
apply plugin: 'com.google.gms.google-services'

android {
    defaultConfig {
        // Required for Android 13+ notification permissions
        minSdkVersion 21
        targetSdkVersion 34
    }
}

dependencies {
    // Firebase BoM for version management
    implementation platform('com.google.firebase:firebase-bom:32.7.0')
    implementation 'com.google.firebase:firebase-messaging'
}
// Place google-services.json in android/app/

// android/app/src/main/AndroidManifest.xml

    
    
    
    
        
        
        
        
        
        
        
        
    

iOS Configuration

// ios/Podfile
platform :ios, '13.0'

target 'YourApp' do
  use_frameworks! :linkage => :static
  
  pod 'Firebase', :modular_headers => true
  pod 'FirebaseCore', :modular_headers => true
  pod 'FirebaseMessaging', :modular_headers => true
end
// ios/YourApp/AppDelegate.mm
#import 
#import 

@implementation AppDelegate

- (BOOL)application:(UIApplication *)application
    didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
  [FIRApp configure];
  
  // Register for remote notifications
  [UNUserNotificationCenter currentNotificationCenter].delegate = self;
  [application registerForRemoteNotifications];
  
  return [super application:application didFinishLaunchingWithOptions:launchOptions];
}

// Handle APNs token registration
- (void)application:(UIApplication *)application
    didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken {
  [FIRMessaging messaging].APNSToken = deviceToken;
}

@end

In Xcode, enable these capabilities:

  • Push Notifications
  • Background Modes: Remote notifications, Background fetch

Upload your APNs Authentication Key (.p8 file) to Firebase Console > Project Settings > Cloud Messaging > iOS app configuration.

Complete Notification Service Implementation

// src/services/NotificationService.ts
import messaging, {
  FirebaseMessagingTypes,
} from '@react-native-firebase/messaging';
import notifee, {
  AndroidImportance,
  AndroidStyle,
  EventType,
} from '@notifee/react-native';
import { Platform, PermissionsAndroid } from 'react-native';
import AsyncStorage from '@react-native-async-storage/async-storage';

export interface NotificationData {
  type?: string;
  targetId?: string;
  action?: string;
  [key: string]: string | undefined;
}

export interface NotificationPayload {
  title: string;
  body: string;
  data?: NotificationData;
  imageUrl?: string;
}

class NotificationService {
  private static instance: NotificationService;
  private isInitialized = false;

  static getInstance(): NotificationService {
    if (!NotificationService.instance) {
      NotificationService.instance = new NotificationService();
    }
    return NotificationService.instance;
  }

  async initialize(): Promise {
    if (this.isInitialized) return;

    // Create notification channels for Android
    await this.createNotificationChannels();

    // Request permissions
    const hasPermission = await this.requestPermission();
    if (!hasPermission) {
      console.log('Notification permission denied');
      return;
    }

    // Get and store FCM token
    await this.getAndStoreToken();

    // Set up message handlers
    this.setupMessageHandlers();

    // Set up token refresh handler
    this.setupTokenRefreshHandler();

    this.isInitialized = true;
    console.log('Notification service initialized');
  }

  private async createNotificationChannels(): Promise {
    if (Platform.OS !== 'android') return;

    // Default channel
    await notifee.createChannel({
      id: 'default',
      name: 'Default Notifications',
      importance: AndroidImportance.HIGH,
      sound: 'default',
      vibration: true,
    });

    // Messages channel with custom sound
    await notifee.createChannel({
      id: 'messages',
      name: 'Messages',
      importance: AndroidImportance.HIGH,
      sound: 'message_sound',
      vibration: true,
    });

    // Silent channel for background updates
    await notifee.createChannel({
      id: 'silent',
      name: 'Silent Updates',
      importance: AndroidImportance.LOW,
      sound: undefined,
      vibration: false,
    });
  }

  async requestPermission(): Promise {
    // iOS permission request
    const authStatus = await messaging().requestPermission({
      alert: true,
      badge: true,
      sound: true,
      provisional: false, // Set true for provisional (quiet) notifications
    });

    const enabled =
      authStatus === messaging.AuthorizationStatus.AUTHORIZED ||
      authStatus === messaging.AuthorizationStatus.PROVISIONAL;

    // Android 13+ requires explicit permission
    if (Platform.OS === 'android' && Platform.Version >= 33) {
      const androidPermission = await PermissionsAndroid.request(
        PermissionsAndroid.PERMISSIONS.POST_NOTIFICATIONS,
      );
      return androidPermission === PermissionsAndroid.RESULTS.GRANTED;
    }

    return enabled;
  }

  async getAndStoreToken(): Promise {
    try {
      // Check if APNs token is available (iOS)
      if (Platform.OS === 'ios') {
        const apnsToken = await messaging().getAPNSToken();
        if (!apnsToken) {
          console.log('APNs token not available yet');
          return null;
        }
      }

      const fcmToken = await messaging().getToken();
      console.log('FCM Token:', fcmToken);

      // Store token locally
      await AsyncStorage.setItem('fcmToken', fcmToken);

      // Send token to your backend
      await this.sendTokenToServer(fcmToken);

      return fcmToken;
    } catch (error) {
      console.error('Error getting FCM token:', error);
      return null;
    }
  }

  private async sendTokenToServer(token: string): Promise {
    try {
      // Replace with your API call
      await fetch('https://your-api.com/devices/register', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          Authorization: `Bearer ${await this.getAuthToken()}`,
        },
        body: JSON.stringify({
          token,
          platform: Platform.OS,
          deviceId: await this.getDeviceId(),
        }),
      });
    } catch (error) {
      console.error('Error sending token to server:', error);
    }
  }

  private setupTokenRefreshHandler(): void {
    messaging().onTokenRefresh(async (newToken) => {
      console.log('FCM Token refreshed:', newToken);
      await AsyncStorage.setItem('fcmToken', newToken);
      await this.sendTokenToServer(newToken);
    });
  }

  private setupMessageHandlers(): void {
    // Foreground messages
    messaging().onMessage(async (remoteMessage) => {
      console.log('Foreground message:', remoteMessage);
      await this.displayLocalNotification(remoteMessage);
    });

    // Notification opened from background state
    messaging().onNotificationOpenedApp((remoteMessage) => {
      console.log('Notification opened app:', remoteMessage);
      this.handleNotificationPress(remoteMessage.data as NotificationData);
    });

    // Check if app was opened from quit state by notification
    messaging()
      .getInitialNotification()
      .then((remoteMessage) => {
        if (remoteMessage) {
          console.log('App opened from quit state:', remoteMessage);
          this.handleNotificationPress(remoteMessage.data as NotificationData);
        }
      });

    // Notifee foreground event handler
    notifee.onForegroundEvent(({ type, detail }) => {
      switch (type) {
        case EventType.PRESS:
          console.log('Notification pressed:', detail.notification);
          this.handleNotificationPress(
            detail.notification?.data as NotificationData,
          );
          break;
        case EventType.ACTION_PRESS:
          console.log('Action pressed:', detail.pressAction?.id);
          this.handleActionPress(
            detail.pressAction?.id,
            detail.notification?.data as NotificationData,
          );
          break;
        case EventType.DISMISSED:
          console.log('Notification dismissed');
          break;
      }
    });
  }

  private async displayLocalNotification(
    message: FirebaseMessagingTypes.RemoteMessage,
  ): Promise {
    const { notification, data } = message;

    if (!notification) return;

    const channelId = (data?.channel as string) || 'default';

    await notifee.displayNotification({
      title: notification.title,
      body: notification.body,
      data: data as Record,
      android: {
        channelId,
        smallIcon: 'ic_notification',
        pressAction: {
          id: 'default',
        },
        // Big picture style for images
        ...(notification.android?.imageUrl && {
          style: {
            type: AndroidStyle.BIGPICTURE,
            picture: notification.android.imageUrl,
          },
        }),
        // Action buttons
        actions: [
          {
            title: 'View',
            pressAction: { id: 'view' },
          },
          {
            title: 'Dismiss',
            pressAction: { id: 'dismiss' },
          },
        ],
      },
      ios: {
        sound: 'default',
        ...(notification.ios?.imageUrl && {
          attachments: [{ url: notification.ios.imageUrl }],
        }),
      },
    });
  }

  private handleNotificationPress(data?: NotificationData): void {
    if (!data) return;

    // Navigation based on notification type
    const { type, targetId } = data;

    switch (type) {
      case 'message':
        // Navigate to chat screen
        // navigationRef.navigate('Chat', { conversationId: targetId });
        break;
      case 'order':
        // Navigate to order details
        // navigationRef.navigate('OrderDetails', { orderId: targetId });
        break;
      case 'promotion':
        // Navigate to promotion screen
        // navigationRef.navigate('Promotion', { promoId: targetId });
        break;
      default:
        // Navigate to home or notifications list
        // navigationRef.navigate('Notifications');
        break;
    }
  }

  private handleActionPress(
    actionId?: string,
    data?: NotificationData,
  ): void {
    switch (actionId) {
      case 'view':
        this.handleNotificationPress(data);
        break;
      case 'dismiss':
        // Just dismiss, no action needed
        break;
      case 'reply':
        // Handle quick reply
        break;
    }
  }

  // Topic subscription
  async subscribeToTopic(topic: string): Promise {
    try {
      await messaging().subscribeToTopic(topic);
      console.log(`Subscribed to topic: ${topic}`);
    } catch (error) {
      console.error(`Error subscribing to topic ${topic}:`, error);
    }
  }

  async unsubscribeFromTopic(topic: string): Promise {
    try {
      await messaging().unsubscribeFromTopic(topic);
      console.log(`Unsubscribed from topic: ${topic}`);
    } catch (error) {
      console.error(`Error unsubscribing from topic ${topic}:`, error);
    }
  }

  // Badge management (iOS)
  async setBadgeCount(count: number): Promise {
    await notifee.setBadgeCount(count);
  }

  async clearBadge(): Promise {
    await notifee.setBadgeCount(0);
  }

  // Cancel notifications
  async cancelAllNotifications(): Promise {
    await notifee.cancelAllNotifications();
  }

  async cancelNotification(notificationId: string): Promise {
    await notifee.cancelNotification(notificationId);
  }

  // Helper methods
  private async getAuthToken(): Promise {
    return (await AsyncStorage.getItem('authToken')) || '';
  }

  private async getDeviceId(): Promise {
    // Implement device ID retrieval
    return 'device-id';
  }
}

export const notificationService = NotificationService.getInstance();

Background Message Handler

// index.js (must be at the root of your project)
import { AppRegistry } from 'react-native';
import messaging from '@react-native-firebase/messaging';
import notifee from '@notifee/react-native';
import { name as appName } from './app.json';
import App from './App';

// Background message handler (runs when app is in background or quit)
messaging().setBackgroundMessageHandler(async (remoteMessage) => {
  console.log('Background message:', remoteMessage);

  // Data-only messages need manual notification display
  if (remoteMessage.data && !remoteMessage.notification) {
    await notifee.displayNotification({
      title: remoteMessage.data.title || 'New Message',
      body: remoteMessage.data.body || '',
      data: remoteMessage.data,
      android: {
        channelId: 'default',
        smallIcon: 'ic_notification',
      },
    });
  }

  // Perform background tasks
  // - Sync data
  // - Update local database
  // - Schedule local notifications
});

// Background event handler for notifee
notifee.onBackgroundEvent(async ({ type, detail }) => {
  const { notification, pressAction } = detail;

  if (type === notifee.EventType.ACTION_PRESS) {
    // Handle action button press in background
    if (pressAction?.id === 'mark-read') {
      // Mark message as read in database
    }
  }

  // Remove notification after handling
  if (notification?.id) {
    await notifee.cancelNotification(notification.id);
  }
});

AppRegistry.registerComponent(appName, () => App);

App Integration

// App.tsx
import React, { useEffect } from 'react';
import { NavigationContainer } from '@react-navigation/native';
import { notificationService } from './src/services/NotificationService';
import { navigationRef } from './src/navigation/NavigationService';

function App(): React.JSX.Element {
  useEffect(() => {
    // Initialize notifications after app mounts
    const initializeNotifications = async () => {
      await notificationService.initialize();

      // Subscribe to user-specific topics
      const userId = await getUserId();
      if (userId) {
        await notificationService.subscribeToTopic(`user_${userId}`);
      }

      // Subscribe to general topics
      await notificationService.subscribeToTopic('announcements');
    };

    initializeNotifications();
  }, []);

  return (
    
      {/* Your app navigator */}
    
  );
}

export default App;

Backend Implementation (Node.js)

// server/services/NotificationService.ts
import admin from 'firebase-admin';

// Initialize Firebase Admin
admin.initializeApp({
  credential: admin.credential.cert({
    projectId: process.env.FIREBASE_PROJECT_ID,
    clientEmail: process.env.FIREBASE_CLIENT_EMAIL,
    privateKey: process.env.FIREBASE_PRIVATE_KEY?.replace(/\\n/g, '\n'),
  }),
});

interface SendNotificationOptions {
  token?: string;
  tokens?: string[];
  topic?: string;
  condition?: string;
  title: string;
  body: string;
  data?: Record;
  imageUrl?: string;
  badge?: number;
  priority?: 'normal' | 'high';
}

class FCMService {
  async sendToDevice(options: SendNotificationOptions): Promise {
    const { token, title, body, data, imageUrl, badge, priority = 'high' } = options;

    if (!token) throw new Error('Device token is required');

    const message: admin.messaging.Message = {
      token,
      notification: {
        title,
        body,
        ...(imageUrl && { imageUrl }),
      },
      data: {
        ...data,
        // Ensure all values are strings
        click_action: 'FLUTTER_NOTIFICATION_CLICK',
      },
      android: {
        priority,
        notification: {
          channelId: data?.channel || 'default',
          priority: priority === 'high' ? 'high' : 'default',
          defaultSound: true,
          defaultVibrateTimings: true,
        },
      },
      apns: {
        payload: {
          aps: {
            alert: { title, body },
            badge: badge ?? 1,
            sound: 'default',
            'mutable-content': 1,
          },
        },
        fcmOptions: {
          ...(imageUrl && { imageUrl }),
        },
      },
    };

    const response = await admin.messaging().send(message);
    console.log('Notification sent:', response);
    return response;
  }

  async sendToMultipleDevices(
    options: Omit & { tokens: string[] },
  ): Promise {
    const { tokens, title, body, data, imageUrl, priority = 'high' } = options;

    const message: admin.messaging.MulticastMessage = {
      tokens,
      notification: { title, body, imageUrl },
      data,
      android: {
        priority,
        notification: {
          channelId: data?.channel || 'default',
        },
      },
      apns: {
        payload: {
          aps: { alert: { title, body }, sound: 'default' },
        },
      },
    };

    const response = await admin.messaging().sendEachForMulticast(message);
    
    // Handle failed tokens
    if (response.failureCount > 0) {
      const failedTokens: string[] = [];
      response.responses.forEach((resp, idx) => {
        if (!resp.success) {
          failedTokens.push(tokens[idx]);
          console.error(`Failed to send to ${tokens[idx]}:`, resp.error);
        }
      });
      // Remove invalid tokens from database
      await this.removeInvalidTokens(failedTokens);
    }

    return response;
  }

  async sendToTopic(options: Omit & { topic: string }): Promise {
    const { topic, title, body, data, imageUrl } = options;

    const message: admin.messaging.Message = {
      topic,
      notification: { title, body, imageUrl },
      data,
    };

    return admin.messaging().send(message);
  }

  async sendDataOnlyMessage(token: string, data: Record): Promise {
    // Data-only messages are always processed by the app
    const message: admin.messaging.Message = {
      token,
      data,
      android: {
        priority: 'high',
      },
      apns: {
        payload: {
          aps: {
            'content-available': 1, // Silent push for iOS
          },
        },
      },
    };

    return admin.messaging().send(message);
  }

  private async removeInvalidTokens(tokens: string[]): Promise {
    // Remove invalid tokens from your database
    // await db.deviceTokens.deleteMany({ token: { $in: tokens } });
  }
}

export const fcmService = new FCMService();

API Endpoints

// server/routes/notifications.ts
import express from 'express';
import { fcmService } from '../services/NotificationService';

const router = express.Router();

// Register device token
router.post('/devices/register', async (req, res) => {
  const { token, platform, deviceId } = req.body;
  const userId = req.user.id;

  try {
    // Store token in database
    await db.deviceTokens.upsert({
      where: { deviceId },
      update: { token, platform, userId, updatedAt: new Date() },
      create: { token, platform, deviceId, userId },
    });

    res.json({ success: true });
  } catch (error) {
    res.status(500).json({ error: 'Failed to register device' });
  }
});

// Send notification to specific user
router.post('/send', async (req, res) => {
  const { userId, title, body, data } = req.body;

  try {
    // Get user's device tokens
    const devices = await db.deviceTokens.findMany({
      where: { userId },
    });

    if (devices.length === 0) {
      return res.status(404).json({ error: 'No devices found' });
    }

    const tokens = devices.map((d) => d.token);

    const result = await fcmService.sendToMultipleDevices({
      tokens,
      title,
      body,
      data,
    });

    res.json({
      success: true,
      successCount: result.successCount,
      failureCount: result.failureCount,
    });
  } catch (error) {
    res.status(500).json({ error: 'Failed to send notification' });
  }
});

// Broadcast to topic
router.post('/broadcast', async (req, res) => {
  const { topic, title, body, data } = req.body;

  try {
    await fcmService.sendToTopic({ topic, title, body, data });
    res.json({ success: true });
  } catch (error) {
    res.status(500).json({ error: 'Failed to broadcast' });
  }
});

export default router;

Common Mistakes to Avoid

Mistake 1: Requesting Permission Too Early

// WRONG - Requesting on app start
function App() {
  useEffect(() => {
    messaging().requestPermission(); // Users will likely deny!
  }, []);
}

// CORRECT - Request at contextually relevant moment
function ChatScreen() {
  const enableNotifications = async () => {
    // Show explanation first
    Alert.alert(
      'Enable Notifications',
      'Get notified when you receive new messages',
      [
        { text: 'Not Now', style: 'cancel' },
        {
          text: 'Enable',
          onPress: async () => {
            await notificationService.requestPermission();
          },
        },
      ],
    );
  };
}

Mistake 2: Not Handling Token Refresh

// WRONG - Only getting token once
const token = await messaging().getToken();
sendToServer(token);
// Token may change and become invalid!

// CORRECT - Listen for token refresh
messaging().onTokenRefresh(async (newToken) => {
  await AsyncStorage.setItem('fcmToken', newToken);
  await sendTokenToServer(newToken);
});

Mistake 3: Not Displaying Foreground Notifications on iOS

// WRONG - iOS won't show notifications when app is in foreground by default
messaging().onMessage(async (message) => {
  console.log('Got message:', message);
  // Notification not displayed!
});

// CORRECT - Use notifee or native display
messaging().onMessage(async (message) => {
  await notifee.displayNotification({
    title: message.notification?.title,
    body: message.notification?.body,
    android: { channelId: 'default' },
  });
});

Testing Push Notifications

# Test using Firebase CLI
firebase messaging:send --project YOUR_PROJECT_ID \
  --json '{"token": "DEVICE_TOKEN", "notification": {"title": "Test", "body": "Hello!"}}'

# Or use curl with FCM v1 API
curl -X POST \
  -H "Authorization: Bearer $(gcloud auth print-access-token)" \
  -H "Content-Type: application/json" \
  -d '{
    "message": {
      "token": "DEVICE_TOKEN",
      "notification": {
        "title": "Test Notification",
        "body": "This is a test"
      }
    }
  }' \
  "https://fcm.googleapis.com/v1/projects/YOUR_PROJECT_ID/messages:send"

Conclusion

Push notifications with Firebase Cloud Messaging provide a reliable and scalable way to engage users in React Native apps. By handling permissions correctly, managing device tokens securely, displaying foreground notifications properly, and implementing robust background handlers, you can deliver timely and meaningful updates that enhance user experience.

Remember to test thoroughly on both platforms, respect user preferences by not sending excessive notifications, and always provide clear value with each notification. For cross-platform patterns, explore Push Notifications in Flutter with Firebase FCM. For real-time communication, see WebSocket Servers in Python with FastAPI. Reference the official Firebase Cloud Messaging documentation and React Native Firebase messaging guide for the latest updates.