How to scan barcodes in your React.js application
Tags: react, zxing, typescript, tutorial
Published: 2022-08-14 21:36:00

> _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](https://www.npmjs.com/package/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](https://www.reddit.com/r/reactjs/comments/wnbrad/barcode_scanner/) seemed to attract some interest.

## What are we building?
I wanted to use a barcode scanner for my side project [Snxbox](https://snxbox.app/). 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`](https://www.npmjs.com/package/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](https://github.com/zxing-js/library/issues/351) 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`](https://github.com/ericblade/quagga2)
This is another package that extends `zxing`. I found an example on how to use it with React but honestly [it seemed daunting](https://github.com/ericblade/quagga2-react-example/blob/master/src/components/Scanner.js).

### [`html5-qrcode`](https://github.com/mebjas/html5-qrcode)
Yet another package extending `zxing`. The implementation was [a bit easier to follow](https://github.com/scanapp-org/html5-qrcode-react/blob/main/src/Html5QrcodePlugin.jsx) 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](https://developer.mozilla.org/en-US/docs/Web/API/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](https://github.com/jamenamcinteer/react-qr-barcode-scanner/blob/main/src/BarcodeScannerComponent.tsx) to begin with.

Also, `react-qr-barcode-scanner` uses [`react-webcam`](https://www.npmjs.com/package/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:

```typescript
import { BrowserMultiFormatReader } from '@zxing/library';

const reader = new BrowserMultiFormatReader();
```

We can then use it's method [`decodeFromConstraints`](https://github.com/zxing-js/library/blob/master/src/browser/BrowserCodeReader.ts#L335) 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.

```typescript
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.

```tsx
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:

```tsx
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:

```tsx
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!