In the rapidly growing health tech space, users expect their apps to seamlessly connect with their primary health ecosystems—Apple Health and Google Fit. For React Native developers, this presents a unique challenge: how do you create a single, elegant solution for two very different native platforms? This tutorial will guide you through building a robust, unified API to read and write user health data securely.
We will build a React Native application that can:
- Request user permission to access health data.
- Read daily step counts and recent heart rate samples.
- Write a completed workout session to the native health store.
- Abstract platform-specific logic into a unified service.
This guide is for developers with a basic understanding of React Native. We will be working with a bare React Native setup to have full control over the native project configurations.
Why this matters to developers:
- Enhanced User Experience: Integrating with the OS-level health store provides a seamless experience for users, who can see all their health data in one place.
- Data-Rich Applications: Leverage valuable health data to provide personalized insights, coaching, and features.
- Increased App Stickiness: Apps that become part of a user's daily health routine are more likely to be retained.
Understanding the Problem
Apple HealthKit (iOS) and Google Fit's new replacement, Health Connect (Android), are powerful but fundamentally different APIs. A naive implementation would lead to tangled, platform-specific code littered throughout your app.
- Platform Divergence: On iOS, we use
react-native-health, a mature library for HealthKit. On Android, the landscape has shifted from the Google Fit API to the more modern and privacy-centric Health Connect API, for which we'll usereact-native-health-connect. - Permission Models: Each platform has its own specific workflow for requesting permissions, which must be handled gracefully.
- Data Structures: The way data is structured and queried is different between HealthKit and Health Connect.
Our approach is to create a "Health Service" abstraction layer. This layer will expose a set of simple, unified methods (e.g., getStepCount(), saveWorkout()) to the rest of our application. Internally, it will detect the operating system and call the appropriate native library.
Prerequisites
- Node.js and Watchman: Ensure you have Node.js (LTS version) and Watchman installed.
- React Native Environment: A properly configured development environment for both iOS and Android (Xcode and Android Studio). Follow the official React Native environment setup guide.
- Physical Devices: Access to a physical iPhone and an Android device is highly recommended. Simulators have limited or no health data.
- React Native Version:
0.71or higher is recommended.
Step 1: Project Setup and Library Installation
First, let's create a new React Native project.
npx react-native@latest init HealthApp
cd HealthApp
Now, we'll install the necessary libraries for each platform.
# For Apple HealthKit on iOS
npm install react-native-health
# For Health Connect on Android
npm install react-native-health-connect
For iOS, we also need to install the pods.
cd ios && pod install && cd ..
Step 2: iOS Native Configuration (react-native-health)
What we're doing
We need to configure the native iOS project to enable HealthKit and let the user know why we're requesting access to their health data.
Implementation
-
Enable HealthKit in Xcode:
- Open
/ios/HealthApp.xcworkspacein Xcode. - Select the
HealthAppproject in the navigator, then select theHealthApptarget. - Go to the "Signing & Capabilities" tab.
- Click
+ Capabilityand double-clickHealthKit. This will add the capability and create a.entitlementsfile.
- Open
-
Add Usage Descriptions to
Info.plist:- In Xcode, open the
Info.plistfile. - Add two new keys by clicking the
+icon:Privacy - Health Share Usage Description: Set the value to a user-friendly message, e.g., "We need to read your health data to provide personalized insights."Privacy - Health Update Usage Description: Set the value to "We need to write workout data to track your activity in Apple Health."
- In Xcode, open the
How it works
Enabling the HealthKit capability tells iOS that our app intends to interact with the HealthKit store. The Info.plist entries are crucial for user privacy; iOS will display these strings to the user when our app first requests permission. Without these, the app will crash.
Step 3: Android Native Configuration (react-native-health-connect)
What we're doing
For Android, we need to declare our app's intent to use Health Connect and specify the exact permissions we'll be requesting in the AndroidManifest.xml.
Implementation
-
Configure
AndroidManifest.xml:- Open
/android/app/src/main/AndroidManifest.xml. - Add the necessary permissions inside the
<manifest>tag.
code<!-- Required to specify which Health Connect permissions the app can request --> <uses-permission android:name="android.permission.health.READ_STEPS" /> <uses-permission android:name="android.permission.health.READ_HEART_RATE" /> <uses-permission android:name="android.permission.health.WRITE_EXERCISE" /> <uses-permission android:name="android.permission.health.READ_EXERCISE" />Code collapsed- Inside the
<application>tag, add an intent filter to handle the Health Connect permission rationale screen.
code<activity android:name=".MainActivity" android:exported="true"> <!-- ... existing intent-filter ... --> <intent-filter> <action android:name="android.health.connect.action.ACTION_HEALTH_CONNECT_SETTINGS" /> </intent-filter> </activity>Code collapsed - Open
How it works
The <uses-permission> tags inform the Android OS and the Google Play Store about the sensitive data your app might access. The intent-filter allows your app to correctly deep-link to the Health Connect permission management screen, which is a required part of the user flow.
Step 4: Creating a Unified Health API Service
This is where we abstract the platform differences.
What we're doing
We will create a single JavaScript file, src/services/HealthService.js, that will expose a consistent set of functions to our React components. This service will internally call the appropriate library based on Platform.OS.
Implementation
Create a new directory src/services and a file HealthService.js.
// src/services/HealthService.js
import { Platform } from 'react-native';
import AppleHealthKit from 'react-native-health';
import {
initialize,
requestPermission,
readRecords,
writeRecords,
} from 'react-native-health-connect';
// Define permissions for both platforms
const permissions = {
ios: {
permissions: {
read: [
AppleHealthKit.Constants.Permissions.Steps,
AppleHealthKit.Constants.Permissions.HeartRate,
AppleHealthKit.Constants.Permissions.Workout,
],
write: [AppleHealthKit.Constants.Permissions.Workout],
},
},
android: {
permissions: [
{ accessType: 'read', recordType: 'Steps' },
{ accessType: 'read', recordType: 'HeartRate' },
{ accessType: 'write', recordType: 'ExerciseSession' },
{ accessType: 'read', recordType: 'ExerciseSession' },
],
},
};
const requestPermissions = async () => {
if (Platform.OS === 'ios') {
return new Promise((resolve, reject) => {
AppleHealthKit.initHealthKit(permissions.ios, (error, results) => {
if (error) {
console.log('[ERROR] Failed to grant permissions!', error);
return reject(error);
}
resolve(results);
});
});
} else {
try {
await initialize();
const granted = await requestPermission(permissions.android);
return granted;
} catch (error) {
console.log('[ERROR] Failed to grant permissions!', error);
throw error;
}
}
};
const getDailyStepCount = async () => {
const today = new Date();
if (Platform.OS === 'ios') {
return new Promise((resolve, reject) => {
const options = { date: today.toISOString() };
AppleHealthKit.getStepCount(options, (err, result) => {
if (err) {
return reject(err);
}
resolve(result.value);
});
});
} else {
const today = new Date();
today.setHours(0, 0, 0, 0);
const records = await readRecords('Steps', {
timeRangeFilter: {
operator: 'between',
startTime: today.toISOString(),
endTime: new Date().toISOString(),
},
});
return records.reduce((sum, record) => sum + record.count, 0);
}
};
// Other methods (getHeartRate, saveWorkout) will go here...
export default {
requestPermissions,
getDailyStepCount,
// ... export other methods
};
How it works
The requestPermissions function uses Platform.OS to decide whether to call AppleHealthKit.initHealthKit or react-native-health-connect's requestPermission. We've defined the permissions for both platforms in a structured way. The getDailyStepCount function does the same, adapting the options and response handling for each library.
Step 5: Reading & Writing Data
Let's complete our HealthService.js with functions for heart rate and workouts.
What we're doing
We'll add getHeartRateSamples and saveWorkout to our service, again handling platform differences.
Implementation
Add the following functions to src/services/HealthService.js.
// ... inside src/services/HealthService.js
const getHeartRateSamples = async () => {
const now = new Date();
const yesterday = new Date();
yesterday.setDate(now.getDate() - 1);
if (Platform.OS === 'ios') {
return new Promise((resolve, reject) => {
const options = {
startDate: yesterday.toISOString(),
endDate: now.toISOString(),
};
AppleHealthKit.getHeartRateSamples(options, (err, results) => {
if (err) {
return reject(err);
}
resolve(results);
});
});
} else {
const records = await readRecords('HeartRate', {
timeRangeFilter: {
operator: 'between',
startTime: yesterday.toISOString(),
endTime: now.toISOString(),
},
});
return records.map(record => ({
value: record.samples[0].beatsPerMinute,
endDate: record.time,
}));
}
};
const saveWorkout = async () => {
const startTime = new Date();
startTime.setHours(startTime.getHours() - 1); // 1-hour workout
const endTime = new Date();
if (Platform.OS === 'ios') {
return new Promise((resolve, reject) => {
const options = {
type: AppleHealthKit.Constants.Activities.Running,
startDate: startTime.toISOString(),
endDate: endTime.toISOString(),
energyBurned: 300, // in calories
totalDistance: 5, // in kilometers
};
AppleHealthKit.saveWorkout(options, (err, result) => {
if (err) {
return reject(err);
}
resolve(result);
});
});
} else {
const records = await writeRecords([
{
recordType: 'ExerciseSession',
startTime: startTime.toISOString(),
endTime: endTime.toISOString(),
exerciseType: 'Running',
title: 'Morning Run',
},
]);
return records;
}
};
export default {
requestPermissions,
getDailyStepCount,
getHeartRateSamples,
saveWorkout,
};
Putting It All Together
Now let's use our shiny new service in a React component.
Implementation
Replace your App.tsx (or App.js) content with the following:
// App.tsx
import React, { useState, useEffect } from 'react';
import {
SafeAreaView,
ScrollView,
StatusBar,
StyleSheet,
Text,
Button,
View,
} from 'react-native';
import HealthService from './src/services/HealthService';
const App = () => {
const [hasPermissions, setHasPermissions] = useState(false);
const [steps, setSteps] = useState(0);
const [heartRate, setHeartRate] = useState(0);
useEffect(() => {
// Check for permissions on component mount
HealthService.requestPermissions().then(setHasPermissions).catch(console.error);
}, []);
const fetchHealthData = () => {
if (!hasPermissions) {
alert('Permissions not granted!');
return;
}
HealthService.getDailyStepCount()
.then(setSteps)
.catch(err => console.error('Steps Error:', err));
HealthService.getHeartRateSamples()
.then(samples => {
if (samples.length > 0) {
setHeartRate(samples[samples.length - 1].value);
}
})
.catch(err => console.error('Heart Rate Error:', err));
};
const handleSaveWorkout = () => {
HealthService.saveWorkout()
.then(() => alert('Workout saved successfully!'))
.catch(err => console.error('Workout Save Error:', err));
};
return (
<SafeAreaView style={styles.container}>
<StatusBar barStyle={'dark-content'} />
<ScrollView contentInsetAdjustmentBehavior="automatic">
<View style={styles.section}>
<Text style={styles.title}>React Native Health Demo</Text>
<Text>Permissions Granted: {hasPermissions ? '✅' : '❌'}</Text>
</View>
<View style={styles.section}>
<Button title="Fetch Health Data" onPress={fetchHealthData} />
<Text style={styles.dataText}>Steps Today: {steps}</Text>
<Text style={styles.dataText}>Latest Heart Rate: {heartRate} BPM</Text>
</View>
<View style={styles.section}>
<Button title="Save a Test Workout" onPress={handleSaveWorkout} />
</View>
</ScrollView>
</SafeAreaView>
);
};
const styles = StyleSheet.create({
container: { flex: 1 },
section: {
padding: 24,
borderBottomWidth: 1,
borderBottomColor: '#ccc',
},
title: {
fontSize: 24,
fontWeight: '600',
marginBottom: 8,
},
dataText: {
fontSize: 18,
marginTop: 8,
},
});
export default App;
Now, run your app on a physical device.
npx react-native run-ios --device="Your iPhone Name"
npx react-native run-android --deviceId="Your Device ID"
You should be prompted for health permissions. After granting them, you can fetch data and save a test workout!
Security Best Practices
Working with health data comes with great responsibility.
- Request Minimal Scope: Only ask for the permissions you absolutely need for your app's functionality. Users are wary of apps that ask for everything.
- Explain Why: Before showing the permission prompt, use a modal or a screen to explain to the user why you need their data and what you will do with it.
- Data Privacy: Never send raw HealthKit or Health Connect data to your servers without explicit user consent and a clear privacy policy. Anonymize or aggregate data whenever possible.
- Handle Denials Gracefully: Users can deny permissions. Your app should not crash. Instead, display a message explaining that certain features are unavailable and provide a shortcut to the settings to enable permissions later.
- HIPAA: If your app is considered to be used in a clinical context in the US, you must be aware of HIPAA (Health Insurance Portability and Accountability Act) regulations, which govern the privacy and security of protected health information.
Conclusion
You have successfully built a cross-platform React Native app that integrates with both Apple HealthKit and Google's Health Connect. By creating a unified API service, you've kept your application logic clean and maintainable, isolating the platform-specific code in one place. This architecture makes it easy to add support for new health metrics in the future.
The next step is to explore more data types, create beautiful visualizations with this data, and build features that provide real value to your users on their health journey.
Resources
- react-native-health (iOS): GitHub Repository
- react-native-health-connect (Android): npm Package
- Apple HealthKit Documentation: Developer Docs
- Google Health Connect Documentation: Developer Docs