hooks/useHeartrate.ts

Purpose

The useHeartRate module provides a cross-platform heart rate retrieval and aggregation system for React Native applications.

It is responsible for:

  • reading heart rate samples from platform health APIs
  • computing a time-weighted rolling heart rate average
  • validating recency and coverage quality of sensor data
  • exposing asynchronous loading/error state
  • supporting both foreground and background collection flows

The implementation integrates with:

  • Health Connect on Android
  • Apple HealthKit on iOS

Architecture Overview

The module contains four major layers:

LayerResponsibility
Platform fetchersRetrieve raw heart rate samples
Processing pipelineCompute weighted averages and confidence
Background entrypointAndroid-safe background access
React hookExpose reactive UI state

The processing model is based on:

time-weighted averaging

rather than naive arithmetic averaging.

This accounts for unequal durations between heart rate readings.


Data Model

HeartRateSample

Represents one raw heart rate sample.

type HeartRateSample = {
  value: number;
  startDate: string;
  endDate: string;
};

HeartRateResult

Represents processed heart rate statistics.

export type HeartRateResult = {
  average: number;
  hasFullWindow: boolean;
};

Fields

FieldTypeDescription
averagenumberTime-weighted average BPM
hasFullWindowbooleanIndicates sufficient data coverage

Invariants

The module maintains the following invariants.


1. Heart Rate Is Computed Over a Fixed Window

The effective analysis window is:

WINDOW_MS = 40 * 60 * 1000

which equals:

40 minutes

The request window is slightly larger:

REQUEST_WINDOW_MS = 50 * 60 * 1000

to allow overlap and ensure adequate sample coverage.


2. Averages Are Time-Weighted

The module does not compute a simple arithmetic mean.

Instead:

sample value × active duration

is accumulated for each interval.

Formula:

weighted average =
  total(value × duration) / total(duration)

This ensures long-duration samples contribute proportionally more.


3. Samples Are Chronologically Ordered Before Processing

Before computing averages:

.sort(...)

orders samples ascending by start time.

This guarantees:

  • deterministic interval calculation
  • valid duration computation
  • stable weighted averaging

4. Stale Data Invalidates Results

If the newest heart rate reading is older than:

MAX_LAST_READING_AGE_MS = 10 minutes

the computation returns:

{
  average: 0,
  hasFullWindow: false
}

This prevents stale sensor streams from being treated as active telemetry.


5. Coverage Confidence Determines Window Validity

A result only reports:

hasFullWindow = true

when covered sample duration exceeds:

WINDOW_CONFIDENCE = 0.95

or:

95% of the target window

This prevents sparse sampling from being treated as statistically complete.


6. Processing Is Platform-Specific

The module routes retrieval through:

PlatformData Source
iOSHealthKit
AndroidHealth Connect

Runtime routing uses:

Platform.OS

7. UI State Is Always Explicit

The React hook guarantees explicit state tracking for:

StateType
resultHeartRateResult | null
isLoadingboolean
errorstring | null

Variants

Platform Variants

Android Variant

Uses:

  • initialize()
  • requestPermission()
  • readRecords()

from Health Connect integrations.


iOS Variant

Uses:

  • requestAuthorization()
  • queryQuantitySamples()

from HealthKit integrations.


Runtime Variants

Foreground Fetch

The React hook:

useHeartRate()

supports UI-driven fetch workflows.


Background Fetch

The standalone function:

fetchAndroidHRBackground()

supports Android background execution contexts.


Coverage Variants

Full Window Coverage

hasFullWindow = true

indicates near-complete telemetry coverage.


Partial Coverage

hasFullWindow = false

indicates sparse or incomplete sensor data.


Exported Functions

useHeartRate()

export const useHeartRate = ()

Primary React hook for fetching and tracking heart rate state.


Returned Properties

PropertyTypeDescription
resultHeartRateResult | nullProcessed HR result
isLoadingbooleanActive fetch state
errorstring | nullLatest fetch error
fetchHeartRate() => Promise<void>Starts retrieval

Responsibilities

The hook:

  • manages async loading state
  • routes platform-specific fetches
  • requests permissions
  • captures errors
  • stores processed averages

Example Usage

const {
  result,
  isLoading,
  error,
  fetchHeartRate
} = useHeartRate();

Example Result

{
  average: 72,
  hasFullWindow: true
}

Exported Functions

fetchAndroidHRBackground()

export async function fetchAndroidHRBackground()

Background-safe Android heart rate fetch helper.


Responsibilities

  • verifies platform compatibility
  • safely initializes Health Connect
  • suppresses initialization failures
  • computes rolling averages

Return Type

Promise<HeartRateResult | null>

Failure Behavior

Returns:

null

when:

  • Health Connect is unavailable
  • initialization fails
  • running on unsupported platforms

Example

const result = await fetchAndroidHRBackground();

Internal Functions

computeTimeWeightedAverage(samples, windowStart)

Core processing pipeline for weighted average calculation.


Responsibilities

  • sorts samples chronologically
  • computes sample durations
  • accumulates weighted averages
  • measures window coverage
  • validates recency

Coverage Formula

Coverage duration is accumulated through:

effectiveEnd - effectiveStart

Recency Validation

Latest sample age:

now - lastSampleTime

must not exceed:

10 minutes

fetchAndroid(startTime)

Android-specific Health Connect fetch implementation.

Responsibilities

  • initializes Health Connect
  • reads HR records
  • maps records into normalized samples
  • computes weighted averages

fetchIOS(startTime)

iOS-specific HealthKit fetch implementation.

Responsibilities

  • requests authorization
  • queries HR samples
  • normalizes HealthKit data
  • computes weighted averages

Health Platform Integrations

Android Health Connect

Uses:

readRecords('HeartRate')

with bounded time filtering.


iOS HealthKit

Uses:

queryQuantitySamples(...)

with:

count/min

as the BPM unit.


Logging Behavior

The module emits structured logs for:

  • authorization flow
  • query lifecycle
  • sample counts
  • stale data detection
  • average completion

Examples:

Heart rate: querying samples
Returning Average

Processing Pipeline

High-Level Flow

fetch samples
  → normalize records
  → sort chronologically
  → compute durations
  → compute weighted average
  → validate coverage
  → return HeartRateResult

Error Handling Strategy

The hook captures and stores errors through:

setError(String(e))

Background fetches instead suppress failures and return:

null

to avoid crashing headless tasks.


Concurrency Notes

The module does not cancel overlapping fetch requests.

Concurrent invocations may:

  • overlap platform queries
  • overwrite UI state
  • resolve out of order

Consumers requiring strict synchronization should serialize fetch calls externally.


Performance Notes

The weighted averaging pipeline:

  • sorts samples in memory
  • iterates linearly through intervals
  • scales approximately O(n log n)

The query limit:

limit: 1000

bounds worst-case processing size.


Example Integration

const { result, fetchHeartRate } = useHeartRate();

await fetchHeartRate();

if (result?.hasFullWindow) {
  console.log(result.average);
}