WellAlly Logo
WellAlly康心伴
Development

From Barcode to Nutrition Facts: Building a Food Scanner with React Native & Vision Camera

A step-by-step guide to creating a barcode scanner in React Native that fetches and displays nutritional information from the Open Food Facts API using the high-performance Vision Camera library.

W
2025-12-12
9 min read

Ever find yourself in a grocery store, wondering about the nutritional content of a product? In an age of health-conscious consumerism, having instant access to this information is a game-changer. This article will walk you through my journey of building a mobile app feature that does just that: a barcode scanner that fetches and displays nutritional data.

We'll be using the powerful react-native-vision-camera library, a high-performance tool for all things camera-related in React Native. For our data source, we'll tap into the incredible Open Food Facts API, a free, open, and crowd-sourced database of food products from around the globe.

This project is more than just a technical exercise; it's about creating a practical tool that empowers users to make more informed decisions about their food. By the end of this tutorial, you'll have a solid understanding of how to integrate camera functionalities into your React Native apps and interact with external APIs to bring valuable data to your users' fingertips.

Prerequisites:

  • Basic understanding of React Native and JavaScript.
  • Node.js and npm/yarn installed.
  • A development environment set up for React Native (including Android Studio or Xcode).
  • A physical device for testing the camera functionality is highly recommended.

Understanding the Problem

The core challenge is twofold: first, we need to reliably capture a barcode from a product using the device's camera. Second, we must take that barcode information and retrieve the corresponding nutritional data from a vast online database.

Technical Challenges:

  • Camera Integration: Accessing and managing the device camera can be complex, involving permissions, different device capabilities, and ensuring a smooth user experience.
  • Barcode Detection: The scanning needs to be fast and accurate, even in suboptimal lighting conditions or with slightly damaged barcodes.
  • API Interaction: We need to handle asynchronous data fetching, parse potentially complex JSON responses, and manage loading and error states gracefully.

Fortunately, modern libraries like react-native-vision-camera have simplified camera integration, and with its built-in code scanning capabilities, we can avoid the need for multiple libraries.

Prerequisites

Before we start coding, let's get our project set up.

  1. Create a new React Native project:

    code
    npx react-native@latest init NutritionScanner
    cd NutritionScanner
    
    Code collapsed
  2. Install react-native-vision-camera:

    code
    npm install react-native-vision-camera
    cd ios && pod install
    
    Code collapsed
  3. Configure Permissions: To use the camera, we need to declare the necessary permissions in our native project files.

    • iOS (ios/NutritionScanner/Info.plist):

      code
      <key>NSCameraUsageDescription</key>
      <string>$(PRODUCT_NAME) needs access to your Camera.</string>
      
      Code collapsed
    • Android (android/app/src/main/AndroidManifest.xml):

      code
      <uses-permission android:name="android.permission.CAMERA" />
      
      Code collapsed

With our project initialized and permissions configured, we're ready to start building.

Step 1: Implementing the Barcode Scanner

Our first major task is to get the camera up and running and actively scanning for barcodes.

What we're doing

We'll create a component that requests camera permissions, displays the camera feed, and uses a hook from react-native-vision-camera to detect barcodes in real-time.

Implementation

Create a new file src/BarcodeScanner.js:

code
// src/BarcodeScanner.js
import React, { useEffect, useState } from 'react';
import { View, StyleSheet, Text, Linking, Button } from 'react-native';
import {
  Camera,
  useCameraDevice,
  useCodeScanner,
} from 'react-native-vision-camera';

const BarcodeScanner = ({ onBarcodeScanned }) => {
  const [hasPermission, setHasPermission] = useState(false);
  const device = useCameraDevice('back');

  useEffect(() => {
    const checkCameraPermission = async () => {
      const status = await Camera.getCameraPermissionStatus();
      if (status === 'not-determined') {
        const newStatus = await Camera.requestCameraPermission();
        setHasPermission(newStatus === 'granted');
      } else {
        setHasPermission(status === 'granted');
      }
    };
    checkCameraPermission();
  }, []);

  const codeScanner = useCodeScanner({
    codeTypes: ['ean-13', 'upc-a', 'qr'], // Specify the barcode types you want to scan
    onCodeScanned: (codes) => {
      if (codes.length > 0) {
        onBarcodeScanned(codes[0].value);
      }
    },
  });

  if (device == null) {
    return <Text>No camera device found.</Text>;
  }

  if (!hasPermission) {
    return (
      <View style={styles.container}>
        <Text>Camera permission is required to scan barcodes.</Text>
        <Button title="Grant Permission" onPress={() => Linking.openSettings()} />
      </View>
    );
  }

  return (
    <View style={StyleSheet.absoluteFill}>
      <Camera
        style={StyleSheet.absoluteFill}
        device={device}
        isActive={true}
        codeScanner={codeScanner}
      />
    </View>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
  },
});

export default BarcodeScanner;
Code collapsed

How it works

  • We use the useEffect hook to check and request camera permissions when the component mounts.
  • useCameraDevice('back') selects the rear camera for scanning.
  • The useCodeScanner hook is the core of our scanning logic. We provide it with the types of barcodes to look for and a callback function, onCodeScanned.
  • When a barcode is detected, onCodeScanned is triggered, and we pass the scanned value to the parent component.

Common pitfalls

  • Permissions Denied: If the user denies camera permission, we need to provide a way for them to grant it later, which is why we offer a button to open the app settings.
  • Simulator Limitations: The camera will not work on a simulator. Always test on a physical device.

Step 2: Fetching Nutritional Data

Now that we can capture a barcode, let's use it to fetch data from the Open Food Facts API.

What we're doing

We'll create a function to call the Open Food Facts API with the scanned barcode. We'll then parse the JSON response to extract the nutritional information we need.

Implementation

First, let's create a service file for our API calls, src/api.js:

code
// src/api.js
const API_URL = 'https://world.openfoodfacts.org/api/v2/product/';

export const fetchNutritionData = async (barcode) => {
  try {
    const response = await fetch(`${API_URL}${barcode}?fields=product_name,nutriments,image_url`);
    if (!response.ok) {
      throw new Error('Product not found');
    }
    const data = await response.json();
    if (data.status === 0 || !data.product) {
      throw new Error('Product not found in the database.');
    }
    return data.product;
  } catch (error) {
    console.error('API Error:', error);
    throw error;
  }
};
Code collapsed

How it works

  • We define the base URL for the Open Food Facts product endpoint.
  • The fetchNutritionData function takes a barcode, constructs the full API URL, and makes a GET request.
  • We've added ?fields=... to the URL. This is a powerful feature of the Open Food Facts API that allows us to request only the data we need, reducing the response size.
  • Basic error handling is included to manage cases where the product isn't found or a network error occurs.

Common pitfalls

  • Incomplete Data: The Open Food Facts database is crowd-sourced, so some products might have incomplete or missing nutritional information. Your UI should be able to handle this gracefully.
  • Rate Limiting: While generous, be mindful of the API's rate limits, especially if your app becomes popular.

Putting It All Together

Now, let's integrate our BarcodeScanner and API service into our main App.js file.

Complete Example

code
// App.js
import React, { useState } from 'react';
import {
  SafeAreaView,
  StyleSheet,
  View,
  Text,
  Button,
  ActivityIndicator,
  Image,
} from 'react-native';
import BarcodeScanner from './src/BarcodeScanner';
import { fetchNutritionData } from './src/api';

const App = () => {
  const [isScanning, setIsScanning] = useState(false);
  const [productData, setProductData] = useState(null);
  const [isLoading, setIsLoading] = useState(false);
  const [error, setError] = useState(null);

  const handleBarcodeScanned = async (barcode) => {
    setIsScanning(false);
    setIsLoading(true);
    setError(null);
    setProductData(null);

    try {
      const data = await fetchNutritionData(barcode);
      setProductData(data);
    } catch (err) {
      setError(err.message);
    } finally {
      setIsLoading(false);
    }
  };

  const renderProductInfo = () => {
    if (!productData) return null;

    const { product_name, nutriments, image_url } = productData;

    return (
      <View style={styles.infoContainer}>
        {image_url && <Image source={{ uri: image_url }} style={styles.productImage} />}
        <Text style={styles.title}>{product_name}</Text>
        <Text>Calories per 100g: {nutriments['energy-kcal_100g'] || 'N/A'}</Text>
        <Text>Carbs per 100g: {nutriments.carbohydrates_100g || 'N/A'}</Text>
        <Text>Protein per 100g: {nutriments.proteins_100g || 'N/A'}</Text>
        <Text>Fat per 100g: {nutriments.fat_100g || 'N/A'}</Text>
      </View>
    );
  };

  if (isScanning) {
    return <BarcodeScanner onBarcodeScanned={handleBarcodeScanned} />;
  }

  return (
    <SafeAreaView style={styles.container}>
      <Text style={styles.header}>Nutrition Scanner</Text>
      <View style={styles.content}>
        {isLoading && <ActivityIndicator size="large" />}
        {error && <Text style={styles.errorText}>Error: {error}</Text>}
        {productData && renderProductInfo()}
      </View>
      <Button
        title={productData ? 'Scan Another Item' : 'Start Scanning'}
        onPress={() => {
          setProductData(null);
          setError(null);
          setIsScanning(true);
        }}
      />
    </SafeAreaView>
  );
};

const styles = StyleSheet.create({
  container: { flex: 1, justifyContent: 'space-between', padding: 16 },
  header: { fontSize: 24, fontWeight: 'bold', textAlign: 'center' },
  content: { flex: 1, justifyContent: 'center', alignItems: 'center' },
  infoContainer: { alignItems: 'center' },
  productImage: { width: 150, height: 150, resizeMode: 'contain', marginBottom: 12 },
  title: { fontSize: 20, fontWeight: 'bold', marginBottom: 8 },
  errorText: { color: 'red', textAlign: 'center' },
});

export default App;
Code collapsed

How it works

  • We manage the app's state with several useState hooks: isScanning, productData, isLoading, and error.
  • When the "Start Scanning" button is pressed, isScanning is set to true, which renders our BarcodeScanner component.
  • Once a barcode is scanned, handleBarcodeScanned is called. It sets isScanning to false, shows a loading indicator, and calls our API service.
  • The results are then displayed in the renderProductInfo function, which conditionally renders the product details.

A Note on User Experience

One common issue with barcode scanners is that they can scan too frequently, leading to multiple API calls for a single item. In our implementation, by setting isScanning to false immediately after the first successful scan, we effectively prevent this issue and provide a more controlled user experience.

Conclusion

We've successfully built a functional and practical feature in a React Native application. We've tackled camera integration with react-native-vision-camera, handled permissions, and fetched and displayed data from the Open Food Facts API.

This project serves as a fantastic foundation. You could expand on it by:

  • Displaying more detailed nutritional information (like vitamins, minerals, and additives).
  • Adding a history of scanned items.
  • Implementing offline capabilities to cache previously scanned products.
  • Contributing back to the Open Food Facts community by allowing users to submit photos of products not yet in the database.

Resources

#

Article Tags

reactnative
project
mobile
nutrition
api
W

WellAlly's core development team, comprised of healthcare professionals, software engineers, and UX designers committed to revolutionizing digital health management.

Expertise

Healthcare Technology
Software Development
User Experience
AI & Machine Learning

Found this article helpful?

Try KangXinBan and start your health management journey