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:
| Layer | Responsibility |
|---|---|
| Platform fetchers | Retrieve raw heart rate samples |
| Processing pipeline | Compute weighted averages and confidence |
| Background entrypoint | Android-safe background access |
| React hook | Expose 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
| Field | Type | Description |
|---|---|---|
average | number | Time-weighted average BPM |
hasFullWindow | boolean | Indicates 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:
| Platform | Data Source |
|---|---|
| iOS | HealthKit |
| Android | Health Connect |
Runtime routing uses:
Platform.OS
7. UI State Is Always Explicit
The React hook guarantees explicit state tracking for:
| State | Type |
|---|---|
result | HeartRateResult | null |
isLoading | boolean |
error | string | 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
| Property | Type | Description |
|---|---|---|
result | HeartRateResult | null | Processed HR result |
isLoading | boolean | Active fetch state |
error | string | null | Latest 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);
}