Camera-Based Barcode Scanning in React

Food Tracker (React + Firebase)

Chapter 4 · Camera-Based Barcode Scanning in React

This chapter builds the piece of the app that feeds Chapter 3's Cloud Function a barcode in the first place — and it's the one chapter in this entire course with no Firebase involvement at all. Everything here runs in the browser, which is exactly why the component built in this chapter will reappear, unchanged, in Food Tracker (React + Express) when that sibling course reaches its own Chapter 4.

Requesting Camera Access

const stream = await navigator.mediaDevices.getUserMedia({ video: { facingMode: "environment" }, });

facingMode: "environment" requests the rear camera on a phone — the one actually useful for scanning a product. "user" would request the front-facing camera instead, which is the wrong default for this app entirely. getUserMedia also only works over HTTPS (or localhost during development) — a real deployment gotcha worth knowing now, though not one this course has to worry much about, since Chapter 12's Firebase Hosting serves everything over HTTPS by default.

Decoding Barcodes From Video Frames

Two genuinely different approaches exist. The native browser BarcodeDetector API is fast and built in — but as of general browser support, it's available in Chrome/Edge on Android and desktop, and not in Safari on iOS. A JS library like ZXing (@zxing/browser) works everywhere, at some added CPU cost, since it decodes frames in pure JavaScript rather than using a native implementation. The practical pattern: try BarcodeDetector where it exists, fall back to ZXing where it doesn't.

import { useEffect, useRef } from "react"; function useBarcodeScanner(onDetected) { const videoRef = useRef(null); useEffect(() => { let stream; let stopped = false; async function start() { stream = await navigator.mediaDevices.getUserMedia({ video: { facingMode: "environment" }, }); videoRef.current.srcObject = stream; await videoRef.current.play(); if ("BarcodeDetector" in window) { const detector = new BarcodeDetector({ formats: ["ean_13", "upc_a"] }); const scan = async () => { if (stopped) return; const barcodes = await detector.detect(videoRef.current); if (barcodes.length > 0) { onDetected(barcodes[0].rawValue); return; } requestAnimationFrame(scan); }; scan(); } else { // fall back to ZXing's BrowserMultiFormatReader here } } start(); return () => { stopped = true; stream?.getTracks().forEach((track) => track.stop()); }; }, [onDetected]); return videoRef; }

Wiring It to This Course's Own Backend

The hook above knows nothing about Firebase — it just calls onDetected(barcode). The parent component supplies that callback, and that's the only place this course's own identity shows up at all:

function ScanScreen() { const handleDetected = async (barcode) => { const result = await lookupBarcode({ barcode }); // Chapter 3's Cloud Function // ...hand result.data to the add-item flow, Chapter 5 }; const videoRef = useBarcodeScanner(handleDetected); return

Food Tracker (React + Express)'s own ScanScreen will look identical except for one line — handleDetected will call its Express server's own POST endpoint instead of a Cloud Function. Everything above this line is shared, unmodified, code.

The point of building the identical component twice
Seeing the exact same camera-scanning code plug into two completely different backends — a Cloud Function here, a plain REST endpoint in the Express course — is itself the lesson: a well-designed frontend component doesn't need to know or care what's on the other end of its one callback prop. The backend is a genuinely swappable detail, not something the component is built around.
Desktop testing doesn't tell the whole story
A laptop webcam is a poor stand-in for the real experience — no autofocus hunting, no holding a curved product steady, none of the resolution constraints a phone camera actually has. Test on a real phone against an HTTPS URL (a Firebase Hosting preview channel, or a tunneling tool like ngrok during local development) before trusting that scanning "works."
Testing only in Chrome hides a real bug
If the ZXing fallback branch is left as a stub (as it is above, for brevity) rather than actually implemented, the scanner will work perfectly in Chrome-based browser testing and then silently fail for every iPhone Safari user — a large share of any real phone-camera app's actual audience. `"BarcodeDetector" in window` being false doesn't throw an error; it just quietly does nothing unless the fallback path is genuinely built and tested, not just sketched in a comment.

Where This Course Is Headed

Direct client writes to Firestore for the add-item flow (this scanned barcode's own destination), Security Rules as the real gatekeeper, expiry alerts, item history and search, marking items used, recipe lookup, Firebase Authentication, deployment, and a capstone.

Hands-On Exercises

Exercise 1

Explain why this chapter's camera-scanning component will reappear unchanged in Food Tracker (React + Express). What is the one thing that actually differs between the two courses' use of it?

📄 View solution
Exercise 2

Explain what happens if the useEffect cleanup function omits `stream?.getTracks().forEach(track => track.stop())`, and why this matters specifically for a scan screen a user might navigate to and away from repeatedly.

📄 View solution
Exercise 3

Explain why leaving the ZXing fallback as an unimplemented stub could pass all of a developer's own testing and still be a real bug in production. Which users specifically would be affected, and why wouldn't Chrome-based testing ever catch it?

📄 View solution

Chapter 4 Quick Reference

  • facingMode: "environment" — the rear camera, not the front-facing one
  • BarcodeDetector — native, fast, Chrome/Edge/Android; not on Safari/iOS — needs a ZXing fallback
  • Cleanup — always stop every track from the stream in the effect's cleanup function, or the camera stays on after unmount
  • This component is backend-agnostic — it only calls one onDetected(barcode) callback; the parent decides what happens next
  • This chapter's own throughline: the identical component will power Food Tracker (React + Express) too — only the parent's callback differs
  • Next chapter: Building the Add-Item Flow with Direct Firestore Writes