Push Notifications in React Native: Firebase, Local Notifications, and Deep Linking

13 min read
#React Native#Push Notifications#Firebase#Mobile Development#Engagement

Push notifications are the primary channel for re-engaging users with your app. A well-implemented notification system drives retention, increases daily active users, and monetization. React Native provides robust tools—Firebase Cloud Messaging for Android, Apple Push Notification service for iOS, and libraries to unify the experience.

Understanding Push Notification Architecture

Push notifications flow from your server through platform services to user devices.

Server → Firebase/APNs → Device → React Native App → User
         (Cloud Services)     (Notification Received)

Two types of notifications:

  • Remote Notifications: From your server (Firebase, your backend)
  • Local Notifications: Scheduled by the app itself (no server required)

Key Takeaways

  • Firebase Cloud Messaging (FCM) and Apple Push Notification service handle remote notifications
  • Local notifications with Notifee enable offline scheduling and reminders
  • Deep linking from notifications routes users to relevant app content
  • Notification channels (Android) and categories (iOS) organize permission requests
  • Engagement metrics (open rate, unsubscribe rate) guide notification strategy refinement
  • User preferences and frequency capping prevent notification fatigue

Setting Up Firebase Cloud Messaging (FCM)

Firebase handles Android push notifications and provides cross-platform capabilities.

Firebase Project Setup

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

Configure Android (google-services.json)

Download google-services.json from Firebase Console and place in android/app/:

{
  "project_info": {
    "project_id": "my-app-12345",
    "project_number": "123456789"
  },
  "client": [
    {
      "client_info": {
        "package_name": "com.myapp"
      }
    }
  ]
}

Initialize Firebase in React Native

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

export const initializeFCM = async () => {
  try {
    await messaging().registerDeviceForRemoteMessages();

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

    return token;
  } catch (error) {
    console.error('FCM initialization error:', error);
  }
};

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

    if (enabled) {
      console.log('Notification permission granted');
      return true;
    }
    return false;
  } catch (error) {
    console.error('Permission request error:', error);
    return false;
  }
};

Handling Push Notifications

Apps must handle notifications in three states: foreground, background, and killed.

Foreground Notifications

App is open when notification arrives.

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

export const useForegroundNotifications = (onNotification) => {
  useEffect(() => {
    const unsubscribe = messaging().onMessage(async (remoteMessage) => {
      console.log('Foreground notification received:', remoteMessage);

      onNotification({
        title: remoteMessage.notification?.title,
        body: remoteMessage.notification?.body,
        data: remoteMessage.data,
      });
    });

    return unsubscribe;
  }, [onNotification]);
};

Background Notifications

App is backgrounded, notification wakes it.

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

messaging().onNotificationOpenedApp((remoteMessage) => {
  console.log('Notification opened from background:', remoteMessage);

  const {data, notification} = remoteMessage;
  handleNotificationPress(data);
});

export const setBackgroundMessageHandler = () => {
  messaging().setBackgroundMessageHandler(async (remoteMessage) => {
    console.log('Background message received:', remoteMessage);

    await saveNotificationToDatabase(remoteMessage);
  });
};

Killed State Handling

App wasn't running when notification arrived.

export const handleInitialNotification = async (navigation) => {
  const initialNotification = await messaging().getInitialNotification();

  if (initialNotification) {
    console.log('App opened from notification (killed state)');

    handleNotificationPress(initialNotification.data, navigation);
  }
};

const handleNotificationPress = (notificationData, navigation) => {
  if (notificationData?.screen) {
    navigation.navigate(notificationData.screen, notificationData.params);
  }
};

Deep Linking from Notifications

Notifications should navigate directly to relevant content.

Notification Payload with Deep Link

From your server:

const payload = {
  notification: {
    title: 'New Message',
    body: 'John sent you a message',
  },
  data: {
    screen: 'Chat',
    conversationId: '123',
    messageId: '456',
  },
  android: {
    priority: 'high',
  },
  webpush: {
    priority: 'high',
  },
};

Handling Deep Links in Navigation

const linking = {
  prefixes: ['myapp://', 'https://myapp.com'],
  config: {
    screens: {
      Chat: 'chat/:conversationId',
      Post: 'post/:postId',
      Profile: 'profile/:userId',
    },
  },
};

const handleNotificationNavigation = (notificationData, navigation) => {
  if (notificationData?.screen === 'Chat') {
    navigation.navigate('Chat', {conversationId: notificationData.conversationId});
  } else if (notificationData?.screen === 'Post') {
    navigation.navigate('Post', {postId: notificationData.postId});
  }
};

Apple Push Notification Service (APNs)

iOS requires Apple Push Notification certificates.

Setting Up APNs

  1. Generate APNs certificate in Apple Developer Portal
  2. Download the .p8 certificate
  3. Upload to Firebase Console (Cloud Messaging settings)

The process is largely handled by Firebase, but you need the certificate in place.

iOS-Specific Handling

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

export const useIOSNotifications = () => {
  useEffect(() => {
    messaging().getIsHeadless().then((isHeadless) => {
      console.log('Is headless:', isHeadless);
    });
  }, []);
};

Local Notifications

Schedule notifications without a server.

Using react-native-notifee

npm install @notifee/react-native

Creating Local Notifications

import notifee from '@notifee/react-native';

export const createLocalNotification = async ({
  title,
  body,
  delayMillis = 5000,
  channelId = 'default',
}) => {
  try {
    await notifee.createChannel({
      id: channelId,
      name: 'Default Channel',
      lights: [0xff0000ff],
      vibration: true,
      sound: 'default',
    });

    return notifee.displayNotification({
      title,
      body,
      android: {
        channelId,
        smallIcon: 'ic_launcher_foreground',
      },
      ios: {
        sound: 'default',
      },
    });
  } catch (error) {
    console.error('Notification creation error:', error);
  }
};

const notificationId = await createLocalNotification({
  title: 'Reminder',
  body: 'Time to take a break!',
  delayMillis: 300000,
});

Scheduled Notifications

export const scheduleNotification = async ({
  title,
  body,
  triggerTime,
  id,
}) => {
  try {
    return notifee.createTriggerNotification(
      {
        title,
        body,
        android: {
          channelId: 'scheduled',
          smallIcon: 'ic_launcher_foreground',
        },
      },
      {
        type: notifee.TriggerType.TIMESTAMP,
        timestamp: triggerTime.getTime(),
      }
    );
  } catch (error) {
    console.error('Schedule error:', error);
  }
};

const tomorrow = new Date();
tomorrow.setDate(tomorrow.getDate() + 1);
tomorrow.setHours(9, 0, 0);

scheduleNotification({
  title: 'Good Morning',
  body: 'Start your day!',
  triggerTime: tomorrow,
  id: 'morning-reminder',
});

Notification Channel Management (Android)

Android Oreo+ requires notification channels for organization.

import notifee from '@notifee/react-native';

export const createNotificationChannels = async () => {
  await notifee.createChannel({
    id: 'messages',
    name: 'Messages',
    description: 'Chat and message notifications',
    importance: notifee.AndroidImportance.HIGH,
    sound: 'default',
    vibration: true,
    lights: [0xff0000ff],
    bypassDnd: false,
  });

  await notifee.createChannel({
    id: 'reminders',
    name: 'Reminders',
    description: 'App reminders and alerts',
    importance: notifee.AndroidImportance.DEFAULT,
    sound: 'notification',
    vibration: false,
  });

  await notifee.createChannel({
    id: 'critical',
    name: 'Critical Alerts',
    description: 'Security and critical alerts',
    importance: notifee.AndroidImportance.MAX,
    sound: 'alert',
    vibration: true,
    bypassDnd: true,
  });
};

Handling Notification Interactions

Users tap notifications or take actions. Handle these interactions gracefully.

import notifee from '@notifee/react-native';
import {useNavigation} from '@react-navigation/native';

export const useNotificationInteraction = () => {
  const navigation = useNavigation();

  useEffect(() => {
    return notifee.onForegroundEvent(({type, detail}) => {
      switch (type) {
        case notifee.EventType.PRESS:
          console.log('User pressed notification');
          handleNotificationTap(detail, navigation);
          break;

        case notifee.EventType.ACTION:
          console.log('User pressed action:', detail.pressAction.id);
          handleNotificationAction(detail, navigation);
          break;

        case notifee.EventType.DISMISSED:
          console.log('User dismissed notification');
          break;
      }
    });
  }, [navigation]);
};

const handleNotificationTap = (detail, navigation) => {
  const {data} = detail.notification;

  if (data?.screen) {
    navigation.navigate(data.screen, data.params);
  }
};

const handleNotificationAction = (detail, navigation) => {
  const {pressAction} = detail;

  if (pressAction.id === 'reply') {
    navigation.navigate('Chat', {conversationId: detail.notification.data?.conversationId});
  } else if (pressAction.id === 'mark_read') {
    markNotificationAsRead(detail.notification.data?.notificationId);
  }
};

Badge Count Management

Show unread count on app icon.

import notifee from '@notifee/react-native';

export const setBadgeCount = async (count) => {
  try {
    await notifee.setBadgeCount(count);
  } catch (error) {
    console.error('Badge count error:', error);
  }
};

export const incrementBadgeCount = async () => {
  try {
    const currentCount = await notifee.getBadgeCount();
    await notifee.setBadgeCount(currentCount + 1);
  } catch (error) {
    console.error('Increment badge error:', error);
  }
};

export const clearBadgeCount = async () => {
  try {
    await notifee.setBadgeCount(0);
  } catch (error) {
    console.error('Clear badge error:', error);
  }
};

Testing Notifications

Test notification flow before production.

Manual Testing

curl -X POST \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer YOUR_FCM_KEY" \
  -d '{
    "message": {
      "token": "FCM_DEVICE_TOKEN",
      "notification": {
        "title": "Test",
        "body": "Test notification"
      },
      "data": {
        "screen": "Home"
      }
    }
  }' \
  https://fcm.googleapis.com/v1/projects/YOUR_PROJECT_ID/messages:send

Automated Testing

describe('Notification Handling', () => {
  it('handles foreground notification correctly', async () => {
    const mockNotification = {
      notification: {
        title: 'Test',
        body: 'Test body',
      },
      data: {screen: 'Chat'},
    };

    const onNotification = jest.fn();
    const {result} = renderHook(() => useForegroundNotifications(onNotification));

    expect(onNotification).toHaveBeenCalledWith(
      expect.objectContaining({
        title: 'Test',
        body: 'Test body',
      })
    );
  });
});

Best Practices for Engagement

Timing:

  • Send notifications at optimal times (based on user timezone and behavior)
  • Avoid early morning or late night (unless critical)
  • Weekend timing differs from weekday

Content:

  • Keep titles under 40 characters
  • Use actionable language
  • Personalize with user name/context
  • Test different messages A/B style

Frequency:

  • Implement frequency capping (max 1-3 per day for promotional)
  • Let users control notification preferences
  • Respect user quiet hours
  • Unsubscribe users who repeatedly ignore

Privacy:

  • Don't expose sensitive data in notification text
  • Encrypt notification payloads
  • Request proper permissions
  • Comply with GDPR/CCPA

Analytics:

  • Track delivery rates
  • Measure open rates
  • Monitor unsubscribe rates
  • A/B test notification types

Push notifications are powerful tools when used responsibly. Well-timed, personalized notifications keep users engaged. Poorly executed notifications drive uninstalls. Master the technical implementation, but equally important is respecting user preferences and attention.


Let's bring your app idea to life

I specialize in mobile and backend development.

Share this article

Related Articles