Recommended resources
The freely available resources this lesson will direct you to.
Welcome back. In our last lesson, we successfully implemented the logic to request user permissions and retrieve a unique device registration token. This token is the "address" your server needs to send a notification to a specific device. Now, we'll focus on the other side of that equation: what happens in your React app when a notification actually arrives?
Today's goal is to handle incoming push notification payloads, covering the two critical scenarios your app will encounter: when it's active in the foreground, and when it's brought into focus from the background after a user taps a notification. Mastering this is key to creating a responsive and engaging user experience, moving beyond simple alerts to enable features like in-app navigation and deep linking.
The Flow of a Push Notification
Before we dive into the code, let's visualize the journey of a push notification. A server-side component, such as your backend or the Firebase console, sends a message request to a service like Firebase Cloud Messaging (FCM). FCM then routes this message to the correct device, identified by the token we retrieved in the previous lesson.

Once the notification reaches the device, the operating system's behavior depends on the state of your application. This is where our new code comes into play.
Handling Foreground Notifications
When your app is open and running in the foreground, the default behavior of both iOS and Android is to not display a system-level notification. The assumption is that you, the developer, might want to handle it differently, perhaps by showing a subtle in-app banner or toast instead of interrupting the user with a standard system alert.
The @capacitor/push-notifications plugin fires the pushNotificationReceived event in this scenario. This allows your web code to intercept the notification payload and decide what to do next.
await PushNotifications.addListener('pushNotificationReceived', (notification) => {
console.log('Push notification received: ', notification);
// Example: Show a custom in-app toast
// showToast(`New message from ${notification.data.sender}`);
});
The notification object passed to this listener contains all the information about the push, including its title, body, and any custom data you sent with it. Let's look at the structure of this object.
Push Notifications Capacitor Plugin API
The official Capacitor documentation defines the PushNotificationSchema interface, which describes the shape of the notification object your listeners will receive.
In the API documentation, find the heading for PushNotificationSchema. Review the properties like data. The data property is particularly important, as it's where you'll place custom information, such as a user ID or a route for deep linking.
Making Foreground Notifications Visible
What if you do want the standard system notification to appear, even when your app is in the foreground? Capacitor allows you to configure this behavior using the presentationOptions property in your capacitor.config.ts file.
This option takes an array of strings:
'badge': Updates the app icon's badge count (iOS only).'sound': Plays a sound.'alert': Displays the system notification alert.
// capacitor.config.ts
import { CapacitorConfig } from '@capacitor/cli';
const config: CapacitorConfig = {
// ... other configs
plugins: {
PushNotifications: {
presentationOptions: ["badge", "sound", "alert"],
},
},
};
export default config;
With these options set, a notification arriving while your app is open will now behave just like one arriving when it's in the background, while still firing the pushNotificationReceived event for you to process.
For a quick visual overview of this configuration, you can refer to the following video.
The Push Notifications Guide for Ionic & Capacitor
Simon Grimm's video on Capacitor push notifications provides a complete walkthrough. This short segment focuses specifically on the presentationOptions.
Watch the section from this timestamp to see how the presentationOptions are added to the Capacitor configuration to control the visibility of notifications when the app is open.
Handling User Interaction and Deep Linking
The most powerful aspect of push notifications is guiding the user to relevant content. When your app is in the background or closed, the operating system displays the notification. When the user taps it, your app is brought to the foreground. This action triggers the pushNotificationActionPerformed event.
This event is your hook for implementing deep linking. The process looks like this:
- When you send the notification from your server, you embed custom data in the payload (e.g.,
{ "route": "/messages/42" }). - Your app registers a listener for
pushNotificationActionPerformed. - When a user taps the notification, the listener fires, and you can access the
notification.datapayload. - You parse the data and use your app's router (like React Router) to navigate the user to the correct screen.
await PushNotifications.addListener('pushNotificationActionPerformed', (action) => {
console.log('Push notification action performed', action);
const route = action.notification.data?.route;
if (route) {
// Assuming you are using React Router and have a `history` object
history.push(route);
}
});
This transforms a generic alert into a seamless pathway, which is crucial for good user experience.
The following resources provide excellent practical guidance on implementing this deep linking flow.
Your Guide to Capacitor Push Notifications
This guide offers a great narrative on using push notifications for better UX. It explains how to structure your payload and app logic for deep linking.
First, read the section on deep linking to understand the concept of adding a custom route to your payload. Then, read the Q&A section about handling closed-app notifications, which reinforces that pushNotificationActionPerformed is the correct tool for this job.
The Push Notifications Guide for Ionic & Capacitor
This video demonstrates the entire deep linking flow, from sending a payload with custom data in Firebase to handling it in the app code.
First, watch how to construct a test notification with a custom data field (detailsID) in the Firebase console from this segment. Then, see how the app code uses the pushNotificationActionPerformed listener to grab this detailsID and route the user to a specific page from this section.
Notification vs. Data Payloads
It's useful to understand a key distinction in how Firebase constructs messages. A payload can have two parts: notification and data.
notificationpayload: Contains standard keys liketitleandbody. When your app is in the background, the OS handles displaying this automatically. The content is visible in thepushNotificationReceivedlistener.datapayload: Contains only your custom key-value pairs. This gives you full control. On Android,data-only messages will be delivered to yourpushNotificationReceivedlistener even if the app is in the background (within OS-specific power limitations), allowing for background processing.
For maximum control and consistent behavior across platforms, it's a common practice to send payloads that contain only a data object, and construct the visible notification from that data within your app code or on the server. The "Your Guide to Capacitor Push Notifications" article has a great table summarizing this.
Your Guide to Capacitor Push Notifications
This section clearly explains the difference between the two payload types.
Read the section Understanding Notification Payloads to see a breakdown of the notification vs. data payloads and their primary use cases.
Tying It All Together
Let's update the useEffect hook from our previous lesson to incorporate these new listeners. This complete example sets up everything needed to both register for notifications and handle them when they arrive.
// In your App.tsx or a similar root component
import React, { useEffect } from 'react';
import { useHistory } from 'react-router-dom'; // Or your router's equivalent
import { PushNotifications, ActionPerformed, PushNotificationSchema, Token } from '@capacitor/push-notifications';
import { isPlatform } from '@capacitor/core';
const PushNotificationHandler: React.FC = () => {
const history = useHistory();
useEffect(() => {
if (isPlatform('mobile')) {
// Add all listeners
PushNotifications.addListener('registration', (token: Token) => {
console.info('Push registration success, token: ', token.value);
// TODO: Send token to your backend
});
PushNotifications.addListener('registrationError', (error: any) => {
console.error('Error on registration: ' + JSON.stringify(error));
});
PushNotifications.addListener(
'pushNotificationReceived',
(notification: PushNotificationSchema) => {
console.log('Push received: ', notification);
// Here you could display an in-app message or toast
}
);
PushNotifications.addListener(
'pushNotificationActionPerformed',
(action: ActionPerformed) => {
console.log('Push action performed: ', action);
const route = action.notification.data?.route;
if (route) {
// Navigate to the specified route
history.push(route);
}
}
);
// Register the app once listeners are set up
const register = async () => {
try {
let permStatus = await PushNotifications.checkPermissions();
if (permStatus.receive === 'prompt') {
permStatus = await PushNotifications.requestPermissions();
}
if (permStatus.receive !== 'granted') {
throw new Error('User denied permissions!');
}
await PushNotifications.register();
} catch (e) {
console.error(e);
}
};
register();
}
// Cleanup on unmount
return () => {
if (isPlatform('mobile')) {
PushNotifications.removeAllListeners();
}
};
}, [history]);
return null; // This component does not render anything
};
// You would then include <PushNotificationHandler /> in your main App component.
Testing Your Implementation
You can now test the full loop using the Firebase Console:
- Foreground Test:
- Run the app on your device (
bunx cap run iosorbunx cap run android). Keep the app open. - In the Firebase Console, go to Engage > Messaging and compose a notification.
- Send a test message to your device using its registration token.
- Check your IDE's console. You should see the log from
pushNotificationReceived. If you configuredpresentationOptions, you should also see the system notification.
- Run the app on your device (
- Background/Deep Link Test:
- Close the app or send it to the background.
- Compose another notification in Firebase. This time, in the "Additional options" section, add a custom data key-value pair, for example,
route:/profile. - Send the notification.
- Tap the notification on your device's notification tray.
- The app should open, and your
pushNotificationActionPerformedlistener should fire, logging the action and navigating to the/profileroute.
Conclusion
You have now implemented the core logic for creating a rich, interactive push notification experience. You've learned how to process incoming notifications in two distinct states and, most importantly, how to use custom data to deep-link users directly to relevant content.
Key takeaways from this lesson:
- The
pushNotificationReceivedevent fires when a notification arrives and the app is in the foreground. - The
pushNotificationActionPerformedevent fires when a user taps a notification, launching the app or bringing it to the foreground. - Use
presentationOptionsincapacitor.config.tsto make foreground notifications visible to the user. - The
notification.datapayload is the key to implementing deep linking and other custom in-app actions.
In our next lesson, we'll shift gears slightly and explore local notifications. These are notifications that you can schedule directly from your app without needing a server, which are perfect for reminders, timers, and other offline-first features.