◉RORK LABJP
●GPT-6.1 SOL — Added to Rork's model menu (09-29) with Low, Medium and High effort; included on Pro and Max●SONNET 5.5 — Claude Sonnet 5.5 is selectable: strong at UI design and faster than Sonnet 5 (09-28)●SDK 58 — Beta since 09/15 with no stable date yet; running projects in Expo Go has required login since 09/03●WIDGET — A write-up traces an iOS widget that renders black only in production builds●OBSERVE — EAS Observe now records native crashes too (SDK 57.0.21 or later, 10/07)●NEW — My App Open Ad Only Showed When the App…●GPT-6.1 SOL — Added to Rork's model menu (09-29) with Low, Medium and High effort; included on Pro and Max●SONNET 5.5 — Claude Sonnet 5.5 is selectable: strong at UI design and faster than Sonnet 5 (09-28)●SDK 58 — Beta since 09/15 with no stable date yet; running projects in Expo Go has required login since 09/03●WIDGET — A write-up traces an iOS widget that renders black only in production builds●OBSERVE — EAS Observe now records native crashes too (SDK 57.0.21 or later, 10/07)●NEW — My App Open Ad Only Showed When the App…
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 Native239Rork585reminders

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-09-04
EAS secret visibility does not keep a value out of your app — deciding prefix and visibility separately
The EXPO_PUBLIC_ prefix decides what ships inside your app; EAS visibility decides who can read it. Why stacking them blanks a value on OTA updates, and how to check your build.
⬡ Dev Tools2026-08-22
Every bulk replace exited zero. The damage was in the lines I did not delete
Run a bulk replace over generated code and the breakage lands on the neighbouring lines, not the matched ones. Here is what broke in a live project, and a dependency-free guard that checks the invariants a replace must preserve.
⬡ Dev Tools2026-08-14
Find the native edits expo prebuild will erase before you upgrade to SDK 57
Expo SDK 57 makes expo prebuild clear and regenerate ios and android by default. Here is how to audit your hand edits first, move them into config plugins, and why 57.0.9 matters for Reanimated apps.