Adding background tasks to your Expo managed React Native app with BackgroundFetch

Bjornar Hvidsten
6 min readFeb 29, 2020

Evolution

Expo is still in beta (as of february 2020), but is a great tool for quick and easy deployment of React Native apps, especially when just starting with React Native. Though there are still a few reasons to eject Expo and “go Native” when getting more proficient and want access to yet unsupported APIs, I really enjoy and appreciate the workflow with Expo. Not long ago you had to eject your Expo managed project to get background task functionality, but BackgroundFetch can now perform background fetch tasks for your app by using the Native API module TaskManager behind the scene. I’m quite new to React Native and didn’t find any articles about this particular setup at the time of writing, so I share my way of implementing BackgroundFetch in a project. I would recommend to have at least some experience with Javascript and React to follow along, but that is probably the case if you’re reading this article. So let’s jump right into the setup and coding…

Installation

The prerequisite needed to follow along is to get Expo and React Native installed on your system or in your project. If you need assistance installing any of these then follow the steps in this section which will set up a new Expo managed React Native project.

Note: If you don’t have NodeJS installed it can be downloaded here, though devs using React should have this on their system already.

  1. Run the following command in terminal to install the Expo CLI:
npm install -g expo-cli

2. Next up is to set up the React Native project. This will create a folder named ‘MyProject’ in the folder where you run the command, so choose a folder name that you want for your project. It will also ask a few questions about what kind of React Native app you want to create. Choose one of the options from Managed workflow which is blank, blank (Typescript) or tabs to create a Expo managed project. The tabs option will create an example project with a couple of screens if you don’t want to start from scratch.

expo init MyProject

3. Navigate to the project folder and start the dev server:

cd MyProject
npm start

Note: if Visual Code is your preferred editor you can open it with code .’ after navigating to the project folder, and then run the project from the integrated terminal with ‘npm start’.

4. Install the npm and dependency for BackgroundFetch with this command:

expo install expo-background-fetch

This will install the npms ‘expo-background-fetch’ and it’s dependency ‘expo-task-manager’.

To avoid linting warnings in some setups also run this command (or add ‘expo-task-manager’ to the dependencies in package.json manually):

npm i -S expo-task-manager

5. If you are going to use BackgroundFetch API in standalone and detached apps on iOS, you need to include background mode in the Info.plist file.
Add this to the “infoPlist” section in the app.json file (in your project root folder):

"UIBackgroundModes": [
"location",
"fetch"
]

The “ios” section in the app.json file will then look something like this (though it might have a few other key/value pairs in there as well):

"ios": {
"infoPlist": {
"UIBackgroundModes": [
"location",
"fetch"
]
}
}

The “fetch” value is what is needed here for setting up a task, though in case you will use background location later you can leave it there like I did.

The Expo client on Android will work without and changes, but on iOS you will also have to test this app using a custom Expo client.

The code

So on to the coding part…

I added the code to App.js since it needs to run in the initialization phase.

First import the needed modules:

import * as BackgroundFetch from 'expo-background-fetch';
import * as TaskManager from 'expo-task-manager';

Then create a task function outside of the App component. This will be used in the task you soon will define and register:

function myTask() {
try {
// fetch data here...
const backendData = "Simulated fetch " + Math.random();
console.log("myTask() ", backendData);
return backendData
? BackgroundFetch.Result.NewData
: BackgroundFetch.Result.NoData;
} catch (err) {
return BackgroundFetch.Result.Failed;
}
}

The important thing to remember here is that you should return a predefined value based on the result of the fetch (or whatever your code do in the task function). Here is the description from the docs:

…your task should return a value that best describes the results of your background fetch work.

BackgroundFetch.Result.NoData - There was no new data to download.
BackgroundFetch.Result.NewData - New data was successfully downloaded.
BackgroundFetch.Result.Failed - An attempt to download data was made but that attempt failed.

This return value is to let iOS know what the result of your background fetch was, so the platform can better schedule future background fetches. Also, your app has up to 30 seconds to perform the task, otherwise your app will be terminated and future background fetches may be delayed.

You can now define the task with a identifier of your choice and your task function like this:
TaskManager.defineTask(“myTaskName”, myTask);
and then register the task with:
BackgroundFetch.registerTaskAsync(“myTaskName”, options);

To avoid breaking the live reload mechanism you have to check if the task already has been defined with TaskManager.isTaskDefined(). This method is not in the docs, but I found this post while investigating a “TaskManager.defineTask must be called during initialization phase!” error in the console when saving React files and triggering reload. You might still get warnings though if changing the background task code.

Since BackgroundFetch.registerTaskAsync() returns a promise I wrapped it in an async function. The promise resolves once the task is registered and rejects in case of any errors. The default interval is 15 minutes if ‘minimumInterval’ is not specified, for other options check this link.

async function initBackgroundFetch(taskName,
taskFn,
interval = 60 * 15) {
try {
if (!TaskManager.isTaskDefined(taskName)) {
TaskManager.defineTask(taskName, taskFn);
}
const options = {
minimumInterval: interval // in seconds
};
await BackgroundFetch.registerTaskAsync(taskName, options);
} catch (err) {
console.log("registerTaskAsync() failed:", err);
}
}

Then finally you call initBackgroundFetch() outside the App component, since TaskManager.defineTask must be called during the initialization phase as mentioned:

initBackgroundFetch('myTaskName', myTask, 5);

And that’s it for BackgroundFetch to start executing your task function when the app is in the background. The interval is not exact though, it will trigger at a quite wide range of intervals. I used 5 seconds in this example, but it usually takes a bit more time.

Updating state from the background task

One more thing I would like to share is the method I used for updating state inside the App component from the background task function. This is after all a common thing to do in React. Here I describe the setup for a functional component, though it would be somewhat similar for a class based component.

First define a global function outside the App component, e.g. like this:

let setStateFn = () => {
console.log("State not yet initialized");
};

Then below the line where you declare/define your state variables with useState() you can now assign the setState function to setStateFn.

const [state, setState] = useState(null);
setStateFn = setState;

You can now use setStateFn() inside the task function like this (even if it’s outside a component) to update the state of the App component:

function myTask() {
try {
// fetch data here...
const backendData = "Simulated fetch " + Math.random();
console.log("myTask() ", backendData);
setStateFn(backendData);
return backendData
? BackgroundFetch.Result.NewData
: BackgroundFetch.Result.NoData;
} catch (err) {
return BackgroundFetch.Result.Failed;
}
}

Here is all the background fetch code from above:

import * as BackgroundFetch from "expo-background-fetch";
import * as TaskManager from "expo-task-manager";
let setStateFn = () => {
console.log("State not yet initialized");
};
function myTask() {
try {
// fetch data here...
const backendData = "Simulated fetch " + Math.random();
console.log("myTask() ", backendData);
setStateFn(backendData);
return backendData
? BackgroundFetch.Result.NewData
: BackgroundFetch.Result.NoData;
} catch (err) {
return BackgroundFetch.Result.Failed;
}
}
async function initBackgroundFetch(taskName,
taskFn,
interval = 60 * 15) {
try {
if (!TaskManager.isTaskDefined(taskName)) {
TaskManager.defineTask(taskName, taskFn);
}
const options = {
minimumInterval: interval // in seconds
};
await BackgroundFetch.registerTaskAsync(taskName, options);
} catch (err) {
console.log("registerTaskAsync() failed:", err);
}
}
initBackgroundFetch('myTaskName', myTask, 5);// Put the next lines inside the React component
const [state, setState] = useState(null);
setStateFn = setState;

That’s it for now, may it help many devs out there as I daily learn from other dev’s articles both here on Medium and elsewhere…

--

--

Bjornar Hvidsten

Love grounded spirituality & metaphysics, play the guitar, enjoy full stack web/app development, mainly JavaScript/Node/React/React Native. Ha’atu!