Cover: "someone scanning a barcode of a package with their smartphone, caricature" – DALL-E mini
EDIT: I have now published this to a new NPM package, as react-zxing.
Background
This would be my first article. I was motivated to write this because I found it particularly difficult to implement, I couldn't find any great tutorial and my reddit post seemed to attract some interest.
What are we building?
I wanted to use a barcode scanner for my side project Snxbox. My criteria were:
- Stream the user's device camera output to a video element so that the user can see what they're aiming their camera at.
- Detect QR and EAN codes accurately from the stream and emit the results.
Alternatives
I started looking for React-compatible packages I could use. The immediate package I found was react-qr-barcode-scanner
which offered a simple drop-in react component.
react-qr-barcode-scanner
The react-qr-barcode-scanner
relies on zxing
for decoding barcodes. I used it for some time until I discovered a bug caused by inconsistent results from reading EAN codes. I found an issue on zxing and it appeared to have been fixed. However the react-qr-barcode-scanner
used an older version of zxing
where this was still an issue.
quokka2
This is another package that extends zxing
. I found an example on how to use it with React but honestly it seemed daunting.
html5-qrcode
Yet another package extending zxing
. The implementation was a bit easier to follow although this seemed to also use an old version of zxing
, so I was a bit cautious about using it.
Using the Barcode Detection API
There is an experimental API for scanning barcodes, but unfortunately it seems to yet have limited support.
The refactoring attempt
I eventually forked the react-qr-barcode-scanner
in an attempt to update its dependencies, but discovered that the implementation was quite straight-forward to begin with.
Also, react-qr-barcode-scanner
uses react-webcam
to stream the camera to a video element from which it at an interval takes snapshots of to be decoded by zxing
– it doesn't actually decode the video stream itself.
We could actually read directly from the video stream with zxing
and preview the stream in a video element, which leaves the react-webcam
dependency redundant.
Getting our hands dirty
The observation is that most alternatives use zxing
for decoding, so it is probably a safe bet.
So, we install the @zxing/library
package. Then, create a reader instance:
import { BrowserMultiFormatReader } from '@zxing/library';
const reader = new BrowserMultiFormatReader();
We can then use it's method decodeFromConstraints
to continuously detect codes from the stream and display it in a video element. The first argument takes a configuration object, the second the video element we're streaming to and the third argument a callback function to handle decoding results.
import { BrowserMultiFormatReader } from '@zxing/library';
let videoElement: HTMLVideoElement;
reader.decodeFromConstraints(
{
audio: false,
video: {
facingMode: 'environment',
},
},
videoElement,
(result, error) => {
if (result) console.log(result);
if (error) console.log(error);
}
);
React implementation
We can hold the video element in a reference, using the useRef
hook and start decoding with useEffect
. The most basic implementation would look like this.
const BarcodeScanner = () => {
const videoRef = useRef<HTMLVideoElement>(null);
const reader = useRef(new BrowserMultiFormatReader());
useEffect(() => {
if (!videoRef.current) return;
reader.current.decodeFromConstraints(
{
audio: false,
video: {
facingMode: 'environment',
},
},
videoRef.current,
(result, error) => {
if (result) console.log(result);
if (error) console.log(error);
}
);
return () => {
reader.current.reset();
}
}, [videoRef]);
return <video ref={videoRef} />;
};
For performance reasons it is important to only instantiate the BrowserMultiFormatReader
once using the useRef
hook and to clean up the useEffect
by calling the reset()
method of that instance.
Using a custom hook
Looking at the basic implementation we notice a few areas of improvement:
- The logic is coupled with the rendering of our video element
- We're not handling result or errors
- We do not allow any configuration by the
BarcodeScanner
consumer
We could improve it by extracting it to a custom hook, so that we can decouple the logic from how we want to render the video element in our application.
This would be the final implementation:
import { BrowserMultiFormatReader, DecodeHintType, Result } from '@zxing/library';
import { useEffect, useMemo, useRef } from 'react';
interface ZxingOptions {
hints?: Map<DecodeHintType, any>;
constraints?: MediaStreamConstraints;
timeBetweenDecodingAttempts?: number;
onResult?: (result: Result) => void;
onError?: (error: Error) => void;
}
const useZxing = ({
constraints = {
audio: false,
video: {
facingMode: 'environment',
},
},
hints,
timeBetweenDecodingAttempts = 300,
onResult = () => {},
onError = () => {},
}: ZxingOptions = {}) => {
const ref = useRef<HTMLVideoElement>(null);
const reader = useMemo<BrowserMultiFormatReader>(() => {
const instance = new BrowserMultiFormatReader(hints);
instance.timeBetweenDecodingAttempts = timeBetweenDecodingAttempts;
return instance;
}, [hints, timeBetweenDecodingAttempts]);
useEffect(() => {
if (!ref.current) return;
reader.decodeFromConstraints(constraints, ref.current, (result, error) => {
if (result) onResult(result);
if (error) onError(error);
});
return () => {
reader.reset();
};
}, [ref, reader]);
return { ref };
};
We could then consume it in a component like this:
export const BarcodeScanner: React.FC<BarcodeScannerProps> = ({
onResult = () => {},
onError = () => {},
}) => {
const { ref } = useZxing({ onResult, onError });
return <video ref={ref} />;
};
What did you think?
Please let me know by submitting a comment!
Top comments (3)
Thanks so much for sharing this. I didn't wind up going with this solution but it helped me ultimately get to what I needed.
I wound up using a Barcode Detection API polyfill package (which itself also depends on zxing). I would've had all that logic coupled with my component if not for this article, so thanks again!
I ran into an issue where the camera was not being turned off and it still continuously attempted to decode the barcode when I called various stop methods on the
reader
instance. I think if I was still going to use zxing'sBrowserMultiFormatReader
, I would set up a continuous detect using one of the other detection methods available on theBrowserMultiFormatReader
.Nice post. Is there an api that works with actual barcode scanners? I have this scanner: barcodediscount.com/catalog/motoro... and I want to be able to scan things directly into my react app.
Hi, How to enable EAN 13 barcode readings?