Add Recurring Reminders to an Expo App
Use a repeating local notification for a reminder on this phone. Keep the notification identifier so editing or disabling the reminder cancels the previous request instead of creating duplicates.
Updated 28 October 2026
What you will build
Enable and Disable controls for a daily 9:00 reminder. The identifier survives an app restart, and enabling again replaces the previous request. This is a device-local reminder, not a server message to every device signed into an account.
- Step 1Ask when enabled
- Step 2Schedule one request
- Step 3Cancel when disabled
The example uses the SDK 58 delivery option bestEffort on purpose. Ordinary reminders may be delayed by system settings and power management. Exact Android alarms are a separate permission and policy decision; a scheduled request is not a delivery guarantee.
Build it in Newly
Open your project in Newly and describe the behavior you want. Use this suggested prompt as a starting point, then review the generated changes and follow the checks below.
Add a daily reminder setting using expo-notifications. Ask for notification permission when the user enables it, create an Android notification channel, and schedule one repeating daily request. Persist the returned identifier, cancel the previous request when the time changes, and let the user disable it after reopening the app. Use best-effort delivery for this ordinary reminder; do not silently request exact-alarm access or promise delivery to the second. Show a clear denied-permission state and keep the setting consistent with pending notification requests.1. Configure the native app and ask at the right moment
Install the package and add expo-notifications to the app config plugins. Rebuild the native app when its configuration changes. Put foreground notification handling in one app-level location. On Android, create the channel before asking for permission; ask when the person enables a reminder, rather than during unrelated onboarding.
2. Store and replace the scheduled identifier
Use a daily trigger for a wall-clock time. Save the identifier returned by scheduleNotificationAsync in persistent app storage. When the person changes the setting, cancel the old request and schedule its replacement. Prevent overlapping button actions so repeated taps cannot create multiple reminders.
3. Make disable and delivery expectations explicit
Disable should cancel the saved identifier and clear it from storage. Inspect getAllScheduledNotificationsAsync when diagnosing mismatches. Test notification behavior while the app is foregrounded, backgrounded and closed. For account-wide reminders or changes made by another person, design a server synchronization flow separately.
Expo implementation example
Install the dependencies in your Expo project. Expo resolves native package versions for your installed SDK.
npx expo install expo-notifications @react-native-async-storage/async-storageimport { useRef, useState } from "react";
import { Button, Platform, Text, View } from "react-native";
import AsyncStorage from "@react-native-async-storage/async-storage";
import * as Notifications from "expo-notifications";
const KEY = "daily-reminder-id";
const CHANNEL = "daily-reminders";
// Merge this with your app-level handler if one already exists.
Notifications.setNotificationHandler({
handleNotification: async () => ({
shouldPlaySound: true, shouldSetBadge: false,
shouldShowBanner: true, shouldShowList: true,
}),
});
function mayNotify(permission: Notifications.NotificationPermissionsStatus) {
return permission.granted ||
permission.ios?.status === Notifications.IosAuthorizationStatus.PROVISIONAL ||
permission.ios?.status === Notifications.IosAuthorizationStatus.EPHEMERAL;
}
export default function DailyReminder() {
const [busy, setBusy] = useState(false);
const [status, setStatus] = useState("");
const running = useRef(false);
async function update(enabled: boolean) {
if (running.current) return;
running.current = true;
setBusy(true);
try {
if (enabled) {
if (Platform.OS === "android") {
await Notifications.setNotificationChannelAsync(CHANNEL, {
name: "Daily reminders",
importance: Notifications.AndroidImportance.DEFAULT,
});
}
let permission = await Notifications.getPermissionsAsync();
if (!mayNotify(permission)) permission = await Notifications.requestPermissionsAsync();
if (!mayNotify(permission)) {
setStatus("Notifications are not allowed. Review this app in system settings.");
return;
}
}
const previous = await AsyncStorage.getItem(KEY);
if (previous) await Notifications.cancelScheduledNotificationAsync(previous);
await AsyncStorage.removeItem(KEY);
if (enabled) {
const id = await Notifications.scheduleNotificationAsync({
content: { title: "Daily check-in", body: "Take a moment to check in.", sound: "default" },
trigger: {
type: Notifications.SchedulableTriggerInputTypes.DAILY,
hour: 9, minute: 0, channelId: CHANNEL, delivery: "bestEffort",
},
});
try {
await AsyncStorage.setItem(KEY, id);
} catch (error) {
await Notifications.cancelScheduledNotificationAsync(id);
throw error;
}
}
setStatus(enabled ? "Daily reminder scheduled. Delivery follows system settings." : "Reminder disabled.");
} catch {
setStatus("Could not update the reminder. Please try again.");
} finally {
running.current = false;
setBusy(false);
}
}
return (
<View style={{ gap: 16, padding: 24 }}>
<Button title="Enable daily reminder" disabled={busy} onPress={() => update(true)} />
<Button title="Disable reminder" disabled={busy} onPress={() => update(false)} />
<Text accessibilityLiveRegion="polite">{status}</Text>
</View>
);
}Add the expo-notifications config plugin and use a native build when testing your own app configuration. This example requests best-effort delivery using the SDK 58 API; check compatibility before copying it into an older project. It intentionally does not request exact-alarm permission.
Test before publishing
- On a fresh install, enable the reminder and test both accepting and denying notification permission.
- Enable twice and inspect pending requests; only one request for this reminder remains.
- Restart the app, disable the reminder and confirm that its pending request is gone.
- For a quick test, schedule a separate one-off notification a minute ahead, verify delivery and then cancel or remove that test request.
- Test foreground, background, notification settings and a time-zone change on physical devices; do not promise exact delivery from a simulator result.
Run these checks in your own project on iOS and Android. A browser preview does not establish native permission, hardware or background behavior.
Common problems
The reminder duplicates after editing
Persist its identifier and cancel that request before replacing it. Keep enable, edit and disable actions serialized; a disabled button alone can still leave overlapping asynchronous work if other code updates the same setting.
The reminder does not alert at the expected minute
Check system permission, the Android channel, Focus or notification settings, pending requests and the chosen delivery mode. Exact alarms on recent Android versions require separate access and must fit the product and store policies.
Questions about recurring reminders
Do recurring local reminders need a server?
A reminder scheduled on this device does not need your server to send each occurrence. Cross-device settings, messages triggered by another person and shared account state require a separate synchronization or server-push design.
Should I schedule a year of individual notifications?
Prefer one repeating request when its recurrence matches the product. Keep the identifier for editing and cancellation. Inspect pending requests and test your target operating systems rather than treating a limit quoted from a deprecated API as a universal modern guarantee.
Does a daily reminder require exact-alarm permission on Android?
SDK 58 distinguishes best-effort delivery from exact delivery. The example chooses bestEffort. Exact delivery on Android 12 and later involves separate alarm access, and recent Android versions may deny it by default. Request it only when the feature and applicable policies justify it.
What does provisional permission mean on iOS?
Provisional authorization can allow noninterruptive notifications. It does not mean the app can guarantee a sound or banner. Respect the user’s current settings and describe the reminder as scheduled rather than promising a loud alert.