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-deviceUsing 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 cancellationSetting 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 AMDaily 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 PMWeekly 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()returnsstatus === 'granted'setNotificationHandleris 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
DATEtrigger type - Daily repeating reminders use the
DAILYtrigger type - Cancel notifications with
cancelScheduledNotificationAsync()orcancelAllScheduledNotificationsAsync()
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.