DEV Community πŸ‘©β€πŸ’»πŸ‘¨β€πŸ’»

DEV Community πŸ‘©β€πŸ’»πŸ‘¨β€πŸ’» is a community of 966,155 amazing developers

We're a place where coders share, stay up-to-date and grow their careers.

Create account Log in
Cover image for How to scan barcodes in your React.js application
Adam Alfredsson
Adam Alfredsson

Posted on

How to scan barcodes in your React.js application

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();
Enter fullscreen mode Exit fullscreen mode

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);
  }
);
Enter fullscreen mode Exit fullscreen mode

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} />;
};
Enter fullscreen mode Exit fullscreen mode

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 };
};
Enter fullscreen mode Exit fullscreen mode

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} />;
};
Enter fullscreen mode Exit fullscreen mode

What did you think?

Please let me know by submitting a comment!

Top comments (0)

🌚 Friends don't let friends browse without dark mode.

Sorry, it's true.