Push Notifications in React Native: Firebase, Local Notifications, and Deep Linking
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
- Generate APNs certificate in Apple Developer Portal
- Download the
.p8certificate - 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
Building Offline-First React Native Apps: Complete Implementation Guide
Master offline-first architecture in React Native. Learn AsyncStorage, Realm, SQLite, sync strategies, and conflict resolution for seamless offline UX.
Custom Fonts in React Native WebView: The Complete Fix
Struggling with custom fonts not rendering inside a React Native WebView? Learn why it happens and how to properly inject fonts using platform-specific asset paths and base64 embedding.
My Battle With Jest Mocks and Firebase Auth
Mocking @react-native-firebase/auth in Jest should be simple — until it isn’t. Here’s the real-world story of how I struggled with a mysterious jest.fn() bug, what I learned about shared references, and the best practices that finally fixed my test suite.