Skip to main content

Notifee vs Expo Notifications vs OneSignal: React Native Push 2026

·PkgPulse Team

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

FeatureNotifeeExpo NotificationsOneSignal
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 tierFree (OSS)Free10k 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.

Comments

Stay Updated

Get the latest package insights, npm trends, and tooling tips delivered to your inbox.