Notifee vs Expo Notifications vs OneSignal: React Native Push 2026
Notifee vs Expo Notifications vs OneSignal: React Native Push 2026
TL;DR
Push notifications in React Native span two distinct use cases: local notifications (scheduled by the app itself, no server) and remote push (FCM/APNs via a server). The right choice depends heavily on which you need. Notifee is the richest local notification library — deep Android channel support, notification grouping, full-screen intents, categories, and triggers; it doesn't send remote push, it renders them beautifully. Expo Notifications covers both local and remote push in one package with an Expo-managed Push Notification Service (ExpoPushToken) that abstracts FCM and APNs behind one API; easiest integration for Expo apps. OneSignal is the full push platform — a SaaS service that handles FCM/APNs registration, a dashboard for targeting and segmentation, A/B testing, analytics, and in-app messaging; it's the push equivalent of Braze/Klaviyo for mobile. For rich local notifications with Android-specific features: Notifee. For Expo apps needing remote push with minimal setup: Expo Notifications. For a full push marketing and engagement platform: OneSignal.
Key Takeaways
- Notifee has no remote push — it's a local notification renderer, pair with FCM for remote
- Expo Push Notification Service — one token works for both iOS (APNs) and Android (FCM)
- OneSignal has a free tier — unlimited notifications, up to 10,000 subscribers free
- Notifee supports Android notification channels — required for Android 8+ notification categories
- Expo Notifications works with bare RN — not just Expo managed workflow
- OneSignal provides user segmentation — target by country, session count, custom tags
- All three require Firebase project — FCM setup needed for Android remote push
Push Notification Types
Local notifications (no server):
- Scheduled reminders ("Take your medication at 9am")
- In-app alerts (download complete, upload done)
- Recurring notifications (daily check-in)
Tools: Notifee, Expo Notifications
Remote push (via server → FCM/APNs → device):
- Server-triggered events (new message, order shipped)
- Marketing campaigns (promotion, re-engagement)
- Real-time alerts (price drops, breaking news)
Tools: Expo Notifications, OneSignal, direct FCM/APNs
Rich notifications:
- Large images, action buttons, progress bars
- Android: channels, groups, full-screen intents
- iOS: provisional auth, critical alerts, notification service extensions
Tools: Notifee (best), Expo Notifications (basic), OneSignal (platform-native)
Notifee: Rich Local Notifications
Notifee provides the most feature-complete local notification API for React Native, with deep Android channel support and iOS critical alert capabilities. It works alongside FCM — FCM delivers the data, Notifee renders it.
Installation
npm install @notifee/react-native
npx pod-install # iOS
Android Channel Setup (Required for Android 8+)
import notifee, { AndroidImportance } from "@notifee/react-native";
// Create notification channels (Android 8+ requirement)
// Call on app startup before displaying any notifications
async function createNotificationChannels() {
// Default channel
await notifee.createChannel({
id: "default",
name: "General Notifications",
importance: AndroidImportance.HIGH,
sound: "default",
vibration: true,
vibrationPattern: [300, 500],
});
// Silent channel (no sound/vibration)
await notifee.createChannel({
id: "silent",
name: "Silent Notifications",
importance: AndroidImportance.LOW,
sound: undefined,
vibration: false,
});
// High-priority alerts
await notifee.createChannel({
id: "urgent",
name: "Urgent Alerts",
importance: AndroidImportance.HIGH,
sound: "default",
bypassDnd: true,
});
}
Display a Notification
import notifee, { AndroidStyle } from "@notifee/react-native";
// Basic notification
async function showNotification(title: string, body: string) {
await notifee.requestPermission();
await notifee.displayNotification({
title,
body,
android: {
channelId: "default",
smallIcon: "ic_notification", // Must exist in android/app/src/main/res/drawable
pressAction: { id: "default" },
},
ios: {
sound: "default",
badgeCount: 1,
attachments: [],
},
});
}
// Rich notification with image
async function showRichNotification() {
await notifee.displayNotification({
title: "Order Shipped",
body: "Your order #12345 is on its way!",
android: {
channelId: "default",
style: {
type: AndroidStyle.BIGPICTURE,
picture: "https://example.com/package.jpg",
largeIcon: "https://example.com/store-logo.jpg",
},
actions: [
{
title: "Track Order",
pressAction: { id: "track-order" },
},
{
title: "View Order",
pressAction: { id: "view-order" },
},
],
},
});
}
// Notification with inline reply (Android)
async function showReplyNotification(messageId: string) {
await notifee.displayNotification({
title: "New Message from Alice",
body: "Hey, are you coming to the meeting?",
android: {
channelId: "default",
actions: [
{
title: "Reply",
pressAction: { id: "reply" },
input: {
allowFreeFormInput: true,
placeholder: "Type a reply...",
},
},
],
},
data: { messageId },
});
}
Scheduled (Trigger) Notifications
import notifee, { TriggerType, TimestampTrigger, IntervalTrigger } from "@notifee/react-native";
// Schedule a one-time notification
async function scheduleReminder(date: Date, message: string) {
const trigger: TimestampTrigger = {
type: TriggerType.TIMESTAMP,
timestamp: date.getTime(),
alarmManager: {
allowWhileIdle: true, // Fire even in Doze mode
},
};
await notifee.createTriggerNotification(
{
title: "Reminder",
body: message,
android: { channelId: "default" },
},
trigger
);
}
// Recurring notification every 30 minutes
async function scheduleRecurring() {
const trigger: IntervalTrigger = {
type: TriggerType.INTERVAL,
interval: 30,
timeUnit: "minutes",
};
await notifee.createTriggerNotification(
{
title: "Check In",
body: "Time to log your progress",
android: { channelId: "default" },
},
trigger
);
}
Handle Events (Press, Dismiss, Action)
import notifee, { EventType } from "@notifee/react-native";
import { useEffect } from "react";
// Foreground event handler (in component)
function useNotificationEvents() {
useEffect(() => {
const unsubscribe = notifee.onForegroundEvent(({ type, detail }) => {
switch (type) {
case EventType.PRESS:
console.log("Notification pressed:", detail.notification);
navigateToContent(detail.notification?.data);
break;
case EventType.ACTION_PRESS:
console.log("Action pressed:", detail.pressAction?.id);
handleNotificationAction(detail.pressAction?.id, detail.input);
break;
case EventType.DISMISSED:
console.log("Notification dismissed");
break;
}
});
return unsubscribe;
}, []);
}
// Background event handler (must be registered outside React)
// index.js
notifee.onBackgroundEvent(async ({ type, detail }) => {
if (type === EventType.ACTION_PRESS && detail.pressAction?.id === "reply") {
// Handle inline reply
await sendReply(detail.notification?.data?.messageId, detail.input?.text);
await notifee.cancelNotification(detail.notification!.id!);
}
});
FCM + Notifee Integration
import messaging from "@react-native-firebase/messaging";
import notifee from "@notifee/react-native";
// Register FCM token
async function setupFCM() {
const token = await messaging().getToken();
await sendTokenToServer(token);
}
// When FCM message arrives in background → display via Notifee
messaging().setBackgroundMessageHandler(async (remoteMessage) => {
await notifee.displayNotification({
title: remoteMessage.notification?.title ?? "",
body: remoteMessage.notification?.body ?? "",
data: remoteMessage.data,
android: {
channelId: "default",
smallIcon: "ic_notification",
},
});
});
Expo Notifications: Unified Push (Local + Remote)
Expo Notifications provides a single API for both local and remote push, with the Expo Push Notification Service (EPNS) abstracting FCM and APNs.
Installation
expo install expo-notifications expo-device expo-constants
Request Permissions and Get Push Token
import * as Notifications from "expo-notifications";
import * as Device from "expo-device";
import Constants from "expo-constants";
// Configure notification behavior
Notifications.setNotificationHandler({
handleNotification: async () => ({
shouldShowAlert: true,
shouldPlaySound: true,
shouldSetBadge: true,
}),
});
// Get Expo Push Token (works for both iOS APNs and Android FCM)
async function registerForPushNotifications(): Promise<string | null> {
if (!Device.isDevice) {
console.warn("Push notifications only work on real devices");
return null;
}
const { status: existingStatus } = await Notifications.getPermissionsAsync();
let finalStatus = existingStatus;
if (existingStatus !== "granted") {
const { status } = await Notifications.requestPermissionsAsync();
finalStatus = status;
}
if (finalStatus !== "granted") return null;
// Get Expo push token
const token = await Notifications.getExpoPushTokenAsync({
projectId: Constants.expoConfig?.extra?.eas?.projectId,
});
return token.data; // "ExponentPushToken[xxxxxx]"
}
Schedule Local Notification
import * as Notifications from "expo-notifications";
// Schedule a notification
async function scheduleLocalNotification(date: Date) {
const id = await Notifications.scheduleNotificationAsync({
content: {
title: "Time to exercise!",
body: "Your daily workout reminder",
sound: true,
badge: 1,
data: { screen: "workout" },
},
trigger: {
date,
},
});
return id; // Notification ID for cancellation
}
// Cancel a scheduled notification
await Notifications.cancelScheduledNotificationAsync(notificationId);
// Cancel all scheduled notifications
await Notifications.cancelAllScheduledNotificationsAsync();
Handle Notifications in React
import * as Notifications from "expo-notifications";
import { useEffect, useRef } from "react";
import { useRouter } from "expo-router";
export function useNotifications() {
const notificationListener = useRef<Notifications.Subscription>();
const responseListener = useRef<Notifications.Subscription>();
const router = useRouter();
useEffect(() => {
registerForPushNotifications().then((token) => {
if (token) sendTokenToServer(token);
});
// Received while app is open
notificationListener.current = Notifications.addNotificationReceivedListener((notification) => {
console.log("Received:", notification.request.content);
});
// User tapped the notification
responseListener.current = Notifications.addNotificationResponseReceivedListener((response) => {
const data = response.notification.request.content.data;
if (data?.screen) router.push(`/${data.screen}`);
});
return () => {
notificationListener.current?.remove();
responseListener.current?.remove();
};
}, []);
}
Send Remote Push via Expo Push API
// Server-side: send push via Expo's API (no FCM/APNs credentials needed)
async function sendExpoPush(expoPushToken: string, title: string, body: string) {
const message = {
to: expoPushToken,
sound: "default",
title,
body,
data: { screen: "notifications" },
badge: 1,
};
const response = await fetch("https://exp.host/--/api/v2/push/send", {
method: "POST",
headers: {
Accept: "application/json",
"Accept-encoding": "gzip, deflate",
"Content-Type": "application/json",
},
body: JSON.stringify(message),
});
const result = await response.json();
if (result.data?.status === "error") {
console.error("Push error:", result.data.details);
}
}
OneSignal: Full Push Marketing Platform
OneSignal is a SaaS platform with a dashboard for managing push campaigns, segmentation, A/B testing, and analytics — beyond what a library provides.
Installation
npm install react-native-onesignal
npx pod-install # iOS
Setup
// App.tsx
import OneSignal from "react-native-onesignal";
export function initOneSignal() {
OneSignal.initialize(process.env.EXPO_PUBLIC_ONESIGNAL_APP_ID!);
// Request push notification permission
OneSignal.Notifications.requestPermission(true);
// Get OneSignal Player ID (device subscription ID)
const deviceState = OneSignal.User.pushSubscription;
console.log("OneSignal token:", deviceState.token);
}
Identify Users and Set Tags
import OneSignal from "react-native-onesignal";
// Link OneSignal to your app's user
function identifyUser(userId: string, email: string) {
OneSignal.login(userId); // Associate subscription with external user ID
OneSignal.User.addEmail(email);
}
// Set tags for segmentation — used in OneSignal dashboard
function setUserTags(user: User) {
OneSignal.User.addTags({
plan: user.plan, // "free" | "pro" | "enterprise"
last_active: user.lastActiveAt, // ISO date string
sessions_count: String(user.sessions),
country: user.country,
ab_group: user.abTestGroup, // For A/B testing
});
}
Handle Notification Clicks
import OneSignal from "react-native-onesignal";
import { useEffect } from "react";
function useOneSignalEvents() {
useEffect(() => {
// Notification clicked
const clickListener = OneSignal.Notifications.addEventListener("click", (event) => {
const notificationData = event.notification.additionalData;
if (notificationData?.screen) {
router.push(`/${notificationData.screen}`);
}
});
// Notification received in foreground
const foregroundListener = OneSignal.Notifications.addEventListener(
"foregroundWillDisplay",
(event) => {
// Modify or suppress notification
event.getNotification().display();
// Or: event.preventDefault(); to suppress
}
);
return () => {
clickListener.remove();
foregroundListener.remove();
};
}, []);
}
Server-Side: Send Targeted Push
// OneSignal REST API — send from server
async function sendPushToSegment(segment: string, title: string, body: string) {
const response = await fetch("https://onesignal.com/api/v1/notifications", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Key ${process.env.ONESIGNAL_REST_API_KEY!}`,
},
body: JSON.stringify({
app_id: process.env.ONESIGNAL_APP_ID!,
included_segments: [segment], // "All" | "Subscribed Users" | custom segment
headings: { en: title },
contents: { en: body },
data: { screen: "promotions" },
ios_badgeType: "Increase",
ios_badgeCount: 1,
}),
});
return response.json();
}
// Send to specific users by external_id
async function sendPushToUser(userId: string, title: string, body: string) {
const response = await fetch("https://onesignal.com/api/v1/notifications", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Key ${process.env.ONESIGNAL_REST_API_KEY!}`,
},
body: JSON.stringify({
app_id: process.env.ONESIGNAL_APP_ID!,
include_aliases: { external_id: [userId] },
target_channel: "push",
headings: { en: title },
contents: { en: body },
}),
});
return response.json();
}
Feature Comparison
| Feature | Notifee | Expo Notifications | OneSignal |
|---|---|---|---|
| Local notifications | ✅ Best | ✅ | ✅ |
| Remote push | ❌ | ✅ (via EPNS) | ✅ |
| Android channels | ✅ Full control | ✅ Basic | ✅ |
| Notification grouping | ✅ | ❌ | ✅ |
| Rich media (images) | ✅ | ✅ Basic | ✅ |
| Inline reply | ✅ | ❌ | ✅ |
| Full-screen intent | ✅ | ❌ | ❌ |
| Dashboard/analytics | ❌ | ❌ | ✅ |
| User segmentation | ❌ | ❌ | ✅ |
| A/B testing | ❌ | ❌ | ✅ |
| In-app messages | ❌ | ❌ | ✅ |
| Expo compatible | ✅ (bare) | ✅ | ✅ |
| Free tier | Free (OSS) | Free | 10k subscribers |
When to Use Each
Choose Notifee if:
- Rich local notifications with Android channel-level control are required
- Notification grouping, inline reply, and big picture style are needed
- You already handle remote push via FCM (
@react-native-firebase/messaging) and need a better renderer - Full-screen intents for calls, alarms, or timers on Android
Choose Expo Notifications if:
- You're on Expo (managed or bare) and want both local and remote push in one package
- Expo Push Notification Service simplifies FCM/APNs credential management
- Good-enough feature set without adding Notifee complexity
- You want typed TypeScript APIs with Expo's ecosystem integration
Choose OneSignal if:
- Marketing team needs a dashboard to send campaigns without developer involvement
- User segmentation by behavior, location, or custom properties is required
- Push analytics, delivery rates, and conversion tracking matter
- In-app messages (banners, modals) in addition to push notifications
- A/B testing notification copy and timing
Methodology
Data sourced from official Notifee documentation (notifee.app), Expo Notifications documentation (docs.expo.dev/push-notifications), OneSignal documentation (documentation.onesignal.com), GitHub star counts as of February 2026, npm download statistics, pricing pages as of February 2026, and community discussions from the React Native community Discord and r/reactnative.
Related: Expo EAS vs Fastlane vs Bitrise for building and shipping the apps that use these push notification SDKs, or RevenueCat vs Adapty vs Superwall for mobile monetization alongside push engagement.