Expo Notifications
Build with Expo Notifications for React Native push notification delivery.
You are a mobile notification engineer who integrates Expo Notifications into React Native projects. Expo Notifications provides a unified API across iOS and Android for local scheduling and remote push delivery. It abstracts platform-specific permission flows, token formats, and notification channels behind a single JavaScript API. The Expo Push Service acts as a relay, accepting a unified payload and routing to APNs and FCM. ## Key Points - **Testing on simulator**: Push tokens cannot be obtained on iOS Simulator or Android Emulator; always test on physical devices. - **Ignoring receipts**: Expo returns ticket IDs that must be checked later for delivery errors; ignoring them lets stale tokens accumulate. - **Requesting permissions immediately**: Prompting on first launch without context leads to denials; pre-explain the value first. - **Hardcoding project ID**: Use `Constants.expoConfig` to read the project ID dynamically rather than hardcoding it. - React Native apps built with the Expo managed or bare workflow - Cross-platform mobile push without direct FCM/APNs server integration - Apps that need both local scheduled and remote push notifications - Teams that want a single push API endpoint instead of managing two platforms - Rapid prototyping of notification features in Expo projects ## Quick Example ```bash npx expo install expo-notifications expo-device expo-constants ``` ```env EXPO_ACCESS_TOKEN=your_expo_access_token ```
skilldb get notification-services-skills/Expo NotificationsFull skill: 196 linesExpo Notifications
You are a mobile notification engineer who integrates Expo Notifications into React Native projects. Expo Notifications provides a unified API across iOS and Android for local scheduling and remote push delivery. It abstracts platform-specific permission flows, token formats, and notification channels behind a single JavaScript API. The Expo Push Service acts as a relay, accepting a unified payload and routing to APNs and FCM.
Core Philosophy
Unified Token Management
Expo issues its own push token (ExpoPushToken) that wraps the underlying platform token. Your server sends to Expo's push service using this token, avoiding direct FCM/APNs integration. Always fetch the token on every app launch since it can change, and persist it to your backend with the associated user ID.
Permission-First Design
iOS requires explicit notification permission; Android 13+ does too. Always check and request permissions before attempting to get a push token. Design your UX to explain why notifications matter before triggering the system prompt, as a denied prompt cannot be re-triggered without the user visiting Settings.
Foreground Handling Matters
By default, notifications received while the app is in the foreground are silently consumed. You must configure setNotificationHandler to decide whether to show alerts, play sounds, or set badges when the app is active. Ignoring this leads to reports of "notifications not working" from users who are actively using the app.
Setup
Install
npx expo install expo-notifications expo-device expo-constants
Environment Variables
EXPO_ACCESS_TOKEN=your_expo_access_token
Key Patterns
1. Register for Push Notifications
Do:
import * as Notifications from "expo-notifications";
import * as Device from "expo-device";
import Constants from "expo-constants";
async function registerForPush(): Promise<string | null> {
if (!Device.isDevice) return null;
const { status: existing } = await Notifications.getPermissionsAsync();
let finalStatus = existing;
if (existing !== "granted") {
const { status } = await Notifications.requestPermissionsAsync();
finalStatus = status;
}
if (finalStatus !== "granted") return null;
const projectId = Constants.expoConfig?.extra?.eas?.projectId;
const token = await Notifications.getExpoPushTokenAsync({ projectId });
return token.data;
}
Not this:
// Skipping permission check and device validation
const token = await Notifications.getExpoPushTokenAsync();
// Crashes on simulator, fails silently without permissions
2. Configure Foreground Handling
Do:
Notifications.setNotificationHandler({
handleNotification: async (notification) => {
const data = notification.request.content.data;
const showInForeground = data.showWhenActive !== false;
return {
shouldShowAlert: showInForeground,
shouldPlaySound: showInForeground,
shouldSetBadge: true,
};
},
});
Not this:
// Forgetting to set handler — notifications silently swallowed in foreground
// No call to setNotificationHandler at all
3. Server-Side Sending via Expo Push API
Do:
import { Expo, ExpoPushMessage } from "expo-server-sdk";
const expo = new Expo();
async function sendPush(tokens: string[], title: string, body: string) {
const messages: ExpoPushMessage[] = tokens
.filter((t) => Expo.isExpoPushToken(t))
.map((to) => ({ to, title, body, sound: "default" as const }));
const chunks = expo.chunkPushNotifications(messages);
for (const chunk of chunks) {
const receipts = await expo.sendPushNotificationsAsync(chunk);
// Store ticket IDs for later receipt checking
}
}
Not this:
// Sending one-by-one without chunking or token validation
for (const token of tokens) {
await fetch("https://exp.host/--/api/v2/push/send", {
method: "POST",
body: JSON.stringify({ to: token, title, body }),
});
}
Common Patterns
Handling Notification Taps
import { useEffect } from "react";
import * as Notifications from "expo-notifications";
import { router } from "expo-router";
function useNotificationNavigation() {
useEffect(() => {
const subscription = Notifications.addNotificationResponseReceivedListener(
(response) => {
const data = response.notification.request.content.data;
if (data.screen) {
router.push(data.screen as string);
}
}
);
return () => subscription.remove();
}, []);
}
Scheduling Local Notifications
await Notifications.scheduleNotificationAsync({
content: { title: "Reminder", body: "Your task is due in 15 minutes" },
trigger: { seconds: 900, type: Notifications.SchedulableTriggerInputTypes.TIME_INTERVAL },
});
Android Channel Configuration
await Notifications.setNotificationChannelAsync("orders", {
name: "Order Updates",
importance: Notifications.AndroidImportance.HIGH,
sound: "default",
vibrationPattern: [0, 250, 250, 250],
});
Checking Receipts for Delivery Errors
const receiptChunks = expo.chunkPushNotificationReceiptIds(ticketIds);
for (const chunk of receiptChunks) {
const receipts = await expo.getPushNotificationReceiptsAsync(chunk);
for (const [id, receipt] of Object.entries(receipts)) {
if (receipt.status === "error") {
if (receipt.details?.error === "DeviceNotRegistered") {
await removeToken(id);
}
}
}
}
Anti-Patterns
- Testing on simulator: Push tokens cannot be obtained on iOS Simulator or Android Emulator; always test on physical devices.
- Ignoring receipts: Expo returns ticket IDs that must be checked later for delivery errors; ignoring them lets stale tokens accumulate.
- Requesting permissions immediately: Prompting on first launch without context leads to denials; pre-explain the value first.
- Hardcoding project ID: Use
Constants.expoConfigto read the project ID dynamically rather than hardcoding it.
When to Use
- React Native apps built with the Expo managed or bare workflow
- Cross-platform mobile push without direct FCM/APNs server integration
- Apps that need both local scheduled and remote push notifications
- Teams that want a single push API endpoint instead of managing two platforms
- Rapid prototyping of notification features in Expo projects
Install this skill directly: skilldb add notification-services-skills
Related Skills
Apple Push Notification
Integrate Apple Push Notification service (APNs) for iOS, macOS, and Safari
Courier
Integrate Courier notification orchestration for multi-channel message routing.
Engagespot
Engagespot is a multi-channel notification API and in-app feed service. It helps
Firebase Cloud Messaging
Integrate Firebase Cloud Messaging (FCM) for cross-platform push notifications.
Knock
Implement Knock notification infrastructure for multi-channel delivery.
Magicbell
Build with MagicBell for embeddable notification inboxes and multi-channel delivery.