Debjyoti Banerjee I'm Debjyoti, software engineer and game developer. Currently exploring JavaScript and Flutter, and trying to come up with solutions to problems in the healthcare sector. Love open source.

Run React Native background tasks with Headless JS

5 min read 1470

React Background Image Processing

Background processing is an important part of mobile application development, particularly when you want to poll a server or periodically send data to a server. Usually, an app is in the background if no activities are visible to the user.

In React Native, performing a task in the background might seem daunting at first. It is not as simple as writing a function in JavaScript, where the function is executed even after the activity is killed. Instead, React Native uses Headless JS to execute JavaScript code in the background.

In this article, we’ll learn to execute background operations in React Native using Native APIs and Headless JS. Headless JS is available only for Android, therefore, we’ll focus only on Android development in this article. First, let’s go over some fundamental tools that we’ll use.

Headless JS

Headless JS is a way to run JavaScript tasks in the background. Obviously, we can’t touch the UI in these tasks, but we can register a HeadlessTask like we would a React application in AppRegistry:

AppRegistry.registerHeadlessTask('DemoTask', () =>  require('DemoTask'));

Native Modules

Native Modules allow us to execute native code from within JavaScript. You can think of it as setting up a bridge between the JavaScript layer and the native layer.

WorkManager

WorkManager is the recommended way to perform background tasks in Android. WorkManager can schedule one-time or periodic tasks in a simple, reliable way.

Setting up Headless JS

Let’s build a toy application that would perform a periodic task using Headless JS. Setup for our application has three steps:

  • Set up a React Native project like you normally would using the React Native CLI
  • Add the WorkManager dependency to the app’s build.gradle as follows:
implementation 'androidx.work:work-runtime:workmanagerversion'

Optionally, you can set up a simple Node.js Echo server and connect your device to the same network as your development machine. I prefer testing on a real device, so for instance, I’d connect my device to the development server over Wi-Fi, then I’d be able to make real requests to the Node server running on the same machine.

Building our React Native application

Create a background worker

We’ll start by creating our background worker. The BackgroundWorker class extends the Worker class, which provides a doWork method where all the background tasks will occur:

public class BackgroundWorker extends Worker {
    private final Context context;

    public BackgroundWorker(@NonNull Context context, @NonNull WorkerParameters workerParams) {
        super(context, workerParams);
        this.context = context;
    }
    @NonNull
    @Override
    public Result doWork() {

        // background work will take place here
        Log.w("bg", "Worker do work");
        return Result.success();

    }
}

Result.success() tells WorkManager that the work finished successfully. We’ll start our Headless service at doWork.

WorkRequest tells WorkManager how and when our background task should execute. In our example, we want a periodic task, so we’ll use PeriodicWorkRequest. For a one-time work task, you’d use OneTimeWorkRequest:

workRequest = new PeriodicWorkRequest.Builder(BackgroundWorker.class, 20, TimeUnit.MINUTES).build();

Create a Native Module

Next, we’ll create a Native Module that provides a way to create and enqueue a work request at the click of a button. Our work request causes WorkManager to execute our work periodically.

Our event handlers will be written in JavaScript, so we need a way to call the native methods using JavaScript:

// BackgroundModule.java
public class BackgroundModule extends ReactContextBaseJavaModule {
    private static final String MODULE_NAME = "BackgroundWorkManager";

    private Context mContext;
    private PeriodicWorkRequest workRequest;

    BackgroundModule(@Nonnull ReactApplicationContext reactContext) {
        super(reactContext);
        mContext = reactContext;
        workRequest = new PeriodicWorkRequest.Builder(BackgroundWorker.class, 20, TimeUnit.MINUTES).build();
    }

    @ReactMethod
    public void startBackgroundWork() {
        WorkManager.getInstance(mContext).enqueueUniquePeriodicWork("testWork", ExistingPeriodicWorkPolicy.KEEP,workRequest);
    }

    @ReactMethod
    public void stopBackgroundWork() {
        WorkManager.getInstance(mContext).cancelUniqueWork("testWork");
    }

    @Nonnull
    @Override
    public String getName() {
        return MODULE_NAME;
    }
}

The getName method should return the name of the module, so we can get this module in our JavaScript code as follows:

const { BackgroundWorkManager } = ReactNative.NativeModules;

Then, methods marked with @ReactMethod can be accessed using:

BackgroundWorkManager.method_name()

Finally, for our Native Module to work, we need to create a ReactPackage and add it to the list returned by getPackages() in MainApplication.java:

// BackgroundPackage.java
public class BackgroundPackage implements ReactPackage {
    @Nonnull
    @Override
    public List<NativeModule> createNativeModules(@Nonnull ReactApplicationContext reactContext) {
        List<NativeModule> modules = new ArrayList<>();
        modules.add(new BackgroundModule(reactContext));
        return modules;
    }

    @Nonnull
    @Override
    public List<ViewManager> createViewManagers(@Nonnull ReactApplicationContext reactContext) {
        return Collections.emptyList();
    }
}

In MainApplication.java, add the following:

protected List<ReactPackage> getPackages() {
  @SuppressWarnings("UnnecessaryLocalVariable")
  List<ReactPackage> packages = new PackageList(this).getPackages();
  // Packages that cannot be autolinked yet can be added manually here, for example:
  // packages.add(new MyReactNativePackage());
    packages.add(new BackgroundPackage());
  return 

Add the Headless Task Service

With our Native Module taken care of, let’s go ahead and add the Headless Task Service, which is the most important piece of the puzzle we’re trying to solve:

// BackgroundHeadlessTaskService.java
public class BackgroundHeadlessTaskService extends HeadlessJsTaskService {
    @Override
    protected @Nullable
    HeadlessJsTaskConfig getTaskConfig(Intent intent) {

        Bundle extras = intent.getExtras();
        if (extras != null) {
            return new HeadlessJsTaskConfig(
                    "BackgroundHeadlessTask",
                    Arguments.fromBundle(extras),
                    5000, // timeout for the task
                    true // optional: defines whether or not  the task is allowed in foreground. Default is false
            );
        }
        return null;
    }
}

The taskKey BackgroundHeadlessTask is the name of our Headless Task that we’ll register in the AppRegistry in React Native.

The optional property allowedInForeground is set to true, allowing us to run the task even when the app is in the foreground. Normally, this is not recommended because it could slow the UI down. However, we won’t perform any heavy tasks in our example application.

Now that our service is ready, we’ll add it to AndroidManifest.xml as follows:

&lt;service android:name="com.package_name.BackgroundHeadlessTaskService" />

Starting our Headless service

Now that the foundation of our application is ready, we only need to let WorkManager start our Headless service periodically. We’ll add the code to start our service in the doWork method of our BackgroundWorker:

public Result doWork() {

    Log.w("bg", "Worker do work");

    Bundle extras = bundleExtras();
    Intent service = new Intent(this.context, BackgroundHeadlessTaskService.class);
    service.putExtras(extras);

    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
        createChannel();
        this.context.startForegroundService(service);
    } else {
        this.context.startService(service);
    }
    return Result.success();
}

If the Android version is O or greater, we start a foreground service, which is because of the background restrictions introduced since Android v8.0.

startForegroundService expects that startForeground will be called within five seconds after the service has been started. Therefore, we have to make a small change in our BackgroundHeadlessTaskService:

@Override
protected @Nullable
HeadlessJsTaskConfig getTaskConfig(Intent intent) {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
        createChannel();
        Notification notification = new NotificationCompat.Builder(getApplicationContext(), "demo")
                .setContentTitle("Headless Work")
                .setTicker("runn")
                .setSmallIcon(R.mipmap.ic_launcher)
                .setOngoing(true)
                .build();
        startForeground(1, notification);
    }

    Bundle extras = intent.getExtras();
    if (extras != null) {
        return new HeadlessJsTaskConfig(
                "BackgroundHeadlessTask",
                Arguments.fromBundle(extras),
                5000, 
                true 
        );
    }
    return null;
}
@RequiresApi(Build.VERSION_CODES.O)
private void createChannel() {
    String description = "test channel";
    int importance = NotificationManager.IMPORTANCE_DEFAULT;
    NotificationChannel channel = new NotificationChannel("demo", "test", importance);
    channel.setDescription(description);
    NotificationManager notificationManager =
            (NotificationManager) getApplicationContext().getSystemService(NOTIFICATION_SERVICE);

    notificationManager.createNotificationChannel(channel);

} 

Starting from Oreo, Android poses restrictions on background services. So, our WorkManager cannot start a background service. However, Android does allow us to start a foreground service from the background. For a service to be called foreground, we have to add a notification that informs the user of work going on in the background.

Now, we need to create the task itself. Head over to your JavaScript code and create a file called BackgroundHeadlessTask.js:

// BackgroundHeadlessTask.js
module.exports = async (taskData) => {
        console.log("Hey this is a headless JS task");
        await fetch(`http://192.168.1.7:3000/test-request`);
        return;
};

If you have a Node.js server running on your development machine, you can create a fetch call and wait for it to complete like I did.

Now, when a button is pressed, we can call the methods we created in our Native Module as follows:

onStartBtnPress = async () => {
    NativeModules.BackgroundWorkManager.startBackgroundWork();
  };
onCancelBtnPress = async () => {
    NativeModules.BackgroundWorkManager.stopBackgroundWork();
  };

The code above will enqueue the work request and run the task periodically for us until the cancel button is pressed. You can check Logcat to verify if the background task is running periodically at all:

Logcat Verify Background Task Running
Logs like this should appear periodically

Logcat Verify Logs Running

If you set up a local Echo server, you should be able to see the logs when the GET request in your Headless task succeeds:

Echo Server Get Request

Conclusion

With the help of Headless JS, Native Modules, and WorkManager, it is fairly easy to create and run JavaScript tasks in React Native. In this article, we covered from beginning to end setting up and running Headless JS in React Native Android, allowing us to run tasks in the background of our application. I hope you enjoyed this tutorial!

LogRocket: Instantly recreate issues in your React Native apps.

LogRocket is a React Native monitoring solution that helps you reproduce issues instantly, prioritize bugs, and understand performance in your React Native apps.

LogRocket also helps you increase conversion rates and product usage by showing you exactly how users are interacting with your app. LogRocket's product analytics features surface the reasons why users don't complete a particular flow or don't adopt a new feature.



Start proactively monitoring your React Native apps — .

Debjyoti Banerjee I'm Debjyoti, software engineer and game developer. Currently exploring JavaScript and Flutter, and trying to come up with solutions to problems in the healthcare sector. Love open source.

6 Replies to “Run React Native background tasks with Headless JS”

  1. Where is the BackgroundWorker function created? Are you able to provide a sample of the project structure?

  2. please add import statements of classes. or provide the sample example repo link. android studio not importing automatically.

    1. I’m not aware of your requirements but according to the Android docs, it recommends high priority FCM messages as they are exempt from background restrictions.
      Also, you could try updating your app logic to do something in the work manager but not start a foreground service.
      You could also look into AlarmManager if that suits your use case.

  3. The lack of import statements makes this an hours-long task to reproduce. On top of that, there’s several functions here that just… don’t exist. `bundleExtras` and createChannel` are completely undefined and I can’t find anything online to replace them with or import them from. Kind of useless

Leave a Reply