1. Setting Up expo-notifications
npx expo install expo-notifications expo-device
import * as Notifications from 'expo-notifications';
Notifications.setNotificationHandler({
handleNotification: async () => ({
shouldShowAlert: true,
shouldPlaySound: true,
shouldSetBadge: false,
}),
});
This handler runs whenever a notification arrives while the app is open — without it, a foreground notification is received silently with no visible alert, which surprises most people building this for the first time.
2. Getting a Push Token
import * as Notifications from 'expo-notifications';
import * as Device from 'expo-device';
export async function registerForPushNotifications() {
if (!Device.isDevice) return null; // push tokens require a real device
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 token = await Notifications.getExpoPushTokenAsync({
projectId: 'your-eas-project-id',
});
return token.data; // send this to your backend, keyed to the signed-in user
}
This token is what your backend sends notifications to — store it against the signed-in user's record the same way you'd store any other device metadata, and re-register (tokens can change) on every app launch.
3. Under the Hood: FCM & APNs
expo-notifications doesn't invent its own delivery network — it's a unified layer over each platform's real push service:
- FCM (Firebase Cloud Messaging) — Android's delivery service. A message sent through FCM reaches the device even if the app isn't running.
- APNs (Apple Push Notification service) — the iOS equivalent, requiring a signed push certificate or key registered with Apple.
Sending through Expo's push service (as the token above implies) means your backend talks to one simple API, and Expo's service handles routing the message to FCM or APNs correctly depending on the token's platform — you can also bypass Expo's service and talk to FCM/APNs directly for more control, at the cost of implementing both.
4. Handling Taps & Foreground Arrivals
useEffect(() => {
const receivedSub = Notifications.addNotificationReceivedListener((notification) => {
console.log('Received in foreground:', notification.request.content.title);
});
const responseSub = Notifications.addNotificationResponseReceivedListener((response) => {
const postId = response.notification.request.content.data.postId;
navigation.navigate('PostDetail', { postId });
});
return () => {
receivedSub.remove();
responseSub.remove();
};
}, [navigation]);
addNotificationReceivedListener fires when a notification arrives
while the app is in the foreground (paired with the handler from earlier deciding
whether to show an alert). addNotificationResponseReceivedListener
fires when the user taps a notification — foreground, background, or from a fully
closed app — and is where deep-link-style navigation, using data attached to the
notification payload, belongs.
5. Hands-on Exercise
Build a working push notification flow
Request permission, register a token, send a real test push, and handle both delivery cases.
Requirements:
- A permission flow that checks existing status before requesting, and stores the result of registration (a token or a clear denied state).
- Send yourself a real test notification using Expo's push notification tool (expo.dev/notifications) with a
postIdin the data payload. - Confirm the notification triggers
addNotificationReceivedListenerand shows an alert when sent while the app is open. - Confirm tapping the notification (with the app backgrounded, and again with it fully closed) navigates to the correct
PostDetailscreen using the payload'spostId. - Both listeners cleaned up correctly on unmount.
Push notifications don't work in the iOS Simulator or most emulator configurations — this exercise genuinely needs a physical device with a development build installed (not Expo Go, for anything beyond the most basic test), so budget time for that setup if you haven't built one yet.
6. Knowledge Check
Four quick questions. Expand each to check your answer.
Q1
Why does a notification arrive silently, with no visible alert, if setNotificationHandler is never called?
Why does a notification arrive silently, with no visible alert, if setNotificationHandler is never called?
Without a handler telling expo-notifications what to do, a foreground-arriving notification has no instruction to show an alert, play a sound, or set a badge — the handler's returned object is what opts into each of those behaviors. This is the single most common "why isn't this working" moment when first setting up notifications.
Q2
What is a push token, and why does it need to be sent to your backend rather than kept only on the device?
What is a push token, and why does it need to be sent to your backend rather than kept only on the device?
It's the address your backend's server needs in order to send a notification to that specific device through Expo's push service (or FCM/APNs directly) — without your backend having it stored, there's no way to target a notification at that user's device at all.
Q3
What do FCM and APNs each actually deliver, and where does expo-notifications fit relative to them?
What do FCM and APNs each actually deliver, and where does expo-notifications fit relative to them?
FCM (Android) and APNs (iOS) are the two platforms' real, underlying push delivery networks — the thing that actually gets a message to a device, even one whose app isn't running. expo-notifications (via Expo's push service) sits on top of both, giving your backend one unified API instead of implementing FCM and APNs integration separately.
Q4
Why does handling a notification tap need a different listener than handling a foreground arrival?
Why does handling a notification tap need a different listener than handling a foreground arrival?
A tap can happen from three very different app states — foreground, backgrounded, or fully closed — and represents user intent to act on the notification (typically, navigate somewhere), while a foreground arrival is just informational and the app is already running normally. addNotificationResponseReceivedListener is specifically the one guaranteed to fire across all three tap scenarios, which a plain "received" listener is not designed to guarantee for a background or closed app.