Camera-Based Barcode Scanning (Frontend)

Food Tracker (Django)

Chapter 5 · Camera-Based Barcode Scanning (Frontend)

Chapter 4 built the endpoint; this chapter builds the page a phone camera actually talks to. The underlying browser APIs are identical to what this app's React-based sibling courses use — but how that code gets packaged looks genuinely different here.

A Template, Not a Component

Django has no equivalent of a portable React component or hook — no lightweight, reusable frontend unit that carries its own logic and can simply be dropped into two different apps unmodified. What it has instead is template inheritance and includes ({% extends %}, {% include %}) — a coarser-grained way to reuse markup, not JavaScript behavior. The scanning logic in this course lives as page-specific script inside one template, not as something this course could hand to a sibling the way the two React-based courses share one component outright.

The View

def scan_page(request): return render(request, "pantry/scan.html")
path("scan/", views.scan_page, name="scan_page"),

The Template

<!-- pantry/templates/pantry/scan.html --> {% extends "pantry/base.html" %} {% block content %} <video id="scanner-video" autoplay playsinline muted></video> <script src="{% static 'pantry/scan.js' %}"></script> {% endblock %}
// pantry/static/pantry/scan.js (async function () { const video = document.getElementById("scanner-video"); const stream = await navigator.mediaDevices.getUserMedia({ video: { facingMode: "environment" } }); video.srcObject = stream; await video.play(); if ("BarcodeDetector" in window) { const detector = new BarcodeDetector({ formats: ["ean_13", "upc_a"] }); const scan = async () => { const barcodes = await detector.detect(video); if (barcodes.length > 0) { stream.getTracks().forEach(t => t.stop()); window.location.href = `/lookup-redirect/${barcodes[0].rawValue}/`; return; } requestAnimationFrame(scan); }; scan(); } else { // fall back to ZXing's BrowserMultiFormatReader here } })();

A Real Full-Page-Navigation Consequence

Detecting a barcode here navigates to an entirely new URL — a genuine server round-trip and full page reload, not an in-memory state update. This is a real, honest consequence of the server-rendered-template default, not a mistake: the two React-based sibling courses handle a successful scan by updating component state directly, with no navigation at all. Chapter 11's Django REST Framework chapter is exactly where this could change — a fetch() call to a JSON endpoint instead of a URL redirect would avoid the page reload entirely, at the cost of writing more client-side JavaScript to manage that state by hand.

Camera Cleanup: The Same Gotcha, a Genuinely Harder Problem Here

stream.getTracks().forEach(t => t.stop()) still has to run before the camera is released — same underlying requirement as any camera-scanning code, in any framework. But without a component lifecycle, this template can only reliably guarantee cleanup on the one exit path it explicitly coded: a successful scan. A user who instead clicks a "Cancel" link, uses the browser's back button, or simply closes the tab leaves the camera running, with no equivalent of a React useEffect cleanup function guaranteed to fire regardless of how the component goes away. This is a genuine structural disadvantage of the plain-template approach for this specific concern, not something this chapter glosses over.

BarcodeDetector Support: The Same Real Limitation

Chrome/Edge on Android and desktop support it natively; Safari on iOS does not, and needs the same ZXing fallback this app's React-based courses rely on. This is a browser API limitation, entirely independent of which backend framework is serving the page.

The same logic, packaged completely differently
The underlying browser APIs — getUserMedia, BarcodeDetector, the ZXing fallback — don't care which backend framework is involved, and are genuinely reusable in spirit across all four Food Tracker courses. What differs is packaging: two React-based sibling courses share one literal, portable component; this course's identical logic lives as a page-specific script, because Django has no equivalent lightweight frontend-component primitive to package it as.
A separate static file, not inline script
Keeping the scanning logic in its own pantry/static/pantry/scan.js, rather than inline inside the template, means Django's own static-file handling (collected and served properly once Chapter 12 covers deployment) applies to it, and it's ready to reuse directly if a second page in this app ever needs scanning too.
Not every exit path stops the camera
Only the successful-scan path in this chapter's own script stops the camera stream. Navigating away any other way — Cancel, back button, closing the tab — leaves it running, with no framework-level guarantee catching every case the way a component's own unmount lifecycle would. Worth being explicit about with users (a visible "Cancel" control that also stops the stream) rather than assuming this is fully solved.

Where This Course Is Headed

The add-item flow via Django Forms, expiry alerts, item history and search, marking items used, recipe lookup, Django REST Framework — which directly revisits this chapter's own full-page-reload limitation — deployment, and a capstone.

Hands-On Exercises

Exercise 1

Explain why this course's camera-scanning JavaScript can't be shared as a single reusable unit the way the two React-based sibling courses share one component. What does Django offer instead, and why is it a coarser-grained kind of reuse?

📄 View solution
Exercise 2

Explain what happens, technically, when this chapter's scan.js detects a barcode, and how that differs from what happens in the React-based sibling courses on a successful scan. What would Chapter 11's DRF work change about this?

📄 View solution
Exercise 3

Explain why camera cleanup is honestly a harder problem in this chapter's template-based approach than in a React component, referencing specifically what a React lifecycle hook guarantees that a plain template script cannot.

📄 View solution

Chapter 5 Quick Reference

  • No portable component — Django's reuse tools ({% extends %}/{% include %}) work at the markup level, not the JS-behavior level
  • Same browser APIs as the React coursesgetUserMedia, BarcodeDetector, ZXing fallback — just packaged differently
  • A successful scan triggers a full page reload — a genuine server round-trip, not an in-memory update; Chapter 11's DRF work is where this could change
  • Cleanup is genuinely harder here — only the coded exit path stops the camera; no lifecycle guarantee covers every way a user might leave
  • Next chapter: Building the Add-Item Flow with Django Forms