How I Implemented Quagga JS Barcode Scanner Library and Landed a Job

How I Implemented Quagga JS Barcode Scanner Library and Landed a Job

When I applied for a Senior Frontend Developer role, I was given the challenge to create an application — a self-checkout system that utilized barcode scanning. The project required integrating the Quagga JS barcode scanner library, managing global states using React Context API, and deploying the application for real-world use.

This article delves into how I built this self-checkout application, the technical decisions I made, and how each piece of code contributed to the final product.

Project Setup and Structure

The project was initiated using Create React App (CRA), a robust framework that simplifies the setup of React applications with minimal configuration. After setting up the project, I organized the codebase into the following main directories: `components`, `context`, `views`, `utils`, and `assets`.

  1. Components: This directory housed all reusable UI components such as the barcode scanner, item modal, controls, header, and the off-canvas sidebar.
  2. Context: The React Context API was used to manage the global state across the application, particularly for the shopping basket and the visibility of the off-canvas component.
  3. Views: This directory contained the main screens of the app, which included the start screen and the primary scanning interface.
  4. Utils: Utility functions that provided small, reusable pieces of logic, such as removing the video element after a barcode scan, were stored here.
  5. Assets: This directory contained static assets such as images, icons, and styles used throughout the application.

Implementing the Main Application

The main application logic was encapsulated in the `App.js` file, which served as the entry point for the entire application.

import React from 'react';
import Header from './components/Header/Header';
import { ToastContainer } from 'react-toastify';
import 'react-toastify/dist/ReactToastify.css';
import { BasketContextProvider } from './context/BasketContext';
import { Routes, Route, Outlet } from 'react-router-dom';
import Main from './views/Main/Main';
import Start from './views/Start/Start';
import { OffcanvasContextProvider } from './context/OffcanvasContext';
import OffcanvasComponent from './components/Offcanvas/OffcanvasComponent';

In this code, I imported the necessary components and context providers. The `App.js` file was designed to wrap the entire application with the `BasketContextProvider` and `OffcanvasContextProvider`. These context providers were responsible for managing the global states related to the shopping basket and the off-canvas sidebar.

const App = () => {
return (
<BasketContextProvider>
<OffcanvasContextProvider>
<Routes>
<Route path={'/'} element={<Layout />}>
<Route index element={<Start />} />
<Route path={'/scan'} element={<Main />} />
</Route>
</Routes>
</OffcanvasContextProvider>
</BasketContextProvider>
);
};

This segment of the code defined the routing for the application. Using `react-router-dom`, I set up two primary routes: the start screen and the main scanning screen. The `Layout` component was used to structure the application’s layout.

const Layout = () => {
return (
<div className={'container-fluid bg-light vh-100 d-flex flex-column'}>
<Header />
<div className={'container'}>
<ToastContainer
position={'bottom-center'}
autoClose={5000}
hideProgressBar={false}
closeOnClick
pauseOnFocusLoss
draggable={false}
pauseOnHover
theme={'light'}
/>
<OffcanvasComponent />
<Outlet />
</div>
</div>
);
};

The `Layout` component ensured that the header, toast notifications, and off-canvas sidebar were consistently rendered across all routes. The `Outlet` component acted as a placeholder for the child routes, displaying the appropriate view based on the current route.

Firebase Integration

For storing the shopping basket data, I integrated the Firebase Realtime Database. The Firebase configuration and initialization were handled in the `firebase.js` file.

import { initializeApp } from 'firebase/app';
import { getDatabase } from 'firebase/database';

const firebaseConfig = {
apiKey: process.env.REACT_APP_FIREBASE_API_KEY,
authDomain: process.env.REACT_APP_FIREBASE_AUTH_DOMAIN,
projectId: process.env.REACT_APP_FIREBASE_PROJECT_ID,
storageBucket: process.env.REACT_APP_FIREBASE_STORAGE_BUCKET,
messagingSenderId: process.env.REACT_APP_FIREBASE_MESSAGING_SENDER_ID,
appId: process.env.REACT_APP_FIREBASE_APP_ID,
measurementId: process.env.REACT_APP_FIREBASE_MEASUREMENT_ID,
databaseURL: process.env.REACT_APP_FIREBASE_DATABASE_URL,
};

const app = initializeApp(firebaseConfig);
export const db = getDatabase(app);

This file ensured that Firebase was initialized with the correct configuration and provided a reference to the Firebase Realtime Database that could be used throughout the application. The actual values were stored in the environment variables.

Building the Start Screen

The start screen was designed to initialize a new shopping session by generating a unique basket ID using the `uuid` library and navigating the user to the scanning interface.

import React from 'react';
import { v4 as uuidv4 } from 'uuid';
import { useBasketContext } from '../../context/BasketContext';
import { useNavigate } from 'react-router-dom';

const Start = () => {
const { setBasketID } = useBasketContext();
const navigate = useNavigate();

Here, I used the `useBasketContext` hook to access the `setBasketID` function from the `BasketContext`. The `useNavigate ` hook from `react-router-dom` was used to programmatically navigate the user to the scanning screen.

  const handleStart = () => {
const uid = uuidv4();
setBasketID(uid);
navigate('/scan');
};

The `handleStart` function generated a new UUID for the basket session and set it in the global context before redirecting the user to the `/scan` route.

  return (
<div className={'row'}>
<div className={'d-flex flex-column col-12 align-items-center'}>
<button
type={'button'}
className={'btn btn-primary mt-5'}
onClick={handleStart}
>
Start the app
</button>
</div>
</div>
);
};

export default Start;

The start screen UI was simple, consisting of a single button that triggered the `handleStart` function when clicked.

Implementing the Scanning Interface

The main scanning interface was where most of the functionality came together. This screen handled barcode scanning, displaying scan results, and managing the scanning state.

import React, { useState, useRef, useCallback } from 'react';
import Quagga from '@ericblade/quagga2';
import { toast } from 'react-toastify';
import ItemModal from '../../components/ItemModal/ItemModal';
import { Modal } from 'bootstrap';
import removeVideoElement from '../../utils/removeVideoElement';
import { useBasketContext } from '../../context/BasketContext';
import Controls from '../../components/Controls/Controls';
import VideoComponent from '../../components/VideoComponent/VideoComponent';

I began by importing the necessary components and utilities. Quagga was the key library for barcode scanning, and `react-toastify` was used for displaying error messages or other notifications. In my project, Eric Blade’s fork of the original Quagga was used, due to better support for React.

const Main = () => {
const [scanning, setScanning] = useState(false);
const [cameras, setCameras] = useState([]);
const [cameraError, setCameraError] = useState('');
const [code, setCode] = useState('');
const scannerRef = useRef(null);
const { items } = useBasketContext();

Several pieces of state were initialized using the `useState` hook to manage scanning status, available cameras, detected codes, and potential camera errors. The `useRef` hook was used to create a reference to the video element for the scanner.

const openModal = useCallback(() => {
const modalElement = document.getElementById('staticBackdrop');
const bsModal = new Modal(modalElement, {
backdrop: 'static',
keyboard: false,
});
bsModal.show();
}, []);

The `openModal` function used Bootstrap’s modal component to display the item modal whenever a barcode was successfully scanned (I purposefully did not want to use reactstrap or react-bootstrap).

const closeModal = useCallback(() => {
const modalElement = document.getElementById('staticBackdrop');
const bsModal = Modal.getInstance(modalElement);
if (bsModal) {
bsModal.hide();
}
setCode('');
}, []);

The `closeModal` function reset the scanned code and hid the modal. Both `openModal` and `closeModal` were wrapped in `useCallback` to memoize the functions and prevent unnecessary re-renders.

const handleDetected = useCallback(
(result) => {
const itemsContainResult = items.some((item) => item.code === result);

if (itemsContainResult) {
return;
}

setCode(result);
setScanning(false);
removeVideoElement();
openModal();
},
[items, openModal]
);

The `handleDetected` function checked if the scanned item was already in the basket. If it wasn’t, it stopped the scanning process, removed the video element, and opened the modal to display the item.

const initializeCamera = useCallback(async () => {
try {
await Quagga.CameraAccess.request(null, {});
await Quagga.CameraAccess.release();
const detectedCameras = await Quagga.CameraAccess.enumerateVideoDevices();

if (detectedCameras.length === 0) {
throw new Error('Camera access denied by user.');
}

setCameras(detectedCameras);
} catch (err) {
toast(`Error accessing camera: ${err.message}`);
setCameraError(`Error accessing camera: ${err.message}`);
}
}, []);

The `initializeCamera` function handled camera access permissions and detected available video devices. If the camera access was denied, an error message was displayed using `react-toastify`.

const handleScanning = useCallback(() => {
if (scanning) {
setScanning(false);
removeVideoElement();
} else if (cameras.length === 0) {
initializeCamera()
.then(() => {
setScanning(true);
})
.catch(() => {});
} else if (cameras.length > 0 && !scanning) {
setScanning(true);
}
}, [cameras.length, initializeCamera, scanning]);

The `handleScanning` function toggled the scanning state. If no cameras were detected, it initialized the camera. Otherwise, it started or stopped the scanning process based on the current state.

  return (
<>
<VideoComponent
handleDetected={handleDetected}
scanning={scanning}
scannerRef={scannerRef}
/>
<Controls
cameras={cameras}
cameraError={cameraError}
handleScanning={handleScanning}
scanning={scanning}
/>
<ItemModal closeModal={closeModal} code={code} />
</>
);
};

export default Main;

The main scanning interface was composed of three components: `VideoComponent`, `Controls`, and `ItemModal`. These components were responsible for displaying the camera feed, controlling the scanning process, and showing scanned item details, respectively.

Context Management

React Context API was used to manage the global state for the shopping basket and the off-canvas sidebar. This ensured that the state was accessible across different components without the need for prop drilling.

1. Basket Context (`BasketContext.js`):

import { createContext, useContext, useEffect, useState } from 'react';
import { ref, set } from 'firebase/database';
import { db } from '../firebase';

const BasketContext = createContext(null);

export const BasketContextProvider = ({ children }) => {
const [items, setItems] = useState([]);
const [basketID, setBasketID] = useState('');

const saveItem = ({ code, itemName }) => {
const item = {
code,
itemName,
};
setItems((prevItems) => [...prevItems, item]);
};

useEffect(() => {
const saveBasket = async () => {
await set(ref(db, `/baskets/${basketID}`), items);
};

if (items.length > 0) {
saveBasket();
}
}, [basketID, items]);

return (
<BasketContext.Provider value={{ items, saveItem, setBasketID, basketID }}>
{children}
</BasketContext.Provider>
);
};

export const useBasketContext = () => {
return useContext(BasketContext);
};

The `BasketContext` managed the state of the scanned items and the basket ID. It provided functions to save items to the basket and to save the basket to Firebase whenever it was updated. This ensured that the basket data persisted across sessions.

2. Offcanvas Context (`OffcanvasContext.js`):

import React, { createContext, useContext, useState } from 'react';

const OffcanvasContext = createContext(null);

export const OffcanvasContextProvider = ({ children }) => {
const [show, setShow] = useState(false);

const toggleOffcanvas = () => {
setShow(!show);
};

return (
<OffcanvasContext.Provider value={{ show, toggleOffcanvas }}>
{children}
</OffcanvasContext.Provider>
);
};

export const useOffcanvasContext = () => {
return useContext(OffcanvasContext);
};

The `OffcanvasContext` managed the visibility of the off-canvas sidebar, allowing it to be toggled on or off as needed. This provided a seamless user experience when interacting with the shopping basket.

Utility Function

A small utility function was created to remove the video element from the DOM after the scanning process was complete. This was crucial to clean up resources.

const removeVideoElement = () => {
let element = document.querySelector('#interactive.viewport canvas, video');
element.parentNode.removeChild(element);
};

export default removeVideoElement;

Component Development and UI Implementation

The UI components were carefully designed to provide a seamless and intuitive user experience. Each component served a specific purpose and was built to be reusable across different parts of the application.

1. Barcode Scanner

The `BarcodeScanner` component was the heart of the scanning functionality. It used the Quagga JS library to read barcodes and trigger the `onDetected` callback when a valid barcode was scanned.

import { useCallback, useLayoutEffect } from 'react';
import PropTypes from 'prop-types';
import Quagga from '@ericblade/quagga2';

const getMedian = (arr) => {
const sortedArr = [...arr].sort((a, b) => a - b);
const midIndex = Math.floor(sortedArr.length / 2);
return sortedArr.length % 2 !== 0
? sortedArr[midIndex]
: (sortedArr[midIndex - 1] + sortedArr[midIndex]) / 2;
};

const getMedianOfCodeErrors = (decodedCodes) =>
getMedian(decodedCodes.flatMap((x) => x.error || []));

const defaultConfig = {
constraints: {
facingMode: 'environment',
aspectRatio: 1,
},
decoders: ['ean_reader'],
};

const BarcodeScanner = ({ onDetected, scannerRef }) => {
const handleDetected = useCallback(
(result) => {
if (!onDetected) return;
const medianError = getMedianOfCodeErrors(result.codeResult.decodedCodes);
if (medianError < 0.25) onDetected(result.codeResult.code);
},
[onDetected]
);

useLayoutEffect(() => {
let ignoreInit = false;

const initQuagga = async () => {
await new Promise((resolve) => setTimeout(resolve, 1));
if (ignoreInit) return;

Quagga.init(
{
inputStream: {
type: 'LiveStream',
constraints: defaultConfig.constraints,
target: scannerRef.current,
},
decoder: { readers: defaultConfig.decoders },
locate: true,
},
async (err) => {
if (err) {
console.error('Error starting Quagga:', err);
return;
}
Quagga.onDetected(handleDetected);

await Quagga.start();
}
);
};

initQuagga();

return () => {
ignoreInit = true;
Quagga.stop();
Quagga.offDetected(handleDetected);
};
}, [handleDetected, scannerRef]);

return null;
};

BarcodeScanner.propTypes = {
onDetected: PropTypes.func.isRequired,
scannerRef: PropTypes.object.isRequired,
};

export default BarcodeScanner;

2. Cart Item

The `CartItem` component displayed individual items in the shopping basket, showing the item name and the corresponding barcode.

import React from 'react';

const CartItem = ({ code, itemName }) => {
return (
<div className="card mb-2">
<div className="card-body">
<h5 className="card-title">{itemName}</h5>
<h6 className="card-subtitle text-body-secondary">{code}</h6>
</div>
</div>
);
};

export default CartItem;

3. Controls

The `Controls` component provided the interface for starting and stopping the scanning process, as well as handling camera permissions.

import React from 'react';
import PropTypes from 'prop-types';

const Controls = ({ cameras, cameraError, handleScanning, scanning }) => {
return (
<div className={'row pt-3'}>
<div
className={
'col-12 d-flex flex-column align-items-center justify-content-center'
}
>
{cameras.length === 0 && !cameraError && (
<div className={'alert alert-light'} role={'alert'}>
Please grant camera permission if prompted.
</div>
)}
</div>
<div className={'d-grid'}>
<button
type={'button'}
className={'btn btn-primary'}
onClick={handleScanning}
>
{scanning ? 'Stop Scanning' : 'Start Scanning'}
</button>
</div>
</div>
);
};

Controls.propTypes = {
cameras: PropTypes.array.isRequired,
cameraError: PropTypes.string,
handleScanning: PropTypes.func.isRequired,
scanning: PropTypes.bool.isRequired,
};

export default Controls;

4. Header

The `Header` component displayed the application’s logo and a basket icon, which showed the number of items in the basket and toggled the off-canvas sidebar.

ScanMart Logo
ScanMart Logo
import logo from '../../assets/img/logo.png';
import bag from '../../assets/img/bag.svg';
import { useBasketContext } from '../../context/BasketContext';
import PropTypes from 'prop-types';
import { useOffcanvasContext } from '../../context/OffcanvasContext';

const Badge = ({ amount }) => {
return (
<span
className={
'position-absolute top-0 start-100 translate-middle badge rounded-pill bg-danger'
}
>
{amount}
</span>
);
};

Badge.propTypes = {
amount: PropTypes.number.isRequired,
};

const Header = () => {
const { items, basketID } = useBasketContext();
const { toggleOffcanvas } = useOffcanvasContext();
return (
<div className={'row'}>
<div className={'col-12 bg-white'}>
<div className={'container'}>
<div className={'row py-2'}>
<div
className={
'col-12 d-flex align-items-center justify-content-between'
}
>
<a href={'/'}>
<img
src={logo}
alt={'scanm.art Logo'}
className={'img-fluid p-2'}
style={{ maxHeight: 55 }}
/>
</a>
{basketID && (
<div
style={{ position: 'relative' }}
onClick={() => toggleOffcanvas()}
>
{items && items.length > 0 && <Badge amount={items.length} />}
<img src={bag} alt={'Basket'} width={'32'} height={'32'} />
</div>
)}
</div>
</div>
</div>
</div>
</div>
);
};

export default Header;

5. Item Modal

The `ItemModal` component allowed users to name the scanned item before adding it to the basket. It utilized Bootstrap’s modal component for a consistent and accessible design.

import React, { useState } from 'react';
import { useBasketContext } from '../../context/BasketContext';
import barcode from '../../assets/img/barcode.svg';
import box from '../../assets/img/box.svg';
import PropTypes from 'prop-types';

const ItemModal = ({ code, closeModal }) => {
const [itemName, setItemName] = useState('');
const { saveItem } = useBasketContext();

const handleItemName = (e) => {
setItemName(e.target.value);
};

const handleSaveItem = () => {
saveItem({ code, itemName });
closeModal();
setItemName('');
};

const handleCloseModal = () => {
setItemName('');
closeModal();
};

return (
<div
className={'modal fade'}
id={'staticBackdrop'}
tabIndex={'-1'}
aria-labelledby={'staticBackdropLabel'}
aria-hidden={'true'}
>
<div className={'modal-dialog modal-dialog-centered'}>
<div className={'modal-content'}>
<div className={'modal-header'}>
<h1 className={'modal-title fs-5'} id={'staticBackdropLabel'}>
Scanned Item
</h1>
<button
type={'button'}
className={'btn-close'}
aria-label={'Close'}
onClick={handleCloseModal}
></button>
</div>
<div className={'modal-body'}>
<div className={'mb-3 row'}>
<label
htmlFor={'scannedItemCode'}
className={'col-sm-4 col-form-label'}
>
Scanned Item
</label>
<div className={'col-sm-8'}>
<div className={'input-group'}>
<span className={'input-group-text'}>
<img src={barcode} alt={'Barcode Icon'} />
</span>
<input
type={'text'}
disabled
className={'form-control'}
id={'scannedItemCode'}
value={code}
/>
</div>
</div>
</div>
<div className={'mb-3 row'}>
<label htmlFor={'itemName'} className={'col-sm-4 col-form-label'}>
Item Name
</label>
<div className={'col-sm-8'}>
<div className={'input-group'}>
<span className={'input-group-text'}>
<img src={box} alt={'Item Icon'} />
</span>
<input
type={'text'}
className={'form-control'}
id={'itemName'}
name={'itemName'}
value={itemName}
onChange={handleItemName}
/>
</div>
</div>
</div>
</div>
<div className={'modal-footer'}>
<button
type={'button'}
className={'btn btn-secondary'}
onClick={handleCloseModal}
>
Dismiss
</button>
<button
type={'button'}
className={'btn btn-primary'}
onClick={handleSaveItem}
>
Add to basket
</button>
</div>
</div>
</div>
</div>
);
};

ItemModal.propTypes = {
code: PropTypes.string.isRequired,
closeModal: PropTypes.func.isRequired,
};

export default ItemModal;

6. Offcanvas Component

The `OffcanvasComponent` provided a sidebar for displaying the basket contents, using Bootstrap’s Offcanvas component for a modern, responsive design.

import React, { useEffect } from 'react';
import { Offcanvas } from 'bootstrap';
import { useOffcanvasContext } from '../../context/OffcanvasContext';
import { useBasketContext } from '../../context/BasketContext';
import CartItem from '../CartItem/CartItem';

const OffcanvasComponent = () => {
const { show, toggleOffcanvas } = useOffcanvasContext();
const { items } = useBasketContext();

useEffect(() => {
const offcanvasElement = document.getElementById('offcanvasRight');
const bsOffcanvas = new Offcanvas(offcanvasElement);

if (show) {
bsOffcanvas.show();
}

offcanvasElement.addEventListener('hidden.bs.offcanvas', toggleOffcanvas);

return () =>
offcanvasElement.removeEventListener(
'hidden.bs.offcanvas',
toggleOffcanvas
);
}, [show, toggleOffcanvas]);

return (
<div
className="offcanvas offcanvas-end"
tabIndex="-1"
id="offcanvasRight"
aria-labelledby="offcanvasRightLabel"
>
<div className="offcanvas-header">
<h5 className="offcanvas-title" id="offcanvasRightLabel">
Basket
</h5>
<button
type="button"
className="btn-close"
data-bs-dismiss="offcanvas"
aria-label="Close"
></button>
</div>
<div className="offcanvas-body">
{items &&
items.length > 0 &&
items.map((item, index) => {
return (
<CartItem key={index} code={item.code} itemName={item.itemName} />
);
})}
</div>
</div>
);
};

export default OffcanvasComponent;

7. Video Component

The `VideoComponent` was responsible for displaying the live camera feed for barcode scanning. It conditionally rendered the `BarcodeScanner` component when scanning was active.

import barcode from '../../assets/img/barcode.svg';
import BarcodeScanner from '../BarcodeScanner/BarcodeScanner';
import React from 'react';
import PropTypes from 'prop-types';

const VideoComponent = ({ scannerRef, scanning, handleDetected }) => {
return (
<div className={'row pt-3'}>
<div
className={
'd-flex flex-column col-12 align-items-center justify-content-center'
}
>
<div
style={{
width: '100%',
minHeight: '300px',
}}
ref={scannerRef}
className={
'border border-5 border-warning bg-white d-flex align-items-center justify-content-center'
}
>
{!scanning && (
<img src={barcode} alt={'Barcode Icon'} width={70} height={70} />
)}
{scanning && (
<BarcodeScanner
scannerRef={scannerRef}
onDetected={(result) => handleDetected(result)}
/>
)}
</div>
</div>
</div>
);
};

VideoComponent.propTypes = {
scannerRef: PropTypes.oneOfType([
PropTypes.func,
PropTypes.shape({ current: PropTypes.instanceOf(Element) }),
]).isRequired,
scanning: PropTypes.bool.isRequired,
handleDetected: PropTypes.func.isRequired,
};
export default VideoComponent;

Conclusion

Building this self-checkout application was an enriching experience that allowed me to dive deep into integrating hardware APIs with modern web technologies.

By structuring the codebase efficiently, implementing barcode scanning with Quagga JS, and managing the state with React Context API, I was able to create a robust and scalable MVP for a self-checkout system. This project ultimately played a key role in landing the Senior Frontend Developer role, demonstrating my capabilities in a practical, hands-on manner.

Whether you’re working on a similar project or simply exploring the potential of self-service applications, I hope this detailed walkthrough provides valuable insights into the development process.

💾 The entire project’s code is available on my GitHub.