In the rapidly growing digital health space, providing users with a holistic view of their fitness and wellness data is no longer a feature but an expectation. For React Native developers, this presents a unique challenge: how to securely access and consolidate data from two distinct, platform-specific ecosystems – Apple's HealthKit on iOS and Google's Health Connect (the successor to Google Fit) on Android.
This tutorial will guide you through building a cross-platform health and fitness application using React Native and Expo. We'll harness the power of native modules to read key health metrics, demonstrating how to handle the intricacies of each platform while maintaining a clean, unified codebase. By the end, you'll have a working app that can fetch step counts, heart rate, and sleep data, and a robust framework for adding more metrics.
This matters to developers because mastering this integration opens the door to creating compelling health tech applications, from workout trackers to comprehensive wellness platforms, all from a single, manageable React Native project.
Prerequisites:
- Node.js (LTS version) and npm/yarn installed.
- Expo CLI installed globally.
- A physical iOS device for testing Apple HealthKit and a physical Android device or emulator for Google Fit/Health Connect.
- Basic understanding of React Native, TypeScript, and React Hooks.
Understanding the Problem: The Two Ecosystems
Integrating health data in a React Native app means dealing with two fundamentally different native APIs:
- Apple HealthKit (iOS): A mature and deeply integrated framework on iOS. It acts as a central, encrypted repository for all health and fitness data. Access is granted on a per-data-type basis, and user privacy is paramount.
- Health Connect (Android): Google's newer, unified platform that allows users to share data between their favorite health and fitness apps. It centralizes data that was previously often siloed within individual apps or accessible through the older Google Fit API.
The primary challenge is that these two platforms expose different data structures, use different data type identifiers, and have unique setup and permission flows. Our goal is to create an abstraction layer that hides this complexity, allowing our React components to request health data without needing to know which OS it's running on.
Prerequisites: Setting Up Your Expo Project
Because we need to use native code for both HealthKit and Health Connect, we cannot use the standard Expo Go app. We must create a custom development client. This client will be a version of your app that includes the necessary native modules.
Step 1: Initialize the Project
Let's start with a fresh Expo project using the TypeScript template:
npx create-expo-app MyHealthApp --template blank-typescript
cd MyHealthApp
Step 2: Install Dependencies
We'll need several packages:
expo-dev-client: For building and using our custom client.react-native-health: The bridge to Apple HealthKit.react-native-health-connect: The bridge to Android's Health Connect.expo-build-properties: To set the minimum SDK version for Android.
Install them all with one command:
npx expo install expo-dev-client react-native-health react-native-health-connect expo-build-properties
Step 3: Configure the Project
We need to make several changes to our app.json file. This is where we'll configure permissions and plugins for our native projects.
Open your app.json and modify it to look like this:
{
"expo": {
"name": "MyHealthApp",
"slug": "MyHealthApp",
"version": "1.0.0",
"orientation": "portrait",
"icon": "./assets/icon.png",
"userInterfaceStyle": "light",
"splash": {
"image": "./assets/splash.png",
"resizeMode": "contain",
"backgroundColor": "#ffffff"
},
"assetBundlePatterns": [
"**/*"
],
"ios": {
"supportsTablet": true,
"bundleIdentifier": "com.yourcompany.myhealthapp",
"infoPlist": {
"NSHealthShareUsageDescription": "We need access to your health data to show you your activity.",
"NSHealthUpdateUsageDescription": "We need to write your workout data to Apple Health."
}
},
"android": {
"adaptiveIcon": {
"foregroundImage": "./assets/adaptive-icon.png",
"backgroundColor": "#ffffff"
},
"package": "com.yourcompany.myhealthapp",
"permissions": [
"android.permission.health.READ_STEPS",
"android.permission.health.READ_HEART_RATE",
"android.permission.health.READ_SLEEP"
]
},
"web": {
"favicon": "./assets/favicon.png"
},
"plugins": [
"react-native-health",
"expo-health-connect",
[
"expo-build-properties",
{
"android": {
"compileSdkVersion": 34,
"targetSdkVersion": 34,
"minSdkVersion": 26
}
}
]
]
}
}
Key changes:
- iOS
infoPlist: We added descriptions for why we need HealthKit access. This is mandatory and will be shown to the user in the permission dialog. - Android
permissions: We specified the Health Connect permissions our app will request. plugins: We added the plugins for each library. Theexpo-health-connectplugin will automatically handle required modifications to the native Android project. Theexpo-build-propertiesplugin is used to ensure our Android app targets a recent enough SDK version for Health Connect.
Step 4: Prebuild and Run the Dev Client
Now, generate the native ios and android directories:
npx expo prebuild
Finally, build and run your custom client on your devices:
For iOS (on a physical device):
npx expo run:ios
For Android (on a physical device or emulator):
npx expo run:android
This process might take a while the first time. Once it's running, you have a development environment ready to test our native health integrations.
The Core Logic: A Unified useHealthData Hook
To manage the platform differences, we'll create a custom hook, useHealthData. This hook will expose a simple API to our components, like requestPermissions() and fetchTodayStats(), and handle all the platform-specific logic internally.
Create a new directory src/hooks and a file inside it named useHealthData.ts.
Step 1: Defining a Common Data Structure
First, let's define a unified HealthData type. This ensures our application works with a consistent data shape, regardless of the source.
// src/hooks/useHealthData.ts
export interface HealthData {
steps: number;
heartRate: number;
sleepHours: number;
}
Step 2: Setting Up the Hook and Platform-Specific Modules
Now, let's create the basic structure for our hook and import the necessary libraries.
// src/hooks/useHealthData.ts
import { useState, useEffect } from 'react';
import { Platform } from 'react-native';
import AppleHealthKit, { HealthKitPermissions } from 'react-native-health';
import { initialize, requestPermission, readRecords } from 'react-native-health-connect';
export interface HealthData {
steps: number;
heartRate: number;
sleepHours: number;
}
// Define permissions for Apple HealthKit
const healthKitPermissions: HealthKitPermissions = {
permissions: {
read: [
AppleHealthKit.Constants.Permissions.Steps,
AppleHealthKit.Constants.Permissions.HeartRate,
AppleHealthKit.Constants.Permissions.SleepAnalysis,
],
write: [],
},
};
// Define permissions for Google Health Connect
const healthConnectPermissions = [
{ accessType: 'read', recordType: 'Steps' },
{ accessType: 'read', recordType: 'HeartRate' },
{ accessType: 'read', recordType: 'SleepSession' },
];
export const useHealthData = () => {
const [healthData, setHealthData] = useState<HealthData | null>(null);
const [isInitialized, setIsInitialized] = useState(false);
// ... functions will go here
};
Step 3: Implementing Permissions
Let's implement the requestPermissions function. This will be the first thing we call to get the user's consent.
// Inside useHealthData hook
const requestPermissions = async (): Promise<boolean> => {
if (Platform.OS === 'ios') {
return new Promise((resolve) => {
AppleHealthKit.initHealthKit(healthKitPermissions, (error, results) => {
if (error) {
console.error("Error initializing HealthKit:", error);
return resolve(false);
}
setIsInitialized(true);
resolve(true);
});
});
} else if (Platform.OS === 'android') {
try {
const isInitialized = await initialize();
if (!isInitialized) {
console.error('Failed to initialize Health Connect');
return false;
}
const grantedPermissions = await requestPermission(healthConnectPermissions);
console.log('Granted Permissions:', grantedPermissions);
setIsInitialized(true);
return true;
} catch (error) {
console.error("Error initializing Health Connect:", error);
return false;
}
}
return false;
};
How it works:
- iOS: We call
AppleHealthKit.initHealthKit. This function both initializes the connection and presents the standard iOS permissions modal to the user. The user can grant or deny each permission individually. - Android: The process is a two-step. First,
initialize()checks if Health Connect is available and ready. Then,requestPermission()opens the Health Connect app's permission screen.
Step 4: Fetching and Normalizing Data
This is where we unify the data. We'll create a fetchTodayStats function that calls the appropriate native method and transforms the result into our HealthData interface.
// Inside useHealthData hook
const fetchTodayStats = async () => {
if (!isInitialized) {
console.warn("Health data not initialized. Please request permissions first.");
return;
}
const today = new Date();
const startDate = new Date(today.getFullYear(), today.getMonth(), today.getDate(), 0, 0, 0).toISOString();
const endDate = today.toISOString();
let steps = 0;
let heartRate = 0;
let sleepHours = 0;
if (Platform.OS === 'ios') {
// --- iOS Data Fetching ---
const options = { date: endDate };
const stepResult = await new Promise<number>((resolve) => {
AppleHealthKit.getStepCount(options, (err, result) => {
resolve(result?.value || 0);
});
});
steps = stepResult;
// ... iOS Heart Rate and Sleep fetching to be added
} else if (Platform.OS === 'android') {
// --- Android Data Fetching ---
const stepRecords = await readRecords('Steps', {
timeRangeFilter: {
operator: 'between',
startTime: startDate,
endTime: endDate,
},
});
steps = stepRecords.reduce((total, record) => total + (record as any).count, 0);
// ... Android Heart Rate and Sleep fetching to be added
}
setHealthData({ steps, heartRate, sleepHours });
};
This snippet only shows fetching steps to illustrate the core concept. The full implementation would involve fetching HeartRate and SleepAnalysis/SleepSession records and normalizing them (e.g., averaging heart rate, summing sleep duration).
Putting It All Together: The UI
Now we can use our useHealthData hook in a React component to display the data.
Create a file App.tsx and add the following code:
// App.tsx
import React, { useEffect } from 'react';
import { View, Text, Button, StyleSheet, SafeAreaView } from 'react-native';
import { useHealthData, HealthData } from './src/hooks/useHealthData'; // Assuming the hook is completed
export default function App() {
const { healthData, requestPermissions, fetchTodayStats } = useHealthData();
useEffect(() => {
// Automatically request permissions on app start
requestPermissions().then(success => {
if (success) {
fetchTodayStats();
}
});
}, []);
return (
<SafeAreaView style={styles.container}>
<Text style={styles.title}>My Health Stats</Text>
<View style={styles.card}>
<Stat label="Steps" value={healthData?.steps ?? 'N/A'} />
<Stat label="Heart Rate (bpm)" value={healthData?.heartRate ?? 'N/A'} />
<Stat label="Sleep (hours)" value={healthData?.sleepHours ?? 'N/A'} />
</View>
<Button title="Refresh Data" onPress={fetchTodayStats} />
</SafeAreaView>
);
}
const Stat = ({ label, value }: { label: string; value: string | number }) => (
<View style={styles.stat}>
<Text style={styles.label}>{label}</Text>
<Text style={styles.value}>{value}</Text>
</View>
);
const styles = StyleSheet.create({
// ... Add styles for container, title, card, etc.
});
This component provides a simple UI to request permissions and display the fetched data, demonstrating the power of our abstraction.
Security Best Practices
When handling sensitive health data, security and user privacy are non-negotiable.
- Request Only What You Need: Only ask for read/write permissions for the specific data types your app's features require.
- Be Transparent: Clearly explain in your UI and privacy policy why you need access to health data. The
NSHealthShareUsageDescriptioninapp.jsonis your first opportunity to build trust. - Handle Data Responsibly: Do not store health data on your servers unless absolutely necessary and compliant with regulations like HIPAA or GDPR. Process data on the device whenever possible.
- Graceful Degradation: Your app should function gracefully if the user denies permissions. Don't crash or lock features unnecessarily; instead, provide a clear explanation of what's unavailable and how they can grant permissions later from the system settings.
Conclusion
Integrating Apple HealthKit and Google Fit in a React Native Expo app is a powerful way to build engaging health and wellness applications. By using a custom development client and creating a unified data hook, you can effectively manage platform-specific APIs while keeping your application logic clean and maintainable. This approach provides a solid foundation that you can extend to support a wide array of health metrics, from nutrition to workout tracking.
Next Steps
- Complete the
fetchTodayStatsfunction to include heart rate and sleep data normalization. - Implement write functionality to save workouts or other health data back to HealthKit and Health Connect.
- Explore background data fetching to keep your app's data fresh.
- Build more sophisticated UI components to visualize health data with charts and graphs.