React Native Push Notifications: Expo Notifications, APNs/FCM, Deep Links, and Notification Center
Implement React Native push notifications with Expo Notifications: APNs and FCM setup, deep link handling, local notifications, notification center UI, and server-side push with Expo Push API.
React Native Push Notifications: Complete Setup (2026)
Push notifications are the lifeline of mobile engagement. They keep users informed, drive app opens, and turn passive users into active ones. But implementing them correctly in React Native involves navigating Firebase, platform-specific quirks, permission handling, and delivery resilience. Get it wrong, and your notifications silently fail in production while users question why they ever installed your app.
At Viprasol, we've integrated push notifications into dozens of React Native applications, from small startups to platforms serving millions. This guide covers the complete journey: from selecting the right service to handling the edge cases that appear in production.
Why Push Notifications Matter for Mobile Apps
Push notifications are unique in mobile development:
- User re-engagement: A well-timed notification can be the difference between a user opening your app or forgetting it exists
- Time-sensitive information: Order updates, appointment reminders, security alerts—these need immediate visibility
- Lower friction than email: Notifications appear on-device; email gets lost in folders
- Metrics that matter: Open rates, conversion paths, and retention all improve with strategic push use
But there's a cost. Poorly implemented notifications create friction: permission denials, battery drain, false alarms. Users who don't grant push permission are significantly harder to engage.
Choosing a Push Service
Before writing code, you need a delivery platform. The main options:
| Service | Strengths | Downsides | Best For |
|---|---|---|---|
| Firebase Cloud Messaging (FCM) | Free, handles both platforms, excellent uptime | Limited analytics, console-focused | Most React Native apps |
| Apple Push Notification (APN) | Native Apple platform | Complex certificate setup | iOS-only scenarios |
| OneSignal | Great dashboard, segmentation, A/B testing | Paid, vendor lock-in | Growth-focused teams |
| Expo Notifications | Works with Expo SDK, simplified API | Limited control, Expo dependency | Expo-managed projects |
At Viprasol, we typically recommend FCM as the starting point for cross-platform apps. It's free, reliable, and integrates well with React Native. For advanced segmentation and analytics, consider OneSignal as a wrapper around FCM.
🌐 Looking for a Dev Team That Actually Delivers?
Most agencies sell you a project manager and assign juniors. Viprasol is different — senior engineers only, direct Slack access, and a 5.0★ Upwork record across 1000+ projects.
- React, Next.js, Node.js, TypeScript — production-grade stack
- Fixed-price contracts — no surprise invoices
- Full source code ownership from day one
- 90-day post-launch support included
Setup: Firebase Cloud Messaging
Step 1: Configure Firebase
Start with a Firebase project:
Code:
# Create a project at https://console.firebase.google.com
# Enable Cloud Messaging
# Download configuration files
For iOS, download the GoogleService-Info.plist. For Android, download google-services.json.
Step 2: Install Dependencies
Code:
npm install firebase @react-native-firebase/app @react-native-firebase/messaging
# or yarn
yarn add firebase @react-native-firebase/app @react-native-firebase/messaging
For Expo projects, use the Expo SDK:
Code:
expo install expo-notifications
Step 3: Platform-Specific Setup
Android: Add your google-services.json to the android/app/ directory. Update android/build.gradle:
Code:
buildscript {
dependencies {
classpath 'com.google.gms:google-services:4.3.15'
}
}
And android/app/build.gradle:
Code:
apply plugin: 'com.google.gms.google-services'
iOS: Add GoogleService-Info.plist to your Xcode project. Enable "Push Notifications" capability in Xcode under your target's Signing & Capabilities tab.
Requesting Permissions
Users must grant permission before receiving notifications. This is a critical UX moment.
Code:
import messaging from '@react-native-firebase/messaging';
import { PermissionsAndroid, Platform } from 'react-native';
async function requestNotificationPermission() {
if (Platform.OS === 'android' && Platform.Version >= 33) {
const permission = await PermissionsAndroid.request(
PermissionsAndroid.PERMISSIONS.POST_NOTIFICATIONS
);
return permission === PermissionsAndroid.RESULTS.GRANTED;
}
if (Platform.OS === 'ios') {
const authorizationStatus = await messaging.requestPermission();
return authorizationStatus === messaging.AuthorizationStatus.AUTHORIZED;
}
return true;
}
// Call during onboarding
await requestNotificationPermission();
Best practice: Request permission at a moment of high context. After a successful action (account creation, first order) works better than on app launch when users say no reflexively.

🚀 Senior Engineers. No Junior Handoffs. Ever.
You get the senior developer, not a project manager who relays your requirements to someone you never meet. Every Viprasol project has a senior lead from kickoff to launch.
- MVPs in 4–8 weeks, full platforms in 3–5 months
- Lighthouse 90+ performance scores standard
- Works across US, UK, AU timezones
- Free 30-min architecture review, no commitment
Recommended Reading
Getting and Storing Device Tokens
Once permission is granted, retrieve the device token:
Code:
async function getDeviceToken() {
try {
const token = await messaging().getToken();
// Send this token to your backend
await apiClient.post('/users/device-token', { token });
return token;
} catch (error) {
console.error('Failed to get notification token:', error);
}
}
// Handle token refresh (happens periodically)
messaging().onTokenRefresh(token => {
console.log('New token:', token);
apiClient.post('/users/device-token', { token });
});
// Call after permission is granted
useEffect(() => {
getDeviceToken();
}, []);
Store tokens in your backend database linked to user accounts. A user can have multiple tokens (different devices), so support that structure.
Handling Incoming Notifications
There are three scenarios for notification arrival:
1. App Foreground (App is Open)
By default, foreground notifications don't show the system alert. Handle them manually:
Code:
messaging().onMessage(remoteMessage => {
console.log('Notification received:', remoteMessage);
// Show in-app alert
Alert.alert(
remoteMessage.notification.title,
remoteMessage.notification.body
);
// Or show a toast using a library like react-native-toast-message
});
2. App Background (App is Closed or Backgrounded)
Use onNotificationOpenedApp to handle taps on background notifications:
Code:
messaging().onNotificationOpenedApp(remoteMessage => {
console.log('Notification opened app:', remoteMessage);
// Navigate to relevant screen
if (remoteMessage.data?.screen === 'Order') {
navigateTo('OrderDetail', { orderId: remoteMessage.data.orderId });
}
});
// Handle notification that opened the app on first launch
messaging().getInitialNotification().then(remoteMessage => {
if (remoteMessage) {
console.log('App opened from notification:', remoteMessage);
// Route accordingly
}
});
3. Deep Linking with Notifications
Use data payload for routing:
Code:
// Send from backend
{
"notification": {
"title": "Your order shipped",
"body": "Order #12345 is on the way"
},
"data": {
"screen": "OrderDetail",
"orderId": "12345"
}
}
Then handle it in your navigation:
Code:
const handleNotificationNavigation = (remoteMessage) => {
const { data } = remoteMessage;
switch (data?.screen) {
case 'OrderDetail':
navigation.navigate('Orders', {
screen: 'Detail',
params: { id: data.orderId }
});
break;
case 'Message':
navigation.navigate('Messages', { id: data.messageId });
break;
default:
break;
}
};
Sending Notifications from Your Backend
You need a backend service to send notifications. Here's a Node.js example:
Code:
const admin = require('firebase-admin');
admin.initializeApp(serviceAccount);
async function sendNotification(deviceToken, title, body, data = {}) {
try {
const response = await admin.messaging().send({
token: deviceToken,
notification: {
title,
body,
},
data,
android: {
priority: 'high',
notification: {
channelId: 'default',
},
},
apns: {
headers: {
'apns-priority': '10',
},
},
});
console.log('Message sent:', response);
return response;
} catch (error) {
console.error('Error sending message:', error);
throw error;
}
}
// Send to multiple devices
async function sendMulticast(deviceTokens, title, body) {
const message = {
notification: { title, body },
android: { priority: 'high' },
};
const response = await admin.messaging().sendMulticast({
...message,
tokens: deviceTokens,
});
// Log failures to retry later
response.responses.forEach((result, index) => {
if (!result.success) {
console.error(**Failed to send to ${deviceTokens[index]}:**, result.error);
}
});
return response;
}
Handling Edge Cases and Failures
Real-world push notifications have complexities:
Invalid Tokens
FCM invalidates tokens when:
- User uninstalls your app
- User clears app data
- User disables notifications
- Device factory resets
- 90+ days without app use
Catch send failures and clean up:
Code:
async function sendWithCleanup(userId, deviceToken, message) {
try {
await admin.messaging().send({
token: deviceToken,
...message
});
} catch (error) {
if (error.code === 'messaging/invalid-registration-token' ||
error.code === 'messaging/registration-token-not-registered') {
// Remove invalid token
await db.collection('users')
.doc(userId)
.update({
deviceTokens: admin.firestore.FieldValue.arrayRemove(deviceToken)
});
}
throw error;
}
}
Rate Limiting
FCM has soft limits on how many messages you can send. Batch and implement exponential backoff:
Code:
async function sendBatchWithRetry(messages, maxRetries = 3) {
let batch = [];
for (const msg of messages) {
batch.push(msg);
if (batch.length === 500) {
await sendBatch(batch);
batch = [];
await delay(1000); // Pause between batches
}
}
if (batch.length > 0) {
await sendBatch(batch);
}
}
async function sendBatch(messages) {
let retries = 0;
while (retries < 3) {
try {
await admin.messaging().sendMulticast({
messages,
});
return;
} catch (error) {
retries++;
if (retries >= 3) throw error;
await delay(Math.pow(2, retries) * 1000);
}
}
}
Silent Failures
Some notifications never reach users (network down, permission denied). Add telemetry:
Code:
// In your React Native app
messaging().onMessage(remoteMessage => {
analytics.track('notification_received', {
title: remoteMessage.notification.title,
timestamp: new Date().toISOString(),
});
});
messaging().onNotificationOpenedApp(remoteMessage => {
analytics.track('notification_opened', {
title: remoteMessage.notification.title,
});
});
Testing Notifications
Local Testing
Use the Firebase Console to send test messages, or send directly from your backend during development.
Segmentation and Analytics
The most sophisticated approach: send test messages to segments of users, track metrics, then scale:
Code:
// Segment: beta testers
const betaTestTokens = await db.collection('users')
.where('isBetaTester', '==', true)
.select('deviceTokens')
.get();
// Send and track
const results = await sendMulticast(betaTestTokens, title, body);
await analytics.logSegmentPerformance('beta_testers', results);
Compliance and Best Practices
Best practices:
- Respect user preferences: Let users control notification frequency and types
- Test on real devices: Simulators don't reliably show notification behavior
- Avoid permission spam: Request once, respectfully
- Monitor battery impact: Too many notifications drain batteries
- Use data payloads thoughtfully: Keep payloads under 4KB
- Implement quiet hours: Don't send notifications outside user's timezone
Compliance:
- GDPR: Get explicit consent before storing device tokens
- CCPA: Allow users to delete device tokens on request
- Platform guidelines: Apple and Google have strict notification rules
Monitoring and Debugging
Common issues and solutions:
| Issue | Cause | Solution |
|---|---|---|
| Notifications not received | Invalid token, disabled permissions | Check Firebase logs, verify permission granted |
| Notification appears but without body | Data-only message | Include notification payload |
| Users never grant permission | Wrong timing or bad UX | Request after positive action |
| High failed-send rate | Stale tokens | Implement cleanup on send failures |
For production apps, integrate with professional cloud solutions that include notification monitoring and alerting.
FAQ
Q: Can I send notifications without Firebase?
Yes, but FCM is free and handles the hard parts. Alternatives like OneSignal, Pusher, or custom AWS SNS setups exist, but you'll be reimplementing what FCM already handles. Start with FCM unless you have a specific reason not to.
Q: Why don't notifications always appear?
Multiple reasons: permissions denied, token invalid, network unavailable, user has notification shade disabled, battery saver on, quiet hours active. Always monitor delivery and have fallback communication channels (email, in-app messages).
Q: What's the difference between notification and data messages?
Notification messages show system alerts automatically. Data messages go to your app code for custom handling. Most use cases need both: notification for visibility, data for routing and metadata.
Q: Can I schedule notifications?
Not natively through FCM—you need a backend job scheduler (Cloud Tasks, Celery, cron). Schedule a task to send at the target time, then send via FCM.
Q: How do I handle users with multiple devices?
Store an array of tokens per user. Segment sends by device type if your message differs. Use user-targeted messaging (instead of device tokens) through Firestore integration for automatic deduplication.
Getting Help
Push notifications can get complex. At Viprasol, our web development and SaaS development teams build notification systems that scale reliably. Whether you're debugging a delivery issue or architecting a new engagement feature, we've solved it before.
The implementation above covers 90% of real-world use cases. What matters most is starting simple, testing thoroughly on real devices, and monitoring from day one.
Last updated: March 2026. Firebase Cloud Messaging APIs are stable; best practices continue to evolve with platform changes.
External Resources
About the Author
Viprasol Tech Team
Custom Software Development Specialists
The Viprasol Tech team specialises in algorithmic trading software, AI agent systems, and SaaS development. With 1000+ projects delivered across MT4/MT5 EAs, fintech platforms, and production AI systems, the team brings deep technical experience to every engagement.
Need a Modern Web Application?
From landing pages to complex SaaS platforms — we build it all with Next.js and React.
Free consultation • No commitment • Response within 24 hours
Need a custom web application built?
We build React and Next.js web applications with Lighthouse ≥90 scores, mobile-first design, and full source code ownership. Senior engineers only — from architecture through deployment.