npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

rn-persistent-timer

v1.1.0

Published

React Native timers that keep running in the foreground, background, and even after the app is killed — with TypeScript support.

Readme

rn-persistent-timer

React Native timers that keep running — in the foreground, background, and even after the app is killed.
Full TypeScript support. Zero bundled native dependencies.

npm version npm downloads license platform TypeScript React Native


✨ Features

| Feature | Foreground | Background | Killed App | |---|:---:|:---:|:---:| | JS-only timer | ✅ | ❌ | ❌ | | Background timer | ✅ | ✅ | ❌ | | Kill-proof timer | ✅ | ✅ | ✅ |

  • 🕐 Stopwatch (count up) and Countdown (count down to zero)
  • 🔔 Persistent Android notification with pause/resume actions (Foreground Service)
  • 📱 iOS background task via UIBackgroundTaskIdentifier + BGTaskScheduler
  • 💾 Killed-state persistence via AsyncStorage — restores elapsed time on next open
  • 🎣 React Hook (usePersistentTimer) + Imperative class (PersistentTimerManager)
  • 📦 Full TypeScript — types included, no @types/ package needed
  • 🔄 Multiple simultaneous timers with independent configs
  • Zero external JS dependencies beyond @react-native-async-storage/async-storage

📋 Table of Contents

  1. Installation
  2. Android Setup
  3. iOS Setup
  4. Quick Start
  5. Usage Examples
  6. API Reference
  7. TypeScript Types
  8. Troubleshooting

📦 Installation

# npm
npm install rn-persistent-timer @react-native-async-storage/async-storage

# yarn
yarn add rn-persistent-timer @react-native-async-storage/async-storage

Note: @react-native-async-storage/async-storage is required for killed-state persistence.


🤖 Android Setup

1. Add the Package to your MainApplication

MainApplication.kt (Kotlin — React Native 0.71+):

import com.rnpersistenttimer.RNPersistentTimerPackage

override fun getPackages(): List<ReactPackage> = listOf(
    MainReactPackage(),
    RNPersistentTimerPackage(),   // ← add this
)

MainApplication.java (Java):

import com.rnpersistenttimer.RNPersistentTimerPackage;

@Override
protected List<ReactPackage> getPackages() {
    return Arrays.asList(
        new MainReactPackage(),
        new RNPersistentTimerPackage()  // ← add this
    );
}

2. Register the Foreground Service in AndroidManifest.xml

<!-- Inside <application> tag -->
<service
    android:name="com.rnpersistenttimer.TimerForegroundService"
    android:foregroundServiceType="dataSync"
    android:exported="false" />

<!-- Inside <manifest> tag (before <application>) -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />

Android 13+ (API 33+): You must request POST_NOTIFICATIONS permission at runtime before starting a timer with notifications.

3. Request Notification Permission (Android 13+)

import { PermissionsAndroid, Platform } from 'react-native';

async function requestNotificationPermission(): Promise<void> {
  if (Platform.OS === 'android' && Platform.Version >= 33) {
    await PermissionsAndroid.request(
      PermissionsAndroid.PERMISSIONS.POST_NOTIFICATIONS,
    );
  }
}

🍎 iOS Setup

1. Add Capability: Background Modes

In Xcode:

  1. Select your project target → Signing & Capabilities
  2. Click + Capability → add Background Modes
  3. Enable ✅ Background fetch and ✅ Background processing

2. Register BGTask Identifier in Info.plist

<key>BGTaskSchedulerPermittedIdentifiers</key>
<array>
    <string>com.yourapp.timer</string>
</array>

Replace com.yourapp.timer with your actual bundle ID prefix.

3. Update RNPersistentTimer.m

Open ios/RNPersistentTimer.m and replace the identifier string:

// Find this line in scheduleBGTask:
[[BGAppRefreshTaskRequest alloc] initWithIdentifier:@"com.yourapp.timer"];

// Replace with your actual bundle ID:
[[BGAppRefreshTaskRequest alloc] initWithIdentifier:@"com.YourApp.timer"];

4. Install Pods

cd ios && pod install && cd ..

⚡ Quick Start

import React from 'react';
import { View, Text, TouchableOpacity } from 'react-native';
import { usePersistentTimer } from 'rn-persistent-timer';

export default function StopwatchScreen() {
  const { snapshot, start, pause, resume, reset, isRunning, isPaused } =
    usePersistentTimer({
      timerId: 'my-stopwatch',    // unique ID — required
      mode: 'stopwatch',
      runInBackground: true,       // keeps ticking when app is backgrounded
    });

  return (
    <View>
      <Text>{snapshot.formattedElapsed}</Text>   {/* "HH:MM:SS" */}
      <Text>State: {snapshot.state}</Text>

      {!isRunning && !isPaused && (
        <TouchableOpacity onPress={start}>
          <Text>▶ Start</Text>
        </TouchableOpacity>
      )}
      {isRunning && (
        <TouchableOpacity onPress={pause}>
          <Text>⏸ Pause</Text>
        </TouchableOpacity>
      )}
      {isPaused && (
        <TouchableOpacity onPress={resume}>
          <Text>▶ Resume</Text>
        </TouchableOpacity>
      )}
      <TouchableOpacity onPress={reset}>
        <Text>⟳ Reset</Text>
      </TouchableOpacity>
    </View>
  );
}

📖 Usage Examples

Stopwatch — Foreground Only

const timer = usePersistentTimer({
  timerId: 'foreground-sw',
  mode: 'stopwatch',
  runInBackground: false,
  runInKilledState: false,
});

Stopwatch — Keeps Running in Background

const timer = usePersistentTimer({
  timerId: 'bg-stopwatch',
  mode: 'stopwatch',
  runInBackground: true,
  showNotification: true,
  notification: {
    title: '⏱ Stopwatch Running',
    body: 'Elapsed: {time}',   // {time} is replaced automatically
    color: '#4CAF50',
    showActions: true,          // adds Pause/Resume buttons to notification
  },
  onBackground: (snap) => console.log('Backgrounded at', snap.formattedElapsed),
  onForeground: (snap) => console.log('Foregrounded at', snap.formattedElapsed),
});

Kill-Proof Timer (Survives App Kill)

import { Alert } from 'react-native';

const timer = usePersistentTimer({
  timerId: 'kill-proof',
  mode: 'stopwatch',
  runInBackground: true,
  runInKilledState: true,    // ← enables AsyncStorage persistence
  onRestore: (snap) => {
    Alert.alert('Welcome back!', `Timer restored to ${snap.formattedElapsed}`);
  },
});

Countdown Timer

const timer = usePersistentTimer({
  timerId: 'workout-countdown',
  mode: 'countdown',
  duration: 5 * 60,          // 5 minutes in seconds
  runInBackground: true,
  runInKilledState: true,
  notification: {
    title: '⏳ Workout Timer',
    body: '{time} remaining',
    color: '#F44336',
  },
  onComplete: () => Alert.alert('Time\'s up! 🎉'),
});

// Use snapshot.formattedRemaining for countdown display
// Use snapshot.progress (0→1) for a progress bar
console.log(timer.snapshot.formattedRemaining); // "04:59"
console.log(timer.snapshot.progress);           // 0.003...

Auto-Pause on Background (Game Timer)

const timer = usePersistentTimer({
  timerId: 'game-timer',
  mode: 'stopwatch',
  pauseOnBackground: true,   // auto-pauses when app is backgrounded
  runInKilledState: false,
});

Multiple Concurrent Timers

const workTimer    = usePersistentTimer({ timerId: 'work',    mode: 'countdown', duration: 25 * 60, runInBackground: true });
const breakTimer   = usePersistentTimer({ timerId: 'break',   mode: 'countdown', duration:  5 * 60, runInBackground: true });
const sessionTimer = usePersistentTimer({ timerId: 'session', mode: 'stopwatch',                    runInBackground: true });

Imperative API — Outside React (Redux, MobX, Services)

import { PersistentTimerManager } from 'rn-persistent-timer';

const manager = new PersistentTimerManager({
  timerId: 'service-timer',
  mode: 'stopwatch',
  runInBackground: true,
  runInKilledState: false,
});

manager.on('onTick', (snap) => {
  console.log('Tick:', snap.formattedElapsed);
  reduxStore.dispatch(updateTimer(snap));
});

manager.on('onComplete', (snap) => {
  console.log('Done!');
});

manager.start();
// Later...
manager.pause();
manager.resume();
manager.reset();
manager.destroy(); // Always call when done to prevent memory leaks

Platform Support Check

import {
  isBackgroundTimerSupported,
  isKilledStateTimerSupported,
} from 'rn-persistent-timer';

useEffect(() => {
  isBackgroundTimerSupported().then((supported) => {
    console.log('Background timer supported:', supported);
  });
  isKilledStateTimerSupported().then((supported) => {
    console.log('Kill-state timer supported:', supported);
  });
}, []);

Utility Functions

import { formatTime, parseTime } from 'rn-persistent-timer';

formatTime(3661);      // → "01:01:01"
formatTime(59);        // → "00:00:59"
parseTime('01:30:00'); // → 5400
parseTime('05:30');    // → 330

📚 API Reference

usePersistentTimer(config)

React hook. Returns UsePersistentTimerReturn.

| Prop | Type | Default | Description | |---|---|---|---| | timerId | string | required | Unique timer ID | | mode | 'stopwatch' \| 'countdown' | 'stopwatch' | Timer direction | | duration | number | 0 | Seconds for countdown | | runInBackground | boolean | true | Continue when app is backgrounded | | runInKilledState | boolean | false | Restore after app kill | | interval | number | 1000 | Tick interval (ms) | | showNotification | boolean | true | Show Android notification | | notification | AndroidNotificationConfig | {} | Notification options | | pauseOnBackground | boolean | false | Auto-pause on background | | resetOnForeground | boolean | false | Auto-reset when returned from kill |

Callbacks (all optional):

| Callback | Signature | Description | |---|---|---| | onTick | (snap: TimerSnapshot) => void | Every tick | | onStart | (snap: TimerSnapshot) => void | Timer started | | onPause | (snap: TimerSnapshot) => void | Timer paused | | onResume | (snap: TimerSnapshot) => void | Timer resumed | | onReset | (snap: TimerSnapshot) => void | Timer reset | | onComplete | (snap: TimerSnapshot) => void | Countdown finished | | onBackground | (snap: TimerSnapshot) => void | App went to background | | onForeground | (snap: TimerSnapshot) => void | App returned to foreground | | onRestore | (snap: TimerSnapshot) => void | State restored after kill | | onError | (error: Error) => void | Internal error |

Return value:

| Field | Type | Description | |---|---|---| | snapshot | TimerSnapshot | Reactive timer state | | isRunning | boolean | state === 'running' | | isPaused | boolean | state === 'paused' | | isCompleted | boolean | state === 'completed' | | appState | AppState | Current app state | | start() | () => void | Start the timer | | pause() | () => void | Pause the timer | | resume() | () => void | Resume from pause | | reset() | () => void | Stop and reset to 0 | | stop() | () => void | Stop without reset | | getSnapshot() | () => TimerSnapshot | Synchronous snapshot | | destroy() | () => void | Clean up all listeners |


PersistentTimerManager

Class-based API for use outside of React components.

const manager = new PersistentTimerManager(config: TimerConfig);

manager.start();
manager.pause();
manager.resume();
manager.reset();
manager.stop();
manager.getSnapshot(): TimerSnapshot;
manager.destroy();

manager.on(event, handler);
manager.off(event, handler);

// Static: restore from killed state
const restored = await PersistentTimerManager.restore(timerId);

TimerSnapshot

interface TimerSnapshot {
  timerId: string;
  elapsed: number;             // seconds since start
  remaining: number | null;    // seconds left (countdown only)
  state: TimerState;           // 'idle' | 'running' | 'paused' | 'completed'
  appState: AppState;          // 'foreground' | 'background' | 'killed'
  startedAt: number | null;    // unix ms when started
  pausedAt: number | null;     // unix ms when paused
  formattedElapsed: string;    // "HH:MM:SS"
  formattedRemaining: string | null;  // "HH:MM:SS" (countdown only)
  progress: number | null;     // 0→1 (countdown only)
}

AndroidNotificationConfig

interface AndroidNotificationConfig {
  title?: string;       // Notification title
  body?: string;        // Body text; use {time} for the current timer value
  channelId?: string;   // Notification channel ID
  channelName?: string; // Channel display name
  icon?: string;        // Drawable resource name
  color?: string;       // Hex accent color, e.g. '#FF5733'
  showTime?: boolean;   // Show time in notification
  showActions?: boolean;// Show Pause/Resume action buttons
}

🔧 Troubleshooting

Native module not found warning

[rn-persistent-timer] Native module not found.

iOS: Run npx pod-install (or cd ios && pod install) then rebuild.
Android: Clean and rebuild the Android project: cd android && ./gradlew clean && cd ...

Foreground (JS-only) timers still work without the native module.


Timer stops in background on Android

  1. Ensure TimerForegroundService is registered in AndroidManifest.xml.
  2. Check that POST_NOTIFICATIONS permission is granted on Android 13+.
  3. Set showNotification: true — Android 8+ requires a visible notification for foreground services.
  4. Some OEM ROMs (Xiaomi, OPPO, etc.) aggressively kill background processes. Guide users to whitelist your app in battery settings.

Timer stops in background on iOS

  1. Confirm Background Modes capability is added in Xcode.
  2. Verify BGTaskSchedulerPermittedIdentifiers is set in Info.plist.
  3. iOS limits background execution to ~30 seconds unless using BGTaskScheduler. The module handles this automatically via UIBackgroundTaskIdentifier.

Timer not restoring after app kill

  1. Ensure runInKilledState: true in your config.
  2. Confirm @react-native-async-storage/async-storage is installed and linked.
  3. Rebuild the project after installing async-storage.

TypeScript errors

Ensure your tsconfig.json has:

{
  "compilerOptions": {
    "moduleResolution": "bundler",
    "strict": true
  }
}

📁 Project Structure

rn-persistent-timer/
├── src/
│   ├── index.ts                   # Public API barrel
│   ├── types.ts                   # All TypeScript types
│   ├── NativeTimerModule.ts       # JS ↔ Native bridge
│   ├── PersistentTimerManager.ts  # Core timer class
│   ├── usePersistentTimer.tsx     # React hook
│   └── utils.ts                   # Utility functions
├── android/
│   ├── build.gradle
│   └── src/main/java/com/rnpersistenttimer/
│       ├── RNPersistentTimerModule.java
│       ├── RNPersistentTimerPackage.java
│       └── TimerForegroundService.java
├── ios/
│   ├── RNPersistentTimer.h
│   └── RNPersistentTimer.m
├── example/
│   └── App.tsx                    # 8-scenario demo app
└── lib/                           # Built output (generated)
    ├── commonjs/
    ├── module/
    └── typescript/

🚀 Running the Example App

# 1. Clone or navigate to the package
cd /path/to/rn-persistent-timer

# 2. Install dependencies
yarn install

# 3. Install example dependencies
cd example && yarn install && cd ..

# 4. iOS — install pods
cd example/ios && pod install && cd ../..
npx react-native run-ios

# 5. Android
npx react-native run-android

📤 Building & Publishing to npm

# 1. Install dependencies
yarn install

# 2. Build the TypeScript output
yarn build      # runs react-native-builder-bob

# 3. Verify types compile
yarn typecheck   # runs tsc --noEmit

# 4. Publish
npm publish --access public

📄 License

MIT © Vipin Jaiswal


🙏 Contributing

PRs and issues welcome! Please open an issue before starting a large change.

  1. Fork the repo
  2. Create your feature branch: git checkout -b feat/my-feature
  3. Commit changes: git commit -m 'feat: add my feature'
  4. Push: git push origin feat/my-feature
  5. Open a Pull Request