Push Notifications
Push notifications on React Native are split into two systems: FCM on Android and APNs on iOS. Most teams use Firebase Cloud Messaging as the unified API (it can deliver to APNs as well). The flow is the same: request permission, get a device token, send it to your backend, and your backend sends notifications via FCM.
FCM setup, token registration, foreground handling
EXAMPLE
// npm i @react-native-firebase/app @react-native-firebase/messaging
import messaging from '@react-native-firebase/messaging';
import { Platform, Alert } from 'react-native';
import { PermissionsAndroid } from 'react-native';
// 1) iOS + Android 13+ require explicit permission
export async function requestPushPermission() {
if (Platform.OS === 'android' && Platform.Version >= 33) {
const r = await PermissionsAndroid.request(
PermissionsAndroid.PERMISSIONS.POST_NOTIFICATIONS,
);
if (r !== PermissionsAndroid.RESULTS.GRANTED) return false;
}
const authStatus = await messaging().requestPermission();
return (
authStatus === messaging.AuthorizationStatus.AUTHORIZED ||
authStatus === messaging.AuthorizationStatus.PROVISIONAL
);
}
// 2) Get the device token and ship it to your backend
export async function registerForPush(userId: string) {
const ok = await requestPushPermission();
if (!ok) return;
const token = await messaging().getToken();
await fetch('/api/devices', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ userId, token, platform: Platform.OS }),
});
// Refresh handler — tokens rotate after backup restore, reinstall, etc.
messaging().onTokenRefresh(async (fresh) => {
await fetch('/api/devices', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ userId, token: fresh, platform: Platform.OS }),
});
});
}
// 3) Handle messages — foreground, background, and tap-to-open
messaging().onMessage(async (remote) => {
// App in foreground: notifications do NOT pop a banner by default.
// Show your own in-app banner here.
Alert.alert(remote.notification?.title ?? 'New message', remote.notification?.body);
});
messaging().setBackgroundMessageHandler(async (remote) => {
// App in background or killed: this runs in a JS context with no UI.
// Use it for data-only notifications to update local cache, badges, etc.
await saveToLocalCache(remote.data);
});
// 4) Cold-start: did the user tap a notification to launch the app?
messaging()
.getInitialNotification()
.then((remote) => {
if (remote?.data?.route) navigation.navigate(remote.data.route);
});
// 5) Backend (Node admin SDK) — send to a specific token or topic
// import admin from 'firebase-admin';
// await admin.messaging().send({
// token: '<device-token>',
// notification: { title: 'Order shipped', body: 'Tap to track' },
// data: { route: 'OrderDetail', orderId: '7001' },
// android: { priority: 'high' },
// apns: { headers: { 'apns-priority': '10' } },
// });
async function saveToLocalCache(data: any) { /* AsyncStorage / SQLite */ }
Why it matters
Send the device token to the backend keyed by user, not by install. Tokens rotate, so always overwrite-on-conflict and treat onTokenRefresh as the source of truth. A backend that keeps stale tokens both wastes FCM quota and miscounts active devices in dashboards.
Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.
Example
Example
import * as Notifications from 'expo-notifications';
const { data: token } = await Notifications.getExpoPushTokenAsync();
console.log('expo push token:', token);
Try it Yourself »
Discussion
Loading…