Scanning and rendering barcodes in a React Progressive Web App

Gareth Cronin
5 min readMar 14, 2023

I build side projects and my front-end of choice is a mobile-first, Progressive Web App (PWA), built in React and Material UI.

I noticed I was carrying a bunch of loyalty and membership cards around in my wallet, but the only way I used them was to scan the barcode on them. My local library system uses a barcode for identification, and the regular supermarket and up-market supermarket that I go to do the same. The library have added the barcode to their app, as has the supermarket, but they haven’t put them in the easiest to navigate to places. The library also doesn’t render the code in a way to make it easy to scan.

This seemed like a nice problem to solve in a quick PWA with only a handful of requirements:

  • Provide a way for the user to add new barcodes by scanning a barcode with a phone camera
  • Show a set of easily identified buttons to bring up the appropriate code
  • Render the code clear and large for easy scanning
  • Work offline

I’ve written recently about how I bootstrap PWAs with Create React App. Once the service worker is enabled, the app will cache its code and work indefinitely offline. I store the scanned codes in browser local storage, which also works offline.

The end result I call “Codex”:

Codex screenshots

Rendering barcodes

The first job I wanted to tick off was showing a manually created barcode. I wasn’t sure what formats the cards I wanted to store were in, so I downloaded a native mobile app called “Barcode Data Decoder Verifier”. It does exactly what it says on the tin. I scanned my three cards and found I had two types of linear barcode: Code 128 and Code 39. That also gave me the content for each code: a long number that matches the one printed under the code — presumably some kind of customer identifier.

I found a solid rendering library called react-barcode. It’s a wrapper for JsBarCode and QRCode.React. I don’t need QR codes for this project, but there’s plenty of formats supported, including the ones I have. Rendering was simple:

import React from 'react';
import Barcode from 'react-barcode';

export default function CodeDisplay({ code, format }) {
return (
<Barcode value={code} format={format} height={200} />
);
}

CodeDisplay.propTypes = {
code: PropTypes.string.isRequired, // the customer ID number
format: PropTypes.string.isRequired // e.g. CODE39 or CODE128
};

Scanning barcodes

I started a React Native app a while back that used QR codes with embedded UUIDs as a way of authenticating devices. There was a great Expo library that “just worked”. However, getting the same thing working in my particular combination of React hooks on a PWA took a lot of trial and error.

The solution involved another React wrapper library called react-zxing that wraps the de facto standard JS barcode parsing library called ZXing. It provides a hook that can take an HTML video embed feed and call back when ZXing has successfully parsed a code.

The biggest problem I had with scanning turned out to be my phone using the wrong camera by default. Late model Samsung phones (mine is an S22) have three separate optical lenses, each of which shows up as a separate HTML video-capable device. The default one is the 0.6 zoom, which is a kind of fisheye lens. ZXing couldn’t get a clear enough picture of the code with that lens to register the scan. Fortunately it’s possible to enumerate the cameras, retrieve an ID, and specify that ID when creating the hook.

I wasted a bit of time with another React wrapper for enumerating devices, and then realised that modern browser APIs are trivial to use. In the root of my app, I set up a useState hook to hold the devices and a useEffect in the root of my app. I’ve left the error handling in below where I use a Material UI Snackbar to show the user if something is going wrong. This could be a lot fancier, but not worth it on this very simple app.

//...
const [devices, setDevices] = React.useState([]);

//...
React.useEffect(() => {
(async () => {
try {
const availableDevices = await navigator.mediaDevices.enumerateDevices();
const availableVideoDevices = availableDevices.filter(device => device.kind === 'videoinput');
if (availableVideoDevices.length === 0) {
setSnackbarMessage('No cameras found');
setSnackbarOpen(true);
}
else {
setDevices(availableVideoDevices);
}
}
catch (e) {
setSnackbarMessage('Failed to find cameras. This could be permissions problem');
setSnackbarOpen(true);
}
})();

I build a drop-down selection box out of the device names so the user can select the correct camera before scanning. Setting up the hook is just a matter of connecting the video element with a reference. When a barcode is detected in the video stream, the onResult callback will fire and an object with a format (a numeric code where 2 maps to Code 39 and 4 maps to Code 128) and text (the customer number embedded in the barcode) will be passed in. I store this in a useState for later use.

import React from 'react';
import { useZxing } from 'react-zxing';

//...

export default function BarcodeScanner() {

//...
const [scan, setScan] = React.useState(null);
const { deviceId } = useParams();

const { ref } = useZxing({
onResult(newScan) {
setScan(newScan);
},
deviceId
});

//...

return (
<>
//...
<video width="300" ref={ref} />
//...
</>);
}

Final touches

I store the list of barcodes in browser local storage and then render them. I added a simple back up and restore from JSON file as a way of copying the list between devices and backing them up.

For the back up I added a menu item to call a chunk of code that I’ve used in other apps for plain file download:

const handleDownloadBackup = () => {
const element = document.createElement('a');
const file = new Blob([JSON.stringify(barcodes)], { type: 'application/json' });
element.href = URL.createObjectURL(file);
element.download = 'codex-backup.json';
document.body.appendChild(element);
element.click();
};

For restore, Material UI supports a hidden HTML input of type file embedded in a button:

const handleUploadBackup = (event) => {
const file = event.target.files[0];
const reader = new FileReader();
reader.onload = (e) => {
const barcodes = JSON.parse(e.target.result);
setBarcodes(barcodes);
};
reader.readAsText(file);
};

//...

<Button variant="contained" component="label">Choose file <input hidden accept="application/json"
multiple type="file"
onChange={(event) => { handleUploadBackup(event); }} /></Button>

//...

Conclusion

That’s it: minimal — but it works very well. I no longer need to carry around a bunch of cards, and with the VISA linked to Google Pay on my banking app, I can shop anywhere any time with only my phone!

--

--

Gareth Cronin

Technology leader in Auckland, New Zealand: start-up founder, father of two, maker of t-shirts and small software products