mobile-background-tasks
Background fetch, processing tasks, background location, headless JS, battery optimization - Expo and bare React Native
Install
npx skills add https://github.com/agents-inc/skills/tree/main/dist/plugins/mobile-background-tasks/skills/mobile-background-tasks
claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install agents-inc-skills@llmmart
git clone https://github.com/agents-inc/skills.git
The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole agents-inc/skills collection as a plugin from our marketplace. Git is the plain clone.
Skill manifest
React Native Background Tasks
Quick Guide: Background tasks in React Native are heavily constrained by OS power management. Use
expo-background-task(Expo) orreact-native-background-fetch(bare RN) for periodic fetch. Useexpo-locationfor background location tracking. iOS gives ~30s for refresh tasks (BGAppRefreshTask) and several minutes for processing tasks (BGProcessingTask). Android enforces 15-minute minimum intervals via WorkManager and restricts execution in Doze mode. Always callfinish()or return a result when done -- the OS will terminate tasks that exceed their time budget.
<critical_requirements>
CRITICAL: Before Using This Skill
All code must follow project conventions in CLAUDE.md (kebab-case, named exports, import ordering,
import type, named constants)
(You MUST define tasks in the top-level scope (global) -- tasks defined inside React components or lifecycle methods will NOT be registered when the app starts from the background)
(You MUST call finish(taskId) or return a BackgroundTaskResult when task execution completes -- failing to signal completion causes the OS to penalize or kill your app)
(You MUST request background permissions explicitly on both platforms -- iOS requires Info.plist UIBackgroundModes entries, Android requires manifest permissions)
(You MUST handle the OS killing your task at any time -- use expiration listeners on iOS and timeout callbacks on Android to clean up gracefully)
(You MUST keep background work minimal -- sync only changed data, avoid heavy computation, respect the ~30s iOS refresh limit)
</critical_requirements>
Auto-detection: expo-task-manager, expo-background-task, expo-background-fetch, expo-location background, react-native-background-fetch, BackgroundFetch, TaskManager, defineTask, registerTaskAsync, startLocationUpdatesAsync, Headless JS, registerHeadlessTask, BGTaskScheduler, WorkManager, background fetch, background processing, background location
When to use:
- Syncing data periodically while the app is backgrounded (new messages, feeds, email)
- Tracking location in the background (fitness, delivery, navigation)
- Running periodic cleanup or maintenance tasks (cache purge, log upload)
- Keeping local data fresh so the app opens with current content
- Processing uploads or downloads that continue after backgrounding
When NOT to use:
- Real-time updates that need sub-second latency (use push notifications + foreground handling)
- Continuous audio playback (use the audio background mode, not task scheduling)
- Tasks that must execute at an exact time (OS scheduling is advisory, not precise)
- Tasks requiring more than a few minutes of CPU (iOS will terminate them)
Key patterns covered:
- Expo background tasks:
expo-background-task(new) andexpo-background-fetch(legacy) - Bare RN background fetch:
react-native-background-fetchwith configure/scheduleTask - Background location tracking with
expo-locationand TaskManager - Android Headless JS for post-termination task execution
- iOS BGTaskScheduler constraints (refresh ~30s vs processing ~minutes)
- Android battery optimization: Doze mode, App Standby, WorkManager guarantees
- Task registration, unregistration, and lifecycle management
Detailed Resources:
- examples/core.md - Expo background task, bare RN background fetch, background location, headless JS
- reference.md - Decision frameworks, platform constraints, permission checklists
<decision_framework>
Decision Framework
Choosing a Background Task Approach
What kind of background work do you need?
|
+-> Periodic data sync (every 15min - 12hrs)?
| +-> Expo project? --> expo-background-task
| +-> Bare RN? --> react-native-background-fetch
|
+-> Continuous location tracking?
| +-> Expo? --> expo-location + startLocationUpdatesAsync
| +-> Bare RN? --> react-native-background-geolocation
|
+-> Complete a task started in foreground (iOS 26+)?
| +-> BGContinuedProcessingTask (new in iOS 26)
|
+-> Long-running processing (ML, export)?
| +-> Foreground service with notification (Android)
| +-> BGProcessingTask (iOS, requires charger + network)
|
+-> Must survive app termination (Android)?
| +-> Headless JS + enableHeadless: true + stopOnTerminate: false
|
+-> Must execute at exact time?
+-> Not possible with background tasks
+-> Use push notifications + server-side scheduling
expo-background-task vs expo-background-fetch
| Feature | expo-background-task | expo-background-fetch |
|---|---|---|
| Status | Active (recommended) | Deprecated |
| iOS API | BGTaskScheduler | Legacy Background Fetch |
| Android API | WorkManager | JobScheduler |
| Min interval | 15 minutes | ~10 minutes (advisory) |
| Network required | Yes (by default) | No |
| Reliability | Higher | Lower |
Platform Execution Limits
| Constraint | iOS | Android |
|---|---|---|
| Refresh task time | ~30 seconds | ~10 minutes |
| Processing task time | Several minutes (charger required) | ~10 minutes |
| Minimum interval | 15 minutes (system-managed) | 15 minutes (WorkManager-enforced) |
| After force-quit | No tasks run | Headless JS can run (with config) |
| After reboot | Tasks resume automatically | Requires startOnBoot: true |
| Simulator support | No (physical device only for BGTaskScheduler) | Partial (Doze may not be enforced) |
</decision_framework>
<red_flags>
RED FLAGS
High Priority Issues:
- Defining tasks inside React components or useEffect -- tasks MUST be at the top-level scope or they won't run when the app starts from background
- Not calling
finish(taskId)or returning a result -- the OS will penalize your app, reducing future scheduling frequency or killing the task - Expecting exact timing -- background task intervals are minimums, the OS may delay execution by hours or even days on iOS
- Using
setTimeout/setIntervalfor background work -- these are killed immediately when the app is backgrounded - Not requesting background permissions -- iOS requires Info.plist UIBackgroundModes, Android requires ACCESS_BACKGROUND_LOCATION and RECEIVE_BOOT_COMPLETED
Medium Priority Issues:
- Doing heavy computation in a background refresh task -- iOS gives ~30 seconds, not minutes
- Not handling the timeout/expiration callback -- if the OS decides to stop your task early, you must save progress and exit
- Assuming background location works with "When In Use" permission -- it requires "Always Allow" on iOS
- Testing only on simulators -- iOS simulators do not execute BGTaskScheduler tasks
- Not checking
getStatusAsync()before registering -- background tasks may be restricted by user settings or device state
Gotchas & Edge Cases:
- iOS force-quit kills ALL background tasks until user reopens the app -- there is no workaround
- Android vendor battery optimizations (Samsung, Xiaomi, Huawei) may kill background tasks beyond stock Android Doze restrictions -- see dontkillmyapp.com
expo-background-taskrequires network connectivity by default -- tasks won't run offline- iOS BGTaskScheduler uses machine learning to predict when to run your task -- it may take days to "settle in" for newly installed apps
- WorkManager enforces a hard 15-minute minimum interval -- you cannot schedule more frequently
- Headless JS is Android-only -- iOS has no equivalent post-termination execution
expo-background-fetchis deprecated in favor ofexpo-background-task-- migrate to the new API- Background tasks registered with expo-task-manager persist across app restarts -- always check
isTaskRegisteredAsyncbefore re-registering - Android 15/16 edge-to-edge changes do not affect background tasks, but foreground service notification requirements have tightened
- iOS 26 introduces BGContinuedProcessingTask for completing user-initiated work in the background -- a new option for tasks started in foreground
</red_flags>
<critical_reminders>
CRITICAL REMINDERS
All code must follow project conventions in CLAUDE.md
(You MUST define tasks in the top-level scope (global) -- tasks defined inside React components or lifecycle methods will NOT be registered when the app starts from the background)
(You MUST call finish(taskId) or return a BackgroundTaskResult when task execution completes -- failing to signal completion causes the OS to penalize or kill your app)
(You MUST request background permissions explicitly on both platforms -- iOS requires Info.plist UIBackgroundModes entries, Android requires manifest permissions)
(You MUST handle the OS killing your task at any time -- use expiration listeners on iOS and timeout callbacks on Android to clean up gracefully)
(You MUST keep background work minimal -- sync only changed data, avoid heavy computation, respect the ~30s iOS refresh limit)
Failure to follow these rules will result in tasks that never execute, apps penalized by the OS scheduler, or apps rejected from the App Store for excessive background resource usage.
</critical_reminders>
Files (skills)
-
examples
-
core.md 12.6 KB
# Background Tasks - Core Patterns > Complete implementations for background fetch, location tracking, and headless tasks. See [SKILL.md](../SKILL.md) for decision guidance and red flags. **Prerequisites:** Familiarity with React Native, Expo SDK, and async JavaScript. --- ## Pattern 1: Expo Background Task (Full Lifecycle) Complete setup with registration, status checking, and unregistration. ```typescript // background-sync.ts -- top-level task definition import * as TaskManager from "expo-task-manager"; import * as BackgroundTask from "expo-background-task"; const SYNC_TASK_NAME = "BACKGROUND_SYNC_TASK"; const MIN_INTERVAL_MINUTES = 60; // 1 hour minimum // CRITICAL: Must be top-level, not inside any component TaskManager.defineTask(SYNC_TASK_NAME, async () => { try { const lastSyncTimestamp = await getLastSyncTimestamp(); const updates = await fetchChangesSince(lastSyncTimestamp); if (updates.length === 0) { return BackgroundTask.BackgroundTaskResult.Failed; } await applyUpdates(updates); await setLastSyncTimestamp(Date.now()); return BackgroundTask.BackgroundTaskResult.Success; } catch { return BackgroundTask.BackgroundTaskResult.Failed; } }); // Registration helper -- call from your app's initialization async function registerBackgroundSync(): Promise<void> { const isRegistered = await TaskManager.isTaskRegisteredAsync(SYNC_TASK_NAME); if (isRegistered) return; const status = await BackgroundTask.getStatusAsync(); if (status === BackgroundTask.BackgroundTaskStatus.Restricted) { // Background tasks not available -- user disabled or system restricted return; } await BackgroundTask.registerTaskAsync(SYNC_TASK_NAME, { minimumInterval: MIN_INTERVAL_MINUTES, }); } // Unregistration helper -- call when user disables sync async function unregisterBackgroundSync(): Promise<void> { const isRegistered = await TaskManager.isTaskRegisteredAsync(SYNC_TASK_NAME); if (!isRegistered) return; await BackgroundTask.unregisterTaskAsync(SYNC_TASK_NAME); } export { registerBackgroundSync, unregisterBackgroundSync }; ``` **Why good:** Task defined at module scope (survives headless launch), checks registration status before registering (idempotent), checks availability before attempting registration, syncs only changes since last timestamp (minimal work), returns explicit result codes, named constants for intervals ```typescript // BAD: Multiple anti-patterns in one example import { useEffect } from "react"; function App() { useEffect(() => { // BAD: Task defined inside component lifecycle TaskManager.defineTask("sync", async () => { // BAD: Fetching everything instead of deltas const allData = await fetchAllData(); await saveAllData(allData); // BAD: No return value -- OS doesn't know if task succeeded }); // BAD: No registration check -- may double-register BackgroundTask.registerTaskAsync("sync", { minimumInterval: 5, // BAD: Below 15-minute minimum, will be ignored }); }, []); } ``` **Why bad:** Task defined in useEffect will not execute when app starts headlessly in background, fetching all data wastes limited execution time, no result returned means OS cannot optimize scheduling, no registration guard causes duplicate registrations, interval below 15 minutes is silently ignored by the OS --- ## Pattern 2: Bare RN Background Fetch (Full Setup) Complete configuration with periodic tasks, one-shot tasks, and headless support. ```typescript // background-fetch-setup.ts import BackgroundFetch from "react-native-background-fetch"; const MIN_FETCH_INTERVAL_MINUTES = 15; const CACHE_CLEANUP_DELAY_MS = 5000; const CACHE_CLEANUP_TASK_ID = "com.myapp.cache-cleanup"; async function initBackgroundFetch(): Promise<number> { const status = await BackgroundFetch.configure( { minimumFetchInterval: MIN_FETCH_INTERVAL_MINUTES, stopOnTerminate: false, startOnBoot: true, enableHeadless: true, requiredNetworkType: BackgroundFetch.NETWORK_TYPE_ANY, requiresBatteryNotLow: false, requiresCharging: false, }, async (taskId) => { // Default fetch event -- runs periodically console.log("[BackgroundFetch] Task started:", taskId); switch (taskId) { case CACHE_CLEANUP_TASK_ID: await cleanupExpiredCache(); break; default: // Default periodic sync await syncLatestData(); break; } // CRITICAL: Must call finish when done BackgroundFetch.finish(taskId); }, async (taskId) => { // Timeout callback -- OS is about to kill this task console.warn("[BackgroundFetch] Task timed out:", taskId); // Save partial progress, then finish immediately BackgroundFetch.finish(taskId); }, ); return status; } // Schedule a one-shot task async function scheduleCacheCleanup(): Promise<void> { await BackgroundFetch.scheduleTask({ taskId: CACHE_CLEANUP_TASK_ID, delay: CACHE_CLEANUP_DELAY_MS, periodic: false, forceAlarmManager: false, requiresNetworkConnectivity: false, }); } export { initBackgroundFetch, scheduleCacheCleanup }; ``` **Why good:** Separate handlers per taskId via switch, timeout callback saves partial progress, one-shot task scheduled separately, all Android options configured explicitly, named constants throughout --- ## Pattern 3: Android Headless JS Setup Enables background task execution after app termination on Android. Requires both JavaScript and native configuration. ```javascript // index.js -- app entry point import { AppRegistry } from "react-native"; import { App } from "./App"; const APP_NAME = "MyApp"; const HEADLESS_TASK_NAME = "com.transistorsoft.fetch"; // Default ID from react-native-background-fetch // Register the React app AppRegistry.registerComponent(APP_NAME, () => App); // Register headless task for Android background execution // This runs when the app is terminated but a background fetch fires AppRegistry.registerHeadlessTask(HEADLESS_TASK_NAME, () => async (event) => { const { taskId } = event; console.log("[HeadlessJS] Task:", taskId); try { await performLightweightSync(); } catch (error) { console.error("[HeadlessJS] Failed:", error); } // Task completes when the async function resolves // No need to call finish() -- the promise resolution signals completion }); ``` **Key requirements for headless JS:** 1. `enableHeadless: true` in BackgroundFetch.configure() 2. `stopOnTerminate: false` to allow post-termination execution 3. `AppRegistry.registerHeadlessTask()` in index.js 4. Android native: extend `HeadlessJsTaskService` (auto-configured by react-native-background-fetch) **Platform limitation:** Headless JS is Android-only. On iOS, force-quitting the app stops all background execution with no workaround. --- ## Pattern 4: Background Location Tracking (Expo) Continuous location updates while the app is backgrounded. Requires "Always Allow" permission. ```typescript // location-tracking.ts import * as TaskManager from "expo-task-manager"; import * as Location from "expo-location"; const LOCATION_TASK_NAME = "BACKGROUND_LOCATION_TRACKING"; const DISTANCE_INTERVAL_METERS = 100; const DEFERRED_UPDATE_INTERVAL_MS = 60000; // Batch updates every 60s // Top-level task definition TaskManager.defineTask(LOCATION_TASK_NAME, async ({ data, error }) => { if (error) { console.error("Background location error:", error.message); return; } if (!data) return; const { locations } = data as { locations: Location.LocationObject[] }; // Process location updates (batch upload, local storage, etc.) await uploadLocationBatch(locations); }); // Permission flow -- must request foreground THEN background separately async function requestLocationPermissions(): Promise<boolean> { const { status: foreground } = await Location.requestForegroundPermissionsAsync(); if (foreground !== "granted") return false; const { status: background } = await Location.requestBackgroundPermissionsAsync(); if (background !== "granted") { // User denied "Always Allow" -- background tracking won't work return false; } return true; } async function startTracking(): Promise<void> { const hasPermission = await requestLocationPermissions(); if (!hasPermission) return; const isTracking = await Location.hasStartedLocationUpdatesAsync(LOCATION_TASK_NAME); if (isTracking) return; // Already tracking await Location.startLocationUpdatesAsync(LOCATION_TASK_NAME, { accuracy: Location.Accuracy.Balanced, distanceInterval: DISTANCE_INTERVAL_METERS, deferredUpdatesInterval: DEFERRED_UPDATE_INTERVAL_MS, showsBackgroundLocationIndicator: true, // iOS: blue bar indicator foregroundService: { // Android: required foreground notification notificationTitle: "Location Tracking", notificationBody: "Tracking your route in the background", }, }); } async function stopTracking(): Promise<void> { const isTracking = await Location.hasStartedLocationUpdatesAsync(LOCATION_TASK_NAME); if (!isTracking) return; await Location.stopLocationUpdatesAsync(LOCATION_TASK_NAME); } export { startTracking, stopTracking, requestLocationPermissions }; ``` **Why good:** Separate foreground and background permission requests (Android requires this flow), checks if already tracking before starting, uses balanced accuracy (battery-efficient), batches updates with deferredUpdatesInterval, includes foreground service notification for Android, iOS background indicator shown ```typescript // BAD: Common location tracking mistakes async function startBadTracking() { // BAD: Only requesting foreground permission const { status } = await Location.requestForegroundPermissionsAsync(); await Location.startLocationUpdatesAsync(LOCATION_TASK_NAME, { accuracy: Location.Accuracy.BestForNavigation, // BAD: highest accuracy drains battery timeInterval: 1000, // BAD: every second is excessive for most use cases // BAD: No foregroundService -- Android will kill the task // BAD: No distanceInterval -- receives updates even when stationary }); } ``` **Why bad:** Missing background permission means tracking stops immediately on background, highest accuracy drains battery for little benefit over Balanced, 1-second interval is excessive, no foreground service on Android means the OS will terminate the task, no distance filter means unnecessary updates while stationary --- ## Pattern 5: iOS Expiration Listener Handle iOS system stopping your task early. The expiration listener fires when BGTaskScheduler decides to reclaim resources. ```typescript import * as BackgroundTask from "expo-background-task"; import type { Subscription } from "expo-modules-core"; let expirationSubscription: Subscription | null = null; function setupExpirationHandler(): void { // Clean up previous listener if any expirationSubscription?.remove(); expirationSubscription = BackgroundTask.addExpirationListener(() => { // iOS is about to stop our task -- save progress immediately savePartialProgress(); // Do NOT start new work here -- you have milliseconds, not seconds }); } function cleanupExpirationHandler(): void { expirationSubscription?.remove(); expirationSubscription = null; } export { setupExpirationHandler, cleanupExpirationHandler }; ``` **Why good:** Saves partial progress when iOS reclaims resources, cleans up subscription to prevent leaks, uses typed Subscription for cleanup **Note:** This is iOS-only. On Android, the timeout callback in `BackgroundFetch.configure()` serves the same purpose. --- ## Pattern 6: Checking Background Task Status Always verify background task availability before attempting registration. ```typescript import * as BackgroundTask from "expo-background-task"; type BackgroundAvailability = | { available: true } | { available: false; reason: string }; async function checkBackgroundAvailability(): Promise<BackgroundAvailability> { const status = await BackgroundTask.getStatusAsync(); switch (status) { case BackgroundTask.BackgroundTaskStatus.Available: return { available: true }; case BackgroundTask.BackgroundTaskStatus.Restricted: return { available: false, reason: "Background tasks are restricted. Check device settings to enable background app refresh.", }; default: { const _exhaustive: never = status; return { available: false, reason: `Unknown status: ${_exhaustive}` }; } } } export { checkBackgroundAvailability, type BackgroundAvailability }; ``` **Why good:** Exhaustive switch handles all enum values, returns structured result for UI consumption, provides actionable user guidance
-
-
reference.md 8 KB
# Background Tasks Quick Reference > Decision frameworks, platform constraints, and permission checklists. See [SKILL.md](SKILL.md) for patterns and red flags. --- ## Platform Constraint Summary ### iOS BGTaskScheduler | Task Type | Time Limit | Requirements | Use Case | | ----------------------------------- | --------------- | -------------------------------- | ------------------------------------ | | BGAppRefreshTask | ~30 seconds | None | Light data sync, feed refresh | | BGProcessingTask | Several minutes | Charger + network (configurable) | ML models, database maintenance | | BGContinuedProcessingTask (iOS 26+) | Until complete | User-initiated action | Export, upload started in foreground | **iOS scheduling behavior:** - System uses ML to predict optimal execution time based on user habits - Newly installed apps may take days for scheduling to stabilize - Force-quitting the app stops ALL background tasks until user reopens - Simulator does NOT run BGTaskScheduler -- test on physical devices only - `BGTaskSchedulerPermittedIdentifiers` must list all task IDs in Info.plist ### Android WorkManager / Doze | State | Network | CPU | AlarmManager | WorkManager | | ----------- | ------- | ---------- | ------------ | -------------------------------------- | | Active | Full | Full | Full | Full | | Doze (idle) | Blocked | Restricted | Deferred | Deferred (runs in maintenance windows) | | App Standby | Blocked | Restricted | Deferred | Deferred | | Deep Doze | Blocked | Blocked | Deferred | Deferred | **Android-specific constraints:** - WorkManager minimum interval: 15 minutes (hard limit) - Doze maintenance windows occur at increasing intervals (first after 1 hour, then 2, 4, etc.) - `forceAlarmManager: true` bypasses JobScheduler but increases battery usage - Vendor skins (Samsung, Xiaomi, Huawei, OPPO) add additional restrictions beyond stock Android - Reference: [dontkillmyapp.com](https://dontkillmyapp.com) for vendor-specific workarounds --- ## Permission Checklist ### iOS Permissions (Info.plist / app.json) ``` Background fetch / processing: [ ] UIBackgroundModes includes "processing" [ ] BGTaskSchedulerPermittedIdentifiers lists task IDs [ ] (expo-background-task auto-configures via CNG prebuild) Background location: [ ] UIBackgroundModes includes "location" [ ] NSLocationAlwaysAndWhenInUseUsageDescription set [ ] NSLocationWhenInUseUsageDescription set [ ] User granted "Always Allow" (not just "When In Use") ``` ### Android Permissions (AndroidManifest.xml) ``` Background fetch: [ ] RECEIVE_BOOT_COMPLETED (if startOnBoot: true) [ ] WAKE_LOCK (for keeping device awake during task) Background location: [ ] ACCESS_FINE_LOCATION [ ] ACCESS_COARSE_LOCATION [ ] ACCESS_BACKGROUND_LOCATION (Android 10+, separate prompt) [ ] FOREGROUND_SERVICE (for foreground service notification) [ ] FOREGROUND_SERVICE_LOCATION (Android 14+) ``` --- ## API Quick Reference ### expo-background-task | Method | Purpose | | --------------------------------------------------- | --------------------------------------- | | `TaskManager.defineTask(name, executor)` | Register task logic (top-level) | | `BackgroundTask.registerTaskAsync(name, options?)` | Schedule task with OS | | `BackgroundTask.unregisterTaskAsync(name)` | Remove scheduled task | | `BackgroundTask.getStatusAsync()` | Check if background tasks are available | | `BackgroundTask.triggerTaskWorkerForTestingAsync()` | Debug-only: trigger task manually | | `TaskManager.isTaskRegisteredAsync(name)` | Check if task is registered | | `BackgroundTask.addExpirationListener(fn)` | iOS: called when OS stops task early | **BackgroundTaskOptions:** - `minimumInterval?: number` -- Minutes between executions (min: 15, default: 720) **Return values:** - `BackgroundTaskResult.Success` (1) -- Task completed successfully - `BackgroundTaskResult.Failed` (2) -- Task failed ### react-native-background-fetch | Method | Purpose | | ------------------------------------------------------- | ---------------------------------- | | `BackgroundFetch.configure(config, onEvent, onTimeout)` | Initialize with handlers | | `BackgroundFetch.scheduleTask(config)` | Schedule one-shot or periodic task | | `BackgroundFetch.finish(taskId)` | Signal OS that task is done | | `BackgroundFetch.start()` | Resume background fetch | | `BackgroundFetch.stop(taskId?)` | Pause background fetch | | `BackgroundFetch.status()` | Check authorization status | **Key configure options:** - `minimumFetchInterval: number` -- Minutes (min: 15) - `stopOnTerminate: boolean` -- Android: stop when app killed (default: true) - `startOnBoot: boolean` -- Android: restart after reboot (default: false) - `enableHeadless: boolean` -- Android: enable Headless JS (default: false) - `forceAlarmManager: boolean` -- Android: use AlarmManager (default: false) - `requiredNetworkType: number` -- Network requirement (NONE, ANY, CELLULAR, WIFI) - `requiresBatteryNotLow: boolean` -- Skip when battery low - `requiresCharging: boolean` -- Only run when charging - `requiresDeviceIdle: boolean` -- Only run when device idle ### expo-location (background) | Method | Purpose | | ------------------------------------------------------- | --------------------------------- | | `Location.requestForegroundPermissionsAsync()` | Request foreground location | | `Location.requestBackgroundPermissionsAsync()` | Request "Always Allow" permission | | `Location.startLocationUpdatesAsync(taskName, options)` | Start background tracking | | `Location.stopLocationUpdatesAsync(taskName)` | Stop background tracking | | `Location.hasStartedLocationUpdatesAsync(taskName)` | Check if tracking active | **Location update options:** - `accuracy: Location.Accuracy.*` -- Lowest, Low, Balanced, High, Highest, BestForNavigation - `distanceInterval: number` -- Meters between updates - `timeInterval: number` -- Milliseconds between updates (Android only) - `deferredUpdatesInterval: number` -- Milliseconds to batch updates - `foregroundService: { notificationTitle, notificationBody }` -- Android foreground notification - `activityType: Location.ActivityType.*` -- iOS: Fitness, OtherNavigation, AutomotiveNavigation, Other --- ## Debugging Checklist ``` Task not executing? | +-> Is the task defined at the top-level scope? | +-> NO --> Move defineTask outside any component or lifecycle method | +-> Is the task registered? (check isTaskRegisteredAsync) | +-> NO --> Call registerTaskAsync | +-> Is background execution available? (check getStatusAsync) | +-> Restricted --> User or system disabled background refresh | +-> Are you testing on a real device? | +-> iOS simulator --> BGTaskScheduler does not run on simulators | +-> Did the user force-quit the app? | +-> iOS --> No tasks run until user reopens the app | +-> Android --> Check stopOnTerminate and enableHeadless settings | +-> Is the device in Doze mode? (Android) | +-> Check with: adb shell dumpsys deviceidle | +-> WorkManager tasks run in maintenance windows | +-> Is a vendor battery optimizer blocking it? (Android) +-> Check dontkillmyapp.com for device-specific settings +-> Guide user to disable battery optimization for your app ``` -
SKILL.md 16.8 KB
--- name: mobile-background-tasks description: Background fetch, processing tasks, background location, headless JS, battery optimization - Expo and bare React Native --- # React Native Background Tasks > **Quick Guide:** Background tasks in React Native are heavily constrained by OS power management. Use `expo-background-task` (Expo) or `react-native-background-fetch` (bare RN) for periodic fetch. Use `expo-location` for background location tracking. iOS gives ~30s for refresh tasks (BGAppRefreshTask) and several minutes for processing tasks (BGProcessingTask). Android enforces 15-minute minimum intervals via WorkManager and restricts execution in Doze mode. Always call `finish()` or return a result when done -- the OS will terminate tasks that exceed their time budget. --- <critical_requirements> ## CRITICAL: Before Using This Skill > **All code must follow project conventions in CLAUDE.md** (kebab-case, named exports, import ordering, `import type`, named constants) **(You MUST define tasks in the top-level scope (global) -- tasks defined inside React components or lifecycle methods will NOT be registered when the app starts from the background)** **(You MUST call `finish(taskId)` or return a `BackgroundTaskResult` when task execution completes -- failing to signal completion causes the OS to penalize or kill your app)** **(You MUST request background permissions explicitly on both platforms -- iOS requires Info.plist UIBackgroundModes entries, Android requires manifest permissions)** **(You MUST handle the OS killing your task at any time -- use expiration listeners on iOS and timeout callbacks on Android to clean up gracefully)** **(You MUST keep background work minimal -- sync only changed data, avoid heavy computation, respect the ~30s iOS refresh limit)** </critical_requirements> --- **Auto-detection:** expo-task-manager, expo-background-task, expo-background-fetch, expo-location background, react-native-background-fetch, BackgroundFetch, TaskManager, defineTask, registerTaskAsync, startLocationUpdatesAsync, Headless JS, registerHeadlessTask, BGTaskScheduler, WorkManager, background fetch, background processing, background location **When to use:** - Syncing data periodically while the app is backgrounded (new messages, feeds, email) - Tracking location in the background (fitness, delivery, navigation) - Running periodic cleanup or maintenance tasks (cache purge, log upload) - Keeping local data fresh so the app opens with current content - Processing uploads or downloads that continue after backgrounding **When NOT to use:** - Real-time updates that need sub-second latency (use push notifications + foreground handling) - Continuous audio playback (use the audio background mode, not task scheduling) - Tasks that must execute at an exact time (OS scheduling is advisory, not precise) - Tasks requiring more than a few minutes of CPU (iOS will terminate them) **Key patterns covered:** - Expo background tasks: `expo-background-task` (new) and `expo-background-fetch` (legacy) - Bare RN background fetch: `react-native-background-fetch` with configure/scheduleTask - Background location tracking with `expo-location` and TaskManager - Android Headless JS for post-termination task execution - iOS BGTaskScheduler constraints (refresh ~30s vs processing ~minutes) - Android battery optimization: Doze mode, App Standby, WorkManager guarantees - Task registration, unregistration, and lifecycle management **Detailed Resources:** - [examples/core.md](examples/core.md) - Expo background task, bare RN background fetch, background location, headless JS - [reference.md](reference.md) - Decision frameworks, platform constraints, permission checklists --- <philosophy> ## Philosophy Background execution on mobile is a **privilege, not a right**. Both iOS and Android aggressively limit what apps can do in the background to preserve battery life and user experience. The OS decides when (and whether) your task runs -- you can only request execution and set minimum intervals. **Core principles:** 1. **Minimize background work** -- Sync only deltas, not full datasets. The less you do, the more reliably the OS will schedule you. 2. **Always signal completion** -- Return a result code or call `finish()`. The OS tracks your task duration and penalizes apps that don't complete promptly. 3. **Define tasks globally** -- Background tasks must be registered at the top-level scope because the app may launch directly into background mode with no UI. 4. **Plan for termination** -- The OS can kill your task at any time. Use expiration/timeout handlers to save partial progress. 5. **Test on real devices** -- iOS simulators do not run BGTaskScheduler tasks. Android emulators may not enforce Doze mode. 6. **Respect platform differences** -- iOS kills all background tasks when the user force-quits. Android Headless JS can survive app termination with proper configuration. **The background execution spectrum:** ``` Most reliable Least reliable | | Push notifications > Foreground services > Background tasks > Timers (instant delivery) (visible to user) (OS-scheduled) (killed) ``` Background tasks sit in the middle -- more reliable than timers, but entirely at the OS's discretion. For critical work, combine with push notifications as a trigger. </philosophy> --- <patterns> ## Core Patterns ### Pattern 1: Expo Background Task (expo-background-task) The modern Expo approach using BGTaskScheduler (iOS) and WorkManager (Android). Replaces the older `expo-background-fetch`. ```typescript import * as TaskManager from "expo-task-manager"; import * as BackgroundTask from "expo-background-task"; const SYNC_TASK_NAME = "BACKGROUND_SYNC_TASK"; const TWELVE_HOURS_IN_MINUTES = 720; // MUST be top-level -- not inside a component TaskManager.defineTask(SYNC_TASK_NAME, async () => { try { const hasNewData = await fetchLatestUpdates(); return hasNewData ? BackgroundTask.BackgroundTaskResult.Success : BackgroundTask.BackgroundTaskResult.Failed; } catch { return BackgroundTask.BackgroundTaskResult.Failed; } }); ``` **Why good:** Task defined at top-level scope (runs even when app launches in background), returns explicit result code, handles errors ```typescript // BAD: Defining task inside a component function App() { useEffect(() => { // This will NOT work when app starts from background TaskManager.defineTask(SYNC_TASK_NAME, async () => { /* ... */ }); }, []); } ``` **Why bad:** Task definition inside component lifecycle will not execute when the OS launches the app headlessly in the background See [examples/core.md](examples/core.md) for complete registration/unregistration lifecycle. --- ### Pattern 2: Bare RN Background Fetch (react-native-background-fetch) For bare React Native projects (non-Expo). Wraps BGAppRefreshTask (iOS) and WorkManager (Android). ```typescript import BackgroundFetch from "react-native-background-fetch"; const MIN_FETCH_INTERVAL_MINUTES = 15; // Configure in app initialization (e.g., App component mount) const status = await BackgroundFetch.configure( { minimumFetchInterval: MIN_FETCH_INTERVAL_MINUTES, stopOnTerminate: false, // Android: continue after app killed startOnBoot: true, // Android: restart after device reboot enableHeadless: true, // Android: enable Headless JS requiredNetworkType: BackgroundFetch.NETWORK_TYPE_ANY, }, async (taskId) => { // Task triggered -- do your work await syncData(); BackgroundFetch.finish(taskId); // MUST call when done }, async (taskId) => { // Timeout -- OS is about to kill the task, clean up immediately BackgroundFetch.finish(taskId); }, ); ``` **Why good:** Explicit timeout handler for graceful cleanup, `finish(taskId)` signals OS completion, Android-specific options for post-termination behavior See [examples/core.md](examples/core.md) for scheduleTask one-shot/periodic tasks and Headless JS setup. --- ### Pattern 3: Background Location Tracking Continuous location updates while backgrounded. Uses `expo-location` with `expo-task-manager`. Requires explicit background permission grants ("Always Allow" on iOS). ```typescript import * as TaskManager from "expo-task-manager"; import * as Location from "expo-location"; const LOCATION_TASK_NAME = "BACKGROUND_LOCATION_TASK"; // Top-level task definition TaskManager.defineTask(LOCATION_TASK_NAME, async ({ data, error }) => { if (error) { console.error("Location task error:", error.message); return; } if (data) { const { locations } = data as { locations: Location.LocationObject[] }; await saveLocationsToServer(locations); } }); ``` **Why good:** Top-level definition, explicit error handling, typed location data extraction See [examples/core.md](examples/core.md) for permission flow, start/stop, and accuracy configuration. --- ### Pattern 4: Android Headless JS Android-only mechanism for running JavaScript after app termination. Requires native setup and registration in `index.js`. ```javascript // index.js -- register headless task alongside app import { AppRegistry } from "react-native"; import { App } from "./App"; const APP_NAME = "MyApp"; const HEADLESS_TASK_NAME = "com.transistorsoft.fetch"; // Default ID from react-native-background-fetch AppRegistry.registerComponent(APP_NAME, () => App); // Headless task runs when app is terminated but task fires AppRegistry.registerHeadlessTask(HEADLESS_TASK_NAME, () => async (taskData) => { await performSync(taskData); // Task completes when promise resolves }); ``` **Why good:** Registered at app entry point, async function allows proper cleanup, runs even after app termination on Android **Gotcha:** Headless JS is Android-only. iOS has no equivalent -- once the user force-quits the app, no background tasks run until the user reopens it. See [examples/core.md](examples/core.md) for complete headless setup with `enableHeadless` configuration. --- ### Pattern 5: Task Registration and Unregistration Lifecycle Always check registration status before registering, and unregister when tasks are no longer needed. ```typescript async function ensureBackgroundSyncRegistered(): Promise<void> { const isRegistered = await TaskManager.isTaskRegisteredAsync(SYNC_TASK_NAME); if (isRegistered) return; await BackgroundTask.registerTaskAsync(SYNC_TASK_NAME, { minimumInterval: TWELVE_HOURS_IN_MINUTES, }); } async function disableBackgroundSync(): Promise<void> { const isRegistered = await TaskManager.isTaskRegisteredAsync(SYNC_TASK_NAME); if (!isRegistered) return; await BackgroundTask.unregisterTaskAsync(SYNC_TASK_NAME); } ``` **Why good:** Guards against double-registration, idempotent enable/disable, named constants for intervals See [examples/core.md](examples/core.md) for status checking and debugging patterns. </patterns> --- <decision_framework> ## Decision Framework ### Choosing a Background Task Approach ``` What kind of background work do you need? | +-> Periodic data sync (every 15min - 12hrs)? | +-> Expo project? --> expo-background-task | +-> Bare RN? --> react-native-background-fetch | +-> Continuous location tracking? | +-> Expo? --> expo-location + startLocationUpdatesAsync | +-> Bare RN? --> react-native-background-geolocation | +-> Complete a task started in foreground (iOS 26+)? | +-> BGContinuedProcessingTask (new in iOS 26) | +-> Long-running processing (ML, export)? | +-> Foreground service with notification (Android) | +-> BGProcessingTask (iOS, requires charger + network) | +-> Must survive app termination (Android)? | +-> Headless JS + enableHeadless: true + stopOnTerminate: false | +-> Must execute at exact time? +-> Not possible with background tasks +-> Use push notifications + server-side scheduling ``` ### expo-background-task vs expo-background-fetch | Feature | expo-background-task | expo-background-fetch | | ---------------- | ------------------------ | ----------------------- | | Status | **Active** (recommended) | **Deprecated** | | iOS API | BGTaskScheduler | Legacy Background Fetch | | Android API | WorkManager | JobScheduler | | Min interval | 15 minutes | ~10 minutes (advisory) | | Network required | Yes (by default) | No | | Reliability | Higher | Lower | ### Platform Execution Limits | Constraint | iOS | Android | | -------------------- | --------------------------------------------- | ---------------------------------- | | Refresh task time | ~30 seconds | ~10 minutes | | Processing task time | Several minutes (charger required) | ~10 minutes | | Minimum interval | 15 minutes (system-managed) | 15 minutes (WorkManager-enforced) | | After force-quit | No tasks run | Headless JS can run (with config) | | After reboot | Tasks resume automatically | Requires `startOnBoot: true` | | Simulator support | No (physical device only for BGTaskScheduler) | Partial (Doze may not be enforced) | </decision_framework> --- <red_flags> ## RED FLAGS **High Priority Issues:** - Defining tasks inside React components or useEffect -- tasks MUST be at the top-level scope or they won't run when the app starts from background - Not calling `finish(taskId)` or returning a result -- the OS will penalize your app, reducing future scheduling frequency or killing the task - Expecting exact timing -- background task intervals are minimums, the OS may delay execution by hours or even days on iOS - Using `setTimeout`/`setInterval` for background work -- these are killed immediately when the app is backgrounded - Not requesting background permissions -- iOS requires Info.plist UIBackgroundModes, Android requires ACCESS_BACKGROUND_LOCATION and RECEIVE_BOOT_COMPLETED **Medium Priority Issues:** - Doing heavy computation in a background refresh task -- iOS gives ~30 seconds, not minutes - Not handling the timeout/expiration callback -- if the OS decides to stop your task early, you must save progress and exit - Assuming background location works with "When In Use" permission -- it requires "Always Allow" on iOS - Testing only on simulators -- iOS simulators do not execute BGTaskScheduler tasks - Not checking `getStatusAsync()` before registering -- background tasks may be restricted by user settings or device state **Gotchas & Edge Cases:** - iOS force-quit kills ALL background tasks until user reopens the app -- there is no workaround - Android vendor battery optimizations (Samsung, Xiaomi, Huawei) may kill background tasks beyond stock Android Doze restrictions -- see dontkillmyapp.com - `expo-background-task` requires network connectivity by default -- tasks won't run offline - iOS BGTaskScheduler uses machine learning to predict when to run your task -- it may take days to "settle in" for newly installed apps - WorkManager enforces a hard 15-minute minimum interval -- you cannot schedule more frequently - Headless JS is Android-only -- iOS has no equivalent post-termination execution - `expo-background-fetch` is deprecated in favor of `expo-background-task` -- migrate to the new API - Background tasks registered with expo-task-manager persist across app restarts -- always check `isTaskRegisteredAsync` before re-registering - Android 15/16 edge-to-edge changes do not affect background tasks, but foreground service notification requirements have tightened - iOS 26 introduces BGContinuedProcessingTask for completing user-initiated work in the background -- a new option for tasks started in foreground </red_flags> --- <critical_reminders> ## CRITICAL REMINDERS > **All code must follow project conventions in CLAUDE.md** **(You MUST define tasks in the top-level scope (global) -- tasks defined inside React components or lifecycle methods will NOT be registered when the app starts from the background)** **(You MUST call `finish(taskId)` or return a `BackgroundTaskResult` when task execution completes -- failing to signal completion causes the OS to penalize or kill your app)** **(You MUST request background permissions explicitly on both platforms -- iOS requires Info.plist UIBackgroundModes entries, Android requires manifest permissions)** **(You MUST handle the OS killing your task at any time -- use expiration listeners on iOS and timeout callbacks on Android to clean up gracefully)** **(You MUST keep background work minimal -- sync only changed data, avoid heavy computation, respect the ~30s iOS refresh limit)** **Failure to follow these rules will result in tasks that never execute, apps penalized by the OS scheduler, or apps rejected from the App Store for excessive background resource usage.** </critical_reminders>
Comments (0)
Sign in to join the conversation.
Reviews (0)
No reviews yet.
No comments yet.