React Native SDK
The React Native SDK provides a complete suite of React hooks and platform-agnostic service adapters to build powerful food logging flows. It handles authentication, data synchronization, caching, and real-time image analysis.
The SDK is built on top of the REST API. You can always bypass the hooks and call the underlying MacrofyClient services directly if you need custom state management.
Installation
The SDK requires React 19.0.0+ and React Native 0.83.0+. It provides zero-config support for Expo apps and modular adapter interfaces for bare React Native projects.
Expo & React Native
Import the core client and hooks from @macrofy/react-native. Whether you use Expo or bare React Native, the SDK handles it cleanly.
Install
npm install @macrofy/react-native expo-camera expo-file-system expo-secure-store
Initialization
import { Macrofy } from '@macrofy/react-native';
const client = new Macrofy();
Provider Setup
To use the SDK hooks anywhere in your app, wrap your component tree in the MacrofyProvider.
The provider requires a connected client instance and the userId returned from a successful connect() handshake.
Once mounted, child components can freely use data hooks like useFoodSearch and useDiary.
App.tsx
import { useEffect, useState } from 'react';
import { MacrofyProvider, Macrofy } from '@macrofy/react-native';
import { FoodLogScreen } from './screens/FoodLogScreen';
const client = new Macrofy();
export default function App() {
const [userId, setUserId] = useState<string | null>(null);
useEffect(() => {
// Authenticate using HMAC exchange
client.connect({
appId: "your_app_id",
userId: "user_123",
signature: "e3b0c44298fc1c149afbf4c8996fb924..."
}).then(session => {
setUserId(session.user.id);
});
}, []);
if (!userId) return <LoadingSpinner />;
return (
<MacrofyProvider client={client} userId={userId}>
<FoodLogScreen />
</MacrofyProvider>
);
}
Food Search
Debounced, cancelable text search across the Macrofy database.
As the user types, the hook waits 450ms (configurable) before firing the query. If the query changes before the network request completes, stale responses are silently discarded to prevent UI tearing.
Configuration
- Name
debounceMs- Type
- number
- Description
Time to wait after the last keystroke. Defaults to
450.
- Name
limit- Type
- number
- Description
Max results to return. Defaults to
20.
- Name
enabled- Type
- boolean
- Description
Set to
falseto pause queries (e.g. when a modal is closed).
FoodSearchUI.tsx
import { useState } from 'react';
import { TextInput, FlatList, Text, View } from 'react-native';
import { useFoodSearch } from '@macrofy/react-native';
export function FoodSearchUI() {
const [query, setQuery] = useState("");
const { results, loading, error } = useFoodSearch(query, {
debounceMs: 300,
limit: 10
});
return (
<View>
<TextInput
placeholder="Search for food..."
value={query}
onChangeText={setQuery}
/>
{loading && <Text>Searching...</Text>}
{error && <Text className="text-red-500">{error.message}</Text>}
<FlatList
data={results}
keyExtractor={item => item.id}
renderItem={({ item }) => (
<View>
<Text>{item.icon} {item.name}</Text>
<Text>{item.calories} kcal</Text>
</View>
)}
/>
</View>
);
}
Image Scanner
The primary hook for AI-powered meal tracking. It takes a local image URI from the device, creates a processing job, uploads the raw bytes directly to GCS via the native adapter, and opens a Server-Sent Events (SSE) stream to wait for the analysis result.
The returned FoodData object contains the exact macros, serving size, and identified food name.
CameraFlow.tsx
import { useState } from 'react';
import { Button, Text, View } from 'react-native';
import { useImageScanner } from '@macrofy/react-native';
export function CameraFlow({ photoUri }) {
const { scan, loading, error } = useImageScanner();
const [result, setResult] = useState(null);
const handleScan = async () => {
try {
const food = await scan(photoUri, "photo");
setResult(food);
} catch (e) {
console.error("Scan failed", e);
}
};
return (
<View>
<Button
title={loading ? "Analyzing Magic..." : "Scan Meal"}
onPress={handleScan}
disabled={loading}
/>
{result && (
<View>
<Text>Identified: {result.name}</Text>
<Text>Calories: {result.calories}</Text>
<Text>Protein: {result.protein}g</Text>
</View>
)}
</View>
);
}
Barcode Scanner
Resolves a scanned UPC/EAN code into a structured FoodData object.
Pass the raw string captured by your device's camera library (e.g. expo-camera). The Macrofy backend automatically tests padded and unpadded format variants to find the right item.
BarcodeScan.tsx
import { useBarcodeScanner } from '@macrofy/react-native';
export function BarcodeScanner() {
const { scan, loading } = useBarcodeScanner();
const onBarcodeDetected = async (code: string) => {
try {
const food = await scan(code);
alert(`Found: ${food.name} (${food.calories} kcal)`);
} catch (error) {
alert('Barcode not found in database.');
}
};
return <YourCameraView onCode={onBarcodeDetected} />;
}
Diary & Server Sync
Manages user nutrition entries for a specific day.
Macrofy handles diary persistence entirely server-side. There is no local SQLite or cross-device sync conflicts to manage.
The hook auto-fetches entries whenever the date parameter changes, and any addEntry or deleteEntry operations instantly re-sync the remote data.
DailyLog.tsx
import { useDiary } from '@macrofy/react-native';
export function DailyLog() {
// Defaults to today if no date is provided
const { entries, loading, addEntry, deleteEntry } = useDiary();
const trackApple = async () => {
await addEntry({
date: "2026-03-12",
mealType: "snacks",
foodName: "Apple",
calories: 95,
protein: 0.5,
carbs: 25,
fat: 0.3,
addedMethod: "manual"
});
};
return (
<View>
{loading && <Spinner />}
{entries.map(entry => (
<MealRow
key={entry.id}
item={entry}
onDelete={() => deleteEntry(entry.id)}
/>
))}
<Button title="Quick Add Apple" onPress={trackApple} />
</View>
);
}
Daily Totals & Macros
Combine useDailyTotals and useMacroData to easily construct progress rings and macro bars for your dashboard.
useDailyTotalsqueries the aggregated sum of all diary entries for a given date.useMacroDatacombines those totals with the user's computed BMR goals (fromuseProfile) to yield{ current, goal }tuples for protein, carbs, and fat.
DashboardHeader.tsx
import { useDailyTotals, useProfile, useMacroData } from '@macrofy/react-native';
export function DashboardHeader({ dateStr }) {
const { totals } = useDailyTotals(dateStr);
const { goals } = useProfile();
// Pure computation combining both state trees
const macros = useMacroData(totals, goals);
return (
<View>
<Text>
Calories: {totals.calories} / {goals?.calories || 0}
</Text>
{/* Use tuple data for UI progress bars */}
<ProgressBar
label="Protein"
current={macros.protein.current}
max={macros.protein.goal}
/>
<ProgressBar
label="Carbs"
current={macros.carbs.current}
max={macros.carbs.goal}
/>
</View>
);
}