
Building a habit tracking app is a fun challenge. For my latest project, I decided to gamify the experience with a UI inspired by dating apps, swipe left to skip a habit, swipe right to complete it.
It was all smooth sailing until I implemented "Duration Habits" (e.g., "Medidate for 20 minutes"). The UI looked great, but I immediately hit a classic React Native roadblock:
The moment the user locked their phone or switched to another app, the Javascript thread paused, and my timer stopped dead.
For a meditation or workout timer, this is broken functionality. The user expects the timer to alert them when they are done, regardless of whether the app is open.
In this post, I'll walk through how I solved this using Android Foreground Services via react-native-background-actions, and crucially, how I synchronized that background process with my modern state management solution, Zustand.
Mobile operating systems are aggresive about saving battery. When an app moves to the background, iOS and Android will suspend its Javascript execution thread almost immediately.
To keep code running, we need a special permit from the OS. On Android, this called a Foreground Service. It tells the OS, "Hey, I'm doing something important that the user is aware of, please don't kill me." This requires showing a persistent notification in the status bar so the user knows the app is still active.
The Stack
first, get the library installed:
You cannot skip this step. You need to declare the necessary permission and the service itself in your android/app/src/main/AndroidManifest.xml file. Without this, the app will crash when you try to start the background task.
Open AndroidManifest.xml and add these lines inside the <manifest> tag and <application> tag respectively:
This is where most developers get stuck.
A background task created by this library runs in a Javascript context that is separate from your React Component lifecyle.
Crucial realization: You cannot use standard React Hooks like useState, useEffect, or custom Zustand hooks like useHabitStore() inside the background task loop. They won't update or react as you expect.
Since we are using Zustand, we have an escape hatch. Instead of using reactive hook inside the background loop, we will access the store directly and imperatively using useHabitStore.getState().
This is the heart of the operation. This function defines what happens while the app is in the background. It's essentially an infinite loop that only breaks when the timer finishes or the user pauses it in the UI.
Here is the real-world code adapted for a timer:
Now we need to connect our React Native UI to this background task. When the user taps "Start Timer" on a habit card, the service should spin up. When they tap "Pause", it should tear down.
We use useEffect within our timer component to monitor changes in our Zustand state and react accordingly.
If you are implementing this in your own app, keep these lessons in mind:
store.getState() and store.dispatch()) to read and write data inside the background loop.BackgroundService.updateNotification allows you to make the notification feel "alive." Updating the progress bar and timer text every second assures the user that the app hasn't crashed while in their pocket.useEffect dependency array is crucial. When timerIsRunning flips to false in your UI, you must call BackgroundService.stop(). Otherwise, you'll leave "ghost processes" running that drain battery and annoy users.Date.now() subtraction rather than just incrementing a counter like elapsed++. Using system time differences ensures that even if the OS briefly pauses execution, the timer calculation remains accurate when it wakes back up.This approach provided the robust "set it and forget it" experience my habit tracker needed, matching the sleekness of the swipe-based UI.