React Native Remote Notifications: Getting It Right Across Different Devices and App States

Adding support for receiving remote push notifications is a great way to drive engagement with your mobile app. While there are plenty of libraries to do the heavy lifting for you, configuring those packages exactly right and manually testing your solution can be tricky.

Below, I’ll share how we configured our React Native mobile app for push notification support using Firebase Cloud Messaging. In addition, I’ll point out the different scenarios you should make sure to hit when manually testing your implementation.

Setup

We used Firebase Cloud Messaging (FCM) for delivering remote push notifications. Follow all of the instructions for the @react-native-firebase/app and @react-native-firebase/messaging modules.

We also used react-native-push-notification for further control over handling received notifications (specifically, managing the app icon unread counter). Make the changes to your project configuration files as specified in the library’s install instructions. Since we can use the React Native Firebase message handlers, we can do the bare minimum for configuration of react-native-push-notification. The following addition to your index.js should be sufficient:


import PushNotificationIOS from '@react-native-community/push-notification-ios';
import PushNotification from 'react-native-push-notification';

PushNotification.configure({
  onNotification: function (notification) {
    // Intentionally not doing anything in this handler
    // We handle received notifications using the FCM handlers instead
    notification.finish(PushNotificationIOS.FetchResult.NoData);
  },
});

Adding Message Handlers

To enroll the device into receiving push notifications through FCM, you should use messaging().requestPermission() and messaging().getToken() wherever it makes sense in your app’s onboarding workflow. For example, we get the Firebase messaging token and send it along to our server on user login.

Once that is done, add the following message handlers. First, drop a background message handler into your index.js before registering your main App component:


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

// Handles messages received when the app is backgrounded or quit.
messaging().setBackgroundMessageHandler(message => {
  setBadgeCount(message);
  return Promise.resolve();
});

Next, add a useEffect for handling messages received in the foreground to your main App component:


  useEffect(() => {
    // Handles messages received when the app is running in the foreground.
    const subscription = messaging().onMessage(async message => {
      setBadgeCount(message);
    });

    return subscription;
  }, []);

And finally, add the following to your main App component to handle when a notification opens the app:


  useEffect(() => {
    // Called when user presses a notification to open the app from a backgrounded state
    Messaging.onNotificationOpenedApp(_message => {
      // This is where you would wire up handling for deep links and loading notification content if user is already authenticated
      return Promise.resolve();
    });
  }, []);

Your message handlers can do whatever you need them to do, but in this example, note the setBadgeCount function. We noticed that for Android, the unread count set on the Android notification config caused the badge count to stack. And for iOS, the unread counter would not update if the notification was received in the foreground.

So, we added this function because we wanted to override the default behavior. To do something similar, I suggest adding the unread count to the message’s data (rather than the notification content, for example). This ensures consistency across different device and message types. Here is the body of that function:


import { FirebaseMessagingTypes } from '@react-native-firebase/messaging';
import PushNotification from 'react-native-push-notification';

export const setBadgeCount = (message: FirebaseMessagingTypes.RemoteMessage) => {
  try {
    const badge = message?.data?.badge;
    if (badge) {
      const count = parseInt(badge, 10);
      if (!isNaN(count)) {
        PushNotification.setApplicationIconBadgeNumber(count);
      }
    }
  } catch {
    console.error('error parsing badge count');
  }
};

Manual Testing

Finally, here are a few considerations as you manually test your solution.

First and foremost, note that whatever is responsible for sending the remote notifications will also need to configure the remote message properly in order to ensure that the message is received. If your app is not even receiving the appropriate messages, this is the first thing to double-check.

There is also device-specific behavior to take into consideration. On iOS, you will need to test your solution on an actual device. Apple’s notification delivery service, APNS, will not deliver remote notifications to a simulator, so be sure to run your app on a real device. For Android, be aware that push notification behavior such as the unread app icon counter depends on the Android launcher for that device. Be sure to test your solution on a device with a launcher that supports badging.

Devices handle receiving messages with notification content and messages with data only differently. Since data-only messages are a common way to handle things like badge count updates and triggering additional data fetches, you’ll want to test that your app is receiving and handling both notification and data-only messages appropriately.

Finally, remote notifications can be received in one of three possible app states: foregrounded, backgrounded or quit. In the above section, we covered which message handlers should be used for which app states. Be sure to test receiving different types of messages in each of these three app states.


I hope this guide will help give you confidence that your React Native app is configured appropriately to receive notifications from Firebase Cloud Messaging.