expo-ios-background-task
v0.1.1
Published
An iOS-only module that exposes beginBackgroundTask and endBackgroundTask to JavaScript, enabling short, system-approved background execution time for finishing important work.
Maintainers
Readme
expo-ios-background-task
An iOS-only Expo module that exposes beginBackgroundTask and endBackgroundTask to JavaScript, enabling short, system-approved background execution time for finishing important work.
Overview
This module provides a JavaScript interface to iOS's background task API (UIApplication.beginBackgroundTask). It allows your app to request additional time to complete critical tasks when transitioning to the background, such as:
- Saving user data
- Uploading/downloading files
- Completing network requests
- Performing cleanup operations
Important: iOS provides a limited amount of background execution time (typically 30 seconds). You must end the task promptly when finished, or the system will terminate your app.
Platform Support
| Platform | Support | |----------|---------| | iOS | ✅ Full support | | Android | ⚠️ No-op (returns success, but doesn't actually run background tasks) | | Web | ⚠️ No-op (returns success, but doesn't actually run background tasks) |
Installation
Managed Expo Projects
npx expo install expo-ios-background-taskBare React Native Projects
- Install the package:
npm install expo-ios-background-task- For iOS, install CocoaPods dependencies:
cd ios && pod install && cd ..Usage
Basic Example (Recommended Pattern)
According to Apple's documentation: Start the background task BEFORE beginning your long-running work, not when the app goes to background.
import ExpoIosBackgroundTask from 'expo-ios-background-task';
// ✅ CORRECT: Start task BEFORE beginning work
async function saveUserData() {
// Begin the background task FIRST
const result = await ExpoIosBackgroundTask.beginBackgroundTask('Save Data');
if (!result.success) {
console.error('Failed to start background task:', result.error);
return;
}
const taskId = result.taskId;
try {
// Now perform your long-running work
await performDatabaseSave();
await syncWithServer();
await uploadPendingFiles();
} catch (error) {
console.error('Error during background work:', error);
} finally {
// Always end the task when done
await ExpoIosBackgroundTask.endBackgroundTask(taskId);
}
}
// Trigger the function when needed (e.g., user action, app state change)
saveUserData();Monitoring App State Changes
While you should start the background task before beginning work, you can use AppState to detect when the app is about to go to background and trigger your work:
import { AppState } from 'react-native';
import ExpoIosBackgroundTask from 'expo-ios-background-task';
// Listen for app state changes
AppState.addEventListener('change', async (nextAppState) => {
// When app is about to go inactive/background
if (nextAppState === 'inactive' || nextAppState === 'background') {
await saveUserData(); // This function starts the task internally
}
});
async function saveUserData() {
// Start background task BEFORE doing work
const result = await ExpoIosBackgroundTask.beginBackgroundTask('Save Data');
if (result.success) {
const taskId = result.taskId;
try {
// Perform your work here
await syncData();
} finally {
await ExpoIosBackgroundTask.endBackgroundTask(taskId);
}
}
}❌ Incorrect Pattern (Don't Do This)
// DON'T: Wait until app is in background to start task
AppState.addEventListener('change', async (nextAppState) => {
if (nextAppState === 'background') {
// ❌ This is too late - work has already started or app is already backgrounded
const result = await ExpoIosBackgroundTask.beginBackgroundTask('Save Data');
// ... do work
}
});With Expiration Handler
import ExpoIosBackgroundTask, { useEvent } from 'expo-ios-background-task';
function MyComponent() {
const [taskId, setTaskId] = useState<number | null>(null);
// Listen for task expiration events
const expirationPayload = useEvent(ExpoIosBackgroundTask, 'onTaskExpiration');
useEffect(() => {
if (expirationPayload) {
console.warn(`Task "${expirationPayload.taskName}" expired!`);
// Clean up and end the task
if (taskId === expirationPayload.taskId) {
setTaskId(null);
}
}
}, [expirationPayload, taskId]);
const performLongTask = async () => {
// Start task BEFORE beginning work
const result = await ExpoIosBackgroundTask.beginBackgroundTask('My Task');
if (result.success) {
setTaskId(result.taskId);
try {
// Do your long-running work
await doSomethingImportant();
} finally {
await ExpoIosBackgroundTask.endBackgroundTask(result.taskId);
setTaskId(null);
}
}
};
return (
<Button title="Start Long Task" onPress={performLongTask} />
);
}Check Remaining Background Time
import ExpoIosBackgroundTask from 'expo-ios-background-task';
const remaining = ExpoIosBackgroundTask.getBackgroundTimeRemaining();
if (remaining === -1) {
console.log('App is in foreground (unlimited time)');
} else {
console.log(`Remaining background time: ${remaining.toFixed(2)} seconds`);
}API Reference
Methods
beginBackgroundTask(taskName?: string): Promise<BeginBackgroundTaskResult>
Begins a background task that extends your app's runtime. Returns a task ID that must be used to end the task.
Important: Call this method BEFORE starting any long-running task, not after the app goes to background.
Parameters:
taskName(optional): A string identifier for the task. Useful for debugging and logging.
Returns:
{
success: boolean;
taskId: number; // Use this ID to end the task
taskName?: string; // The name you provided
error?: string; // Error message if success is false
}Example:
const result = await ExpoIosBackgroundTask.beginBackgroundTask('Upload Photo');
if (result.success) {
console.log(`Task started with ID: ${result.taskId}`);
try {
// Do your work...
await uploadPhoto();
} finally {
await ExpoIosBackgroundTask.endBackgroundTask(result.taskId);
}
} else {
console.error(`Failed to start task: ${result.error}`);
}Possible Errors:
"Failed to begin background task. App may not be in background state or maximum tasks reached."- You've reached the maximum number of concurrent background tasks (typically 1-2).
endBackgroundTask(taskId: number): Promise<boolean>
Ends a background task that was previously started. Always call this when your work is complete to free system resources.
Parameters:
taskId: The task ID returned frombeginBackgroundTask.
Returns:
Promise<boolean>:trueif the task was ended successfully,falseif the task doesn't exist or has already expired.
Example:
const success = await ExpoIosBackgroundTask.endBackgroundTask(taskId);
if (!success) {
console.warn('Task may have already expired');
}getBackgroundTimeRemaining(): number
Gets the amount of background time remaining for the app.
Returns:
number: The remaining background time in seconds, or-1if the app is in the foreground.
Example:
const remaining = ExpoIosBackgroundTask.getBackgroundTimeRemaining();
if (remaining === -1) {
console.log('App is in foreground');
} else if (remaining < 5) {
console.warn('Less than 5 seconds remaining!');
}Events
onTaskExpiration
Fired when a background task is about to expire. The system calls this when it's about to terminate your app.
Event Payload:
{
taskId: number;
taskName: string;
}Example:
import { useEvent } from 'expo';
import ExpoIosBackgroundTask from 'expo-ios-background-task';
function MyComponent() {
const expirationPayload = useEvent(ExpoIosBackgroundTask, 'onTaskExpiration');
useEffect(() => {
if (expirationPayload) {
console.warn(`Task "${expirationPayload.taskName}" is expiring!`);
// End the task immediately to avoid app termination
ExpoIosBackgroundTask.endBackgroundTask(expirationPayload.taskId);
}
}, [expirationPayload]);
}TypeScript Support
This package includes TypeScript definitions. Import types as needed:
import ExpoIosBackgroundTask, {
BeginBackgroundTaskResult,
TaskExpirationEventPayload
} from 'expo-ios-background-task';Important Notes
⚠️ When to Start Background Tasks
Apple's Recommendation: Start the background task BEFORE beginning your long-running work, not when the app transitions to background.
From Apple's documentation:
"Don't wait until your app moves to the background to call the
beginBackgroundTask(withName:expirationHandler:)method. Call the method before performing any long-running task."
// ✅ CORRECT
async function saveData() {
const task = await ExpoIosBackgroundTask.beginBackgroundTask('Save');
try {
await performSave(); // Start work AFTER beginning task
} finally {
await ExpoIosBackgroundTask.endBackgroundTask(task.taskId);
}
}
// ❌ INCORRECT
async function saveData() {
await performSave(); // Work started before task begins
const task = await ExpoIosBackgroundTask.beginBackgroundTask('Save');
await ExpoIosBackgroundTask.endBackgroundTask(task.taskId);
}⚠️ Background Time Limits
- iOS typically provides ~30 seconds of background execution time
- The exact time varies based on system conditions and battery level
- Always end tasks promptly to avoid app termination
⚠️ Task Limits
- iOS allows a limited number of concurrent background tasks (typically 1-2)
- Starting a new task when at the limit will fail
- Always end tasks when finished
⚠️ Always End Tasks
- Critical: Always call
endBackgroundTaskwhen your work is complete - Use try/finally blocks to ensure tasks are ended even if errors occur
- If a task expires, the system will end it automatically, but your app may be terminated
Troubleshooting
Task fails to start
- Check task limit: You may have reached the maximum number of concurrent tasks
- Verify iOS platform: This module only works on iOS
- System resources: iOS may deny background tasks under low memory or battery conditions
Task expires immediately
- Check remaining time: Use
getBackgroundTimeRemaining()to see available time - System conditions: iOS may provide less time under certain conditions (low battery, etc.)
Module not found error
- Rebuild native code: Run
npx expo prebuild --cleanand rebuild your app - Check installation: Ensure the package is installed and linked correctly
- Development builds: Make sure you're using a development build, not Expo Go (Expo Go doesn't support custom native modules)
Example App
See the example app for a complete working example with:
- Task management UI
- Timer demonstration
- App state monitoring
- Expiration handling
Show Your Support
If you find this package helpful, please consider giving it a ⭐️ on GitHub!
Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
- Fork the repository
- Create your feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'Add some amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
License
MIT © Prathamesh Karambelkar
Links
Made with ❤️ by Prathamesh Karambelkar
