All answers

Expo

How do I cancel or replace notifications in an Expo app?

Schedule with your own identifier, then call cancelScheduledNotificationAsync(id) to stop it before it fires, or dismissNotificationAsync(id) to clear one that's already showing. To replace instead of stacking, reuse the identifier, or send pushes with collapseId and, on Android, tag.

Updated October 31, 2026

When reminders pile up in the notification tray, or a reminder you cancelled still goes off, it’s frustrating, especially when the code looks right. Every notification in expo-notifications has an identifier, and once you choose that identifier yourself, cancelling and replacing take one call each.

How it works

There are two places a notification can be. A scheduled notification hasn’t fired yet, and cancelScheduledNotificationAsync(identifier) stops it from firing. A presented notification is already in the tray, and dismissNotificationAsync(identifier) removes it. Neither call touches the other, so for a reminder you no longer need, call both.

scheduleNotificationAsync resolves with the notification’s identifier. If you pass your own identifier in the request, that’s the one used, which means you can rebuild it later from your data instead of storing a random ID.

How to do it

  1. Create an Android channel first. On Android 8.0 and later every notification goes through a channel, and on Android 13 the permission prompt won’t appear until at least one channel exists:

    if (Platform.OS === "android") {
      await Notifications.setNotificationChannelAsync("reminders", {
        name: "Reminders",
        importance: Notifications.AndroidImportance.DEFAULT,
      });
    }

    Once a channel is created on Android, only its name and description can change, so pick the other settings carefully.

  2. Schedule with an identifier you can rebuild:

    await Notifications.scheduleNotificationAsync({
      identifier: `task-${task.id}`,
      content: { title: "Task due", body: task.title },
      trigger: {
        type: Notifications.SchedulableTriggerInputTypes.DATE,
        date: task.dueAt,
        channelId: "reminders",
      },
    });
  3. Cancel it when the task is done, both from the schedule and from the tray:

    const id = `task-${task.id}`;
    await Notifications.cancelScheduledNotificationAsync(id);
    await Notifications.dismissNotificationAsync(id);
  4. Replace it when something changes by cancelling the old one and scheduling again with the same identifier. On iOS, Apple documents that a new request with a matching identifier replaces the pending or delivered one, and on Android the identifier is used as the notification’s tag, so a new one with the same identifier takes the old one’s place in the tray. We’d still cancel first, because it makes the intent obvious and covers the case where the old one hasn’t fired yet.

  5. Clear everything on sign out with cancelAllScheduledNotificationsAsync() and dismissAllNotificationsAsync(), so the next person on the phone doesn’t get someone else’s reminders.

Collapsing push notifications

The cancel functions only manage notifications your app scheduled on the device. For notifications sent through Expo’s push service, you set the replacing behavior in the message itself. collapseId replaces an earlier notification on iOS, but on Android it only drops older messages that haven’t been delivered yet, so add tag to replace one that’s already showing on Android:

{
  "to": "ExponentPushToken[xxxxxxxxxxxxxxxxxxxxxx]",
  "title": "Order 1042",
  "body": "Out for delivery",
  "collapseId": "order-1042",
  "tag": "order-1042",
  "channelId": "orders"
}

If you only want related messages grouped on iOS, not replaced, use threadId. And if you’re testing push in Expo Go on Android, it won’t work there from SDK 53 on. You don’t need to eject, though, because a development build is enough, and local notifications still work in Expo Go.

How you know it works

If you schedule a notification for a minute from now and cancel it, nothing arrives, and getAllScheduledNotificationsAsync() no longer lists it. Scheduling twice with the same identifier leaves one notification, not two.

If it still shows up

Log the result of getAllScheduledNotificationsAsync() and compare each identifier with the one you’re cancelling, since a typo or a number where you expected a string is enough to miss it. If the notification came from a push, find it with getPresentedNotificationsAsync() and dismiss it by the identifier you see there. And if Android pushes never appear at all, check that the channelId you send matches a channel your app has already created, because a push sent to a channel that doesn’t exist on the device isn’t shown.

Ask the agent

If your app is in Draftbit, paste this into the agent, and it can read your code and make the change for you.

Give each reminder notification its own ID based on the task, cancel it when the task is completed or deleted, and replace the existing notification when the due date changes instead of adding a second one. Set up an Android notification channel for reminders too.

Open Draftbit

More Expo answers