What Is Headless JS in React Native?

Headless JS is a built‑in React Native feature that lets you execute JavaScript code in the background even when the application’s user interface is not active. Unlike foreground execution, where the app is visible and interactive, Headless JS runs tasks as separate processes that do not depend on the main UI thread. This is essential for operations that must continue after the user closes the app or before they open it – such as syncing data, processing incoming push notifications, or handling time‑critical alerts. Headless JS bridges the gap between React Native’s JavaScript runtime and the native layers of Android and iOS, allowing you to write cross‑platform background logic with minimal native code.

Common Use Cases for Headless JS

Understanding when to use Headless JS helps you decide if it fits your project. Typical scenarios include:

  • Data synchronization – Uploading logs or downloading new content when the app is in the background.
  • Push notification handling – Performing custom logic when a notification arrives (e.g., updating a badge, storing data locally) before showing the alert.
  • Location updates – Polling GPS coordinates in the background for geofencing or real‑time tracking.
  • Time‑based tasks – Executing a job at a scheduled interval (like daily clean‑up) even if the user hasn’t opened the app.
  • Quick actions from the home screen – Performing a short operation when the user presses a 3D Touch or widget action.

Headless JS is not designed for long‑running or heavy computations – the system may kill your background process if it takes too long. For extended background work, combine it with native services like WorkManager (Android) or BackgroundTasks (iOS).

Setting Up Headless JS in React Native: Step‑by‑Step

Implementing Headless JS involves two main parts: creating a JavaScript handler and registering it in the native code for each platform. We will walk through both parts.

1. Create the JavaScript Background Handler

Start by creating a new file, for example app/background/BackgroundTask.js. This file will contain the function that runs when the background task is triggered. The function receives a taskData object, which can hold parameters passed from the native side.

// BackgroundTask.js
import { AppRegistry } from 'react-native';

const backgroundTask = async (taskData) => {
  console.log('Background task started with data:', taskData);
  // Your background logic goes here
  // For example, fetch new data from an API
  try {
    const response = await fetch('https://api.example.com/sync');
    const data = await response.json();
    // Store data in AsyncStorage or a local database
    console.log('Background sync complete');
  } catch (error) {
    console.error('Background task failed:', error);
    // Optionally throw an error to let the system know
  }
};

AppRegistry.registerHeadlessTask('BackgroundTask', () => backgroundTask);

Note that the handler must be a function that returns a Promise (or an async function). The system will wait for the Promise to resolve before considering the task complete. If the Promise rejects, the native side may treat it as a failure.

2. Register the Task in Android Native Code

Android requires you to register the headless task in your MainApplication.java (or MainActivity.java depending on the React Native version). Additionally, you need a HeadlessJsTaskService to manage the task lifecycle.

Step A – Update MainApplication.java

// android/app/src/main/java/com/yourapp/MainApplication.java
import com.facebook.react.HeadlessJsTaskService;

public class MainApplication extends Application implements ReactApplication {
  // ... existing code

  @Override
  public void onCreate() {
    super.onCreate();
    // Initialize ReactNative instance as usual
    // The task will be registered via AppRegistry in JavaScript
  }
}

No explicit registration in Java is needed for the task itself if you use AppRegistry.registerHeadlessTask – React Native automatically maps the JavaScript handler to a native task. However, you must create a service that can be started by the system.

Step B – Create a HeadlessJsTaskService

Create a new Java class BackgroundTaskService.java in the same package:

// android/app/src/main/java/com/yourapp/BackgroundTaskService.java
package com.yourapp;

import android.content.Intent;
import android.os.Bundle;
import com.facebook.react.HeadlessJsTaskService;
import com.facebook.react.bridge.Arguments;
import com.facebook.react.jstasks.HeadlessJsTaskConfig;

public class BackgroundTaskService extends HeadlessJsTaskService {
  @Override
  protected HeadlessJsTaskConfig getTaskConfig(Intent intent) {
    Bundle extras = intent.getExtras();
    if (extras != null) {
      return new HeadlessJsTaskConfig(
        "BackgroundTask",
        Arguments.fromBundle(extras),
        5000, // timeout in ms
        true  // allows task to run in foreground as well
      );
    }
    return null;
  }
}

This service extracts any extras from the Intent (like data from a notification) and passes them to the JavaScript handler. The timeout prevents runaway tasks.

Step C – Add the Service to AndroidManifest.xml

<service android:name=".BackgroundTaskService" />

Now any native module can start this service using an Intent, or you can use a broadcast receiver to trigger it from a push notification.

3. Set Up iOS Background Capabilities

iOS is more restrictive about background execution. To use Headless JS on iOS, you must enable appropriate background modes and use native scheduling (like push notifications or background fetch).

Step A – Enable Background Modes

Open your Xcode project, go to Signing & Capabilities, and add the Background Modes capability. Check the modes your app needs, for example “Background fetch” or “Remote notifications”.

Step B – Implement App Delegate Methods

In AppDelegate.m, implement the required delegate methods to receive background events and start the headless task.

// AppDelegate.m (Objective-C)
#import <React/RCTAppDelegate.h>
#import "AppDelegate.h"
#import <React/RCTBridge.h>
#import <React/RCTRootView.h>

@implementation AppDelegate

- (void)application:(UIApplication *)application
 performFetchWithCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler {
  // You can get the bridge from your ReactNative root view
  RCTBridge *bridge = [self.rootView bridge];
  [bridge.eventDispatcher sendAppEventWithName:@"BackgroundFetch"
                                         body:@{@"taskName": @"BackgroundTask"}];
  completionHandler(UIBackgroundFetchResultNewData);
}

@end

On the JavaScript side, you need to listen for the BackgroundFetch event and then start the headless task manually. However, Headless JS on iOS is more commonly used together with push notifications – when a notification arrives, you can execute headless code before showing the alert.

For push notifications, implement application:didReceiveRemoteNotification:fetchCompletionHandler: in AppDelegate and send an event to React Native.

Triggering Background Tasks

Once set up, you can trigger a Headless JS task from various sources:

  • Push notifications (Android & iOS) – Use a custom broadcast receiver on Android or the app delegate on iOS to start the service when a silent notification arrives.
  • Native modules – Create a native module that calls context.startService(new Intent(context, BackgroundTaskService.class)) on Android or posts a notification on iOS.
  • Background fetch (iOS) – iOS calls your app delegate periodically; you can use that opportunity to fire the headless task.
  • Alarms – Use react-native-background-fetch or react-native-alarm to schedule periodic execution.

Example: Starting the Android service from a broadcast receiver that listens for a custom action:

public class AlarmReceiver extends BroadcastReceiver {
  @Override
  public void onReceive(Context context, Intent intent) {
    Intent serviceIntent = new Intent(context, BackgroundTaskService.class);
    serviceIntent.putExtra("source", "alarm");
    context.startService(serviceIntent);
  }
}

Then register the receiver in AndroidManifest.xml and schedule an alarm using AlarmManager.

Best Practices for Reliable Background Operation

Background tasks are inherently fragile – the OS can kill your process at any time. Follow these guidelines to make your Headless JS tasks robust:

  • Keep tasks short. Aim for under 30 seconds of execution. Use timeouts (the 5000ms in our example) to prevent hanging tasks.
  • Handle errors gracefully. Wrap your logic in try/catch. If the task fails, consider logging locally using a crash reporter (like Sentry) and do not block the UI.
  • Use offline‑first patterns. Since background tasks often run without network guarantees, implement retries and store failed operations for later.
  • Avoid heavy state management. Do not rely on Redux or React state – background tasks run in a separate JavaScript context. Use AsyncStorage, SQLite (react-native-sqlite-storage), or secure storage for data persistence.
  • Test on real devices. The Android emulator and iOS simulator behave differently from physical hardware. Test with device screen off and in Doze mode (Android) or Low Power Mode (iOS).
  • Respect battery optimization. Use startService with a wake lock on Android to prevent the CPU from sleeping, but release the lock promptly. On iOS, background fetch intervals are managed by the system – respect the granted time.

Debugging Headless JS Tasks

Debugging background code is tricky because you cannot use the Chrome DevTools inspector when the UI is not active. Use these techniques:

  • Logcat (Android) – Add console.log statements; they appear in logcat with the tag “ReactNativeJS”.
  • Local file logs – Write log entries to a file on the device and retrieve them later.
  • Flipper – React Native Flipper can capture console logs from headless tasks (ensure you set isBackgroundTask to true in your Flipper plugin).
  • Native logging – Use android.util.Log in your Java service to trace when the task starts and ends.

Limitations and Considerations

Headless JS is not a silver bullet. Understand its limitations to avoid surprises:

  • No UI interaction. You cannot render views or show alerts inside a headless task. Use push notifications to inform the user of results.
  • Single task per process. React Native’s headless service runs only one JavaScript task at a time. If you need multiple parallel background operations, consider using native services (WorkManager on Android, BGTaskScheduler on iOS).
  • iOS restrictions. iOS heavily throttles background execution. Headless JS on iOS is only reliable for short tasks triggered by push notifications or background fetch – the system may delay or prevent execution if the user force‑quits the app.
  • No Redux store. As mentioned earlier, the headless JavaScript context does not share the same Redux store as the foreground app. Use persistent storage to pass data between the two contexts.
  • App termination. If the user force‑closes your app on Android 12+ or iOS, background tasks may not run until the user opens the app again.

Comparison with Other Background Libraries

React Native’s Headless JS is the lowest‑level built‑in mechanism. For many use cases, community packages provide a more convenient API:

LibraryPlatformFeatures
react-native-background-fetchAndroid & iOSPeriodic background fetching with system‑managed scheduling; supports Headless JS internally.
react-native-background-actionsAndroid & iOSRuns a long‑running foreground service (with persistent notification) – can execute heavy tasks.
WorkManager (Android only via native bridge)AndroidDeferrable, reliable background work with constraints (network, battery); does not use Headless JS by default.
Headless JS (vanilla)Android & iOSSimple, built‑in, no extra dependencies; best for short tasks triggered by push or alarms.

If your task must run even after a reboot, or you need complex scheduling, consider wrapping a native WorkManager implementation and exposing it via a React Native native module. Headless JS remains excellent for lightweight “fire‑and‑forget” jobs.

Complete Example: Syncing Data on Push Notification

Let’s put everything together with a realistic scenario: when a silent push notification arrives on Android, the app download the latest data from an API and stores it locally. The user never sees a UI change – the data is ready when they next open the app.

Android – Push Receiver

Create a broadcast receiver that listens for the push action (e.g., from FCM):

public class PushReceiver extends BroadcastReceiver {
  @Override
  public void onReceive(Context context, Intent intent) {
    Intent serviceIntent = new Intent(context, BackgroundTaskService.class);
    serviceIntent.putExtra("action", "syncData");
    context.startService(serviceIntent);
  }
}

Register it in AndroidManifest.xml with an intent‑filter for your push action (or use FirebaseMessagingService’s onMessageReceived to start the service directly).

JavaScript Handler

// BackgroundTask.js (expanded)
import { AppRegistry } from 'react-native';
import AsyncStorage from '@react-native-async-storage/async-storage';

const backgroundTask = async (taskData) => {
  const { action } = taskData;
  if (action === 'syncData') {
    try {
      const response = await fetch('https://api.example.com/latest-data');
      const json = await response.json();
      await AsyncStorage.setItem('cachedData', JSON.stringify(json));
      console.log('Data synced successfully');
    } catch (error) {
      // Write error to a local log
      const errorLog = await AsyncStorage.getItem('errorLog');
      const newLog = (errorLog ? errorLog + '\n' : '') + new Date().toISOString() + ': ' + error.message;
      await AsyncStorage.setItem('errorLog', newLog);
    }
  }
};

AppRegistry.registerHeadlessTask('BackgroundTask', () => backgroundTask);

When the user reopens the app, the foreground can read AsyncStorage and update the UI immediately, creating a seamless offline‑first experience.

Performance and Battery Impact

Headless tasks run in the same JavaScript engine as the main app, but in a separate context. Each task spins up a new headless JS instance, which consumes memory and CPU. To minimize battery drain:

  • Use the shortest timeout that still allows the work to complete.
  • Do not perform frequent polling – prefer push‑based triggers.
  • On Android, call stopSelf() from the service after the task finishes (HeadlessJsTaskService does this automatically when the Promise resolves).
  • Combine with battery‑aware APIs like WorkManager for Android and BGTaskScheduler for iOS if you need periodic work.

Conclusion

Headless JS is a powerful tool in the React Native developer’s belt for handling short background tasks that must execute regardless of the app’s visibility. By setting up a JavaScript handler and connecting it with native background triggers – such as push notifications, alarms, or background fetch – you can build responsive, data‑driven applications that feel always‑on. Remember to follow best practices around brevity, error handling, and platform‑specific limitations to ensure your background operations are both reliable and battery‑friendly. For advanced scheduling or longer tasks, complement Headless JS with dedicated libraries like react-native-background-fetch or native WorkManager.

For further reading, refer to the official React Native documentation on Headless JS (Android) and Headless JS (iOS). Also check the react-native-background-fetch library for periodic task management.