RORK LABJP
TOOLING — Rork's developer repos keep moving: rork-xcode was updated on July 16, rork-device on July 15, and rork-plist on July 13OPUS46 — Claude Opus 4.6 is live in Rork, and Rork Max is built to assemble apps on top of Claude CodeSIM — A cloud iOS simulator runs in the browser, with one click to install on a device and two clicks to publish to the App StoreMAX — Rork Max emits pure Swift rather than React Native, reaching iPhone, iPad, Apple Watch, Apple TV, Vision Pro, and even iMessageNATIVE — That opens up HealthKit, ARKit and LiDAR, NFC, Dynamic Island, Live Activities, 3D through Metal, and on-device inference with Core MLSEED — Rork raised a $15M seed led by Left Lane Capital, with Peak XV and a16z Speedrun joining the roundTOOLING — Rork's developer repos keep moving: rork-xcode was updated on July 16, rork-device on July 15, and rork-plist on July 13OPUS46 — Claude Opus 4.6 is live in Rork, and Rork Max is built to assemble apps on top of Claude CodeSIM — A cloud iOS simulator runs in the browser, with one click to install on a device and two clicks to publish to the App StoreMAX — Rork Max emits pure Swift rather than React Native, reaching iPhone, iPad, Apple Watch, Apple TV, Vision Pro, and even iMessageNATIVE — That opens up HealthKit, ARKit and LiDAR, NFC, Dynamic Island, Live Activities, 3D through Metal, and on-device inference with Core MLSEED — Rork raised a $15M seed led by Left Lane Capital, with Peak XV and a16z Speedrun joining the round
Articles/Dev Tools
Dev Tools/2026-04-05Beginner

Rork × expo-notifications — Local Notifications That Fire When You Expect Them To

A complete guide to implementing local notifications in Rork using expo-notifications. Learn how to schedule one-time and repeating reminders with clear code examples.

expo-notifications7local notificationsscheduled notificationsReact Native209Rork515reminders

Local Notifications vs. Push Notifications: What's the Difference?

Before adding notifications to your Rork app, it's worth understanding the key distinction between the two main types.

Local notifications are scheduled directly on the device by the app itself. No server required — they work even without an internet connection. They're perfect for habit reminders, timer completions, alarms, and anything that "lives" on the device.

Push notifications, on the other hand, are delivered through Apple's APNs or Google's FCM from an external server. They're ideal for real-time events like new messages, promotions, or activity feeds.

For most Rork apps, starting with local notifications is the practical choice. You can implement everything with a single library — expo-notifications — without configuring any backend.

In this guide, you'll learn how to implement:

  • Requesting notification permissions
  • Sending immediate notifications
  • Scheduling notifications for a specific date and time
  • Setting up daily recurring reminders
  • Managing and canceling scheduled notifications

Setting Up expo-notifications

Installing the Package

In your Rork project, ask the AI assistant to add the package, or install it directly:

# Use expo install to ensure version compatibility
npx expo install expo-notifications expo-device

Using expo install instead of npm install ensures that the version of expo-notifications is compatible with your current Expo SDK.

Configuring app.json

Add the notifications plugin to your app.json (or app.config.js):

{
  "expo": {
    "plugins": [
      [
        "expo-notifications",
        {
          "icon": "./assets/notification-icon.png",
          "color": "#ffffff",
          "sounds": ["./assets/notification.wav"]
        }
      ]
    ]
  }
}

The icon and sounds fields are optional. Adding a branded notification icon gives your app a more polished feel.


Requesting Notification Permissions

iOS requires explicit user permission before you can send any notifications. Android 13+ follows the same requirement. Here's a reliable permission flow:

import * as Notifications from 'expo-notifications';
import * as Device from 'expo-device';
import { Platform } from 'react-native';
 
// Define how notifications behave when the app is in the foreground
Notifications.setNotificationHandler({
  handleNotification: async () => ({
    shouldShowAlert: true,   // Show the banner
    shouldPlaySound: true,   // Play the sound
    shouldSetBadge: false,   // Don't update the badge count
  }),
});
 
export async function requestNotificationPermission(): Promise<boolean> {
  // Notifications don't work in simulators — use a real device
  if (!Device.isDevice) {
    console.warn('Please test on a physical device');
    return false;
  }
 
  const { status: existingStatus } = await Notifications.getPermissionsAsync();
  let finalStatus = existingStatus;
 
  // Ask the user if we haven't asked yet
  if (existingStatus !== 'granted') {
    const { status } = await Notifications.requestPermissionsAsync();
    finalStatus = status;
  }
 
  if (finalStatus !== 'granted') {
    console.warn('Notification permission was not granted');
    return false;
  }
 
  // Set up a notification channel on Android
  if (Platform.OS === 'android') {
    await Notifications.setNotificationChannelAsync('default', {
      name: 'Default',
      importance: Notifications.AndroidImportance.MAX,
      vibrationPattern: [0, 250, 250, 250],
      lightColor: '#FF231F7C',
    });
  }
 
  return true;
}

Important: requestPermissionsAsync() can only show the system permission dialog once. If the user denies it, subsequent calls won't show the dialog — you'll need to guide them to the Settings app instead.


Sending an Immediate Notification

Once you have permission, the simplest notification to send is an immediate one:

import * as Notifications from 'expo-notifications';
 
export async function sendImmediateNotification(
  title: string,
  body: string,
  data?: Record<string, unknown>
): Promise<string> {
  const notificationId = await Notifications.scheduleNotificationAsync({
    content: {
      title,
      body,
      data: data ?? {},
      sound: true,
    },
    trigger: null, // null = fire immediately
  });
 
  console.log('Notification ID:', notificationId);
  return notificationId;
}
 
// Example usage
await sendImmediateNotification(
  'Task Complete! 🎉',
  'You finished your daily walk.',
  { taskId: 'walk-20260405' }
);
// Expected output: Notification appears immediately; ID is returned for future cancellation

Setting trigger: null fires the notification right away. The setNotificationHandler configuration we set up earlier ensures it displays even when the app is in the foreground.


Scheduling Notifications

One-Time Scheduled Notification

Use this when you need to notify the user at a specific future date and time — like a meeting reminder or an event countdown:

import * as Notifications from 'expo-notifications';
 
export async function scheduleNotificationAt(
  title: string,
  body: string,
  date: Date
): Promise<string> {
  const notificationId = await Notifications.scheduleNotificationAsync({
    content: {
      title,
      body,
      sound: true,
    },
    trigger: {
      type: Notifications.SchedulableTriggerInputTypes.DATE,
      date, // Pass a JavaScript Date object directly
    },
  });
 
  return notificationId;
}
 
// Example: Notify tomorrow at 9:00 AM
const tomorrow9am = new Date();
tomorrow9am.setDate(tomorrow9am.getDate() + 1);
tomorrow9am.setHours(9, 0, 0, 0);
 
const id = await scheduleNotificationAt(
  'Morning Check-in',
  'Time to review your tasks for today!',
  tomorrow9am
);
// Expected output: Notification ID returned; notification fires tomorrow at 9 AM

Daily Repeating Reminder

Perfect for habit trackers, hydration reminders, or meditation apps:

import * as Notifications from 'expo-notifications';
 
export async function scheduleDailyReminder(
  title: string,
  body: string,
  hour: number,
  minute: number
): Promise<string> {
  const notificationId = await Notifications.scheduleNotificationAsync({
    content: {
      title,
      body,
      sound: true,
    },
    trigger: {
      type: Notifications.SchedulableTriggerInputTypes.DAILY,
      hour,    // 0–23
      minute,  // 0–59
    },
  });
 
  return notificationId;
}
 
// Example: Daily water reminder at 8:00 PM
const reminderId = await scheduleDailyReminder(
  '💧 Hydration Check',
  'Have you had enough water today?',
  20, // 8 PM
  0
);
 
console.log('Daily reminder set:', reminderId);
// Expected output: Notification fires every day at 8:00 PM

Weekly Reminder

Useful for weekly reviews, gym days, or recurring appointments:

export async function scheduleWeeklyReminder(
  title: string,
  body: string,
  weekday: 1 | 2 | 3 | 4 | 5 | 6 | 7, // 1=Sunday, 2=Monday...7=Saturday
  hour: number,
  minute: number
): Promise<string> {
  const notificationId = await Notifications.scheduleNotificationAsync({
    content: { title, body, sound: true },
    trigger: {
      type: Notifications.SchedulableTriggerInputTypes.WEEKLY,
      weekday,
      hour,
      minute,
    },
  });
  return notificationId;
}
 
// Example: Weekly review every Monday at 9 AM
await scheduleWeeklyReminder(
  '📋 Weekly Review Time',
  'Let\'s reflect on last week and plan ahead.',
  2, // Monday
  9,
  0
);

Managing Notifications

Listing All Scheduled Notifications

import * as Notifications from 'expo-notifications';
 
export async function getScheduledNotifications() {
  const notifications = await Notifications.getAllScheduledNotificationsAsync();
 
  notifications.forEach(notification => {
    console.log({
      id: notification.identifier,
      title: notification.content.title,
      trigger: notification.trigger,
    });
  });
 
  return notifications;
}

Canceling Notifications

// Cancel a specific notification
export async function cancelNotification(notificationId: string): Promise<void> {
  await Notifications.cancelScheduledNotificationAsync(notificationId);
  console.log(`Canceled notification: ${notificationId}`);
}
 
// Cancel all scheduled notifications
export async function cancelAllNotifications(): Promise<void> {
  await Notifications.cancelAllScheduledNotificationsAsync();
  console.log('All notifications canceled');
}

Handling Notification Taps

When a user taps a notification, you can navigate them to a specific screen in your app:

import { useEffect, useRef } from 'react';
import * as Notifications from 'expo-notifications';
import { useRouter } from 'expo-router';
 
export function useNotificationHandler() {
  const router = useRouter();
  const responseListener = useRef<Notifications.EventSubscription>();
 
  useEffect(() => {
    responseListener.current = Notifications.addNotificationResponseReceivedListener(
      response => {
        const data = response.notification.request.content.data;
 
        // Use data to navigate to the right screen
        if (data.screen) {
          router.push(data.screen as string);
        }
      }
    );
 
    return () => {
      if (responseListener.current) {
        responseListener.current.remove();
      }
    };
  }, [router]);
}

By embedding { screen: '/tasks/123' } in the notification's data field, you can deep-link users directly to the relevant content.

If you're looking to go further with server-driven push notifications and automated delivery, check out our Rork Push Notification: Advanced Segmentation & Automation Guide.


Common Errors and Fixes

Error: "Simulator is not supported"

Notifications don't work in iOS/Android simulators. Always test on a real device. With Rork, you can distribute a build via TestFlight or use Expo Go for quick testing.

Error: Notifications not showing on iOS

Starting with iOS 18, background processing restrictions are tighter. Check:

  • Settings → Notifications → Your App → "Allow Notifications" is toggled on
  • Notifications.getPermissionsAsync() returns status === 'granted'
  • setNotificationHandler is configured at the top of your app

Error: Duplicate notifications being delivered

If you reschedule a reminder without canceling the old one first, you'll end up with multiple notifications at the same time. The fix is simple:

async function updateDailyReminder(hour: number, minute: number) {
  // Cancel existing before rescheduling
  await Notifications.cancelAllScheduledNotificationsAsync();
  await scheduleDailyReminder('Reminder', 'Time!', hour, minute);
}

For persisting user notification preferences locally (so they survive app restarts), see our Rork AsyncStorage / MMKV / SQLite Local Storage Guide.


Looking back

Local notifications with expo-notifications are one of the most effective tools for driving user engagement in your Rork app. Here's a quick recap:

  • Request permissions with requestPermissionsAsync() before scheduling anything
  • Immediate notifications use trigger: null
  • One-time scheduling uses the DATE trigger type
  • Daily repeating reminders use the DAILY trigger type
  • Cancel notifications with cancelScheduledNotificationAsync() or cancelAllScheduledNotificationsAsync()

Whether you're building a habit tracker, a meditation timer, or a task management app, local notifications help keep your users engaged without requiring any server infrastructure. Once you've mastered local notifications, your next step is to explore how app performance and rendering tie into the overall user experience — take a look at the Rork App Performance Optimization Complete Guide for deeper insights.

Share

Thank You for Reading

Rork Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • Copy-paste ready implementation code
  • New advanced guides published daily
  • $5/mo or $10 for lifetime access
View Membership →

If you found this article helpful, a small tip ($1.50) would mean a lot to us. Your support helps keep this site ad-free and covers server and hosting costs.

Related Articles

Dev Tools2026-06-25
Why Your 9 AM Reminder Stops Arriving Abroad — Making Expo Local Notifications Survive Time Zones and DST
Daily reminders built with Rork (Expo) can drift to the wrong local time when users travel or DST flips. Here is the timeInterval trap, and a design that reschedules against local wall-clock time, with working code.
Dev Tools2026-06-22
Your Daily Reminder Stops Firing After a Couple of Weeks — iOS's Invisible 64-Notification Cap
When a daily reminder built with Rork (Expo) goes silent after a while, the cause is usually iOS's 64 pending-notification limit. Design a repeating calendar trigger for fixed messages and a rolling reschedule for daily-changing content, with working code that survives DST and multiple reminders.
Dev Tools2026-07-17
Shipping Notifications Without Asking First — Provisional Authorization in Rork Apps, and the Expo Snippet That Quietly Undoes It
iOS lets you start delivering notifications with no permission dialog at all, via provisional authorization. The catch: expo-notifications reports granted as false for provisional devices, so the registration snippet in Expo's own docs re-requests permission and fires the very dialog you were avoiding. Here's why granted lies, a hook that models authorization as five states, how to write notifications for quiet delivery, when to ask for the upgrade, and how to keep provisional out of your CTR.
📚RECOMMENDED BOOKS
Build a Large Language Model (From Scratch)
Sebastian Raschka
LLM Dev
Prompt Engineering for LLMs
Berryman & Ziegler
Prompting
AI Engineering
Chip Huyen
AI Eng
* Contains affiliate links
See all →