Context
Barcode scanners are commonly used in various industries for scanning barcodes quickly and efficiently.
Imagine a UX in which users can:
-
- Write the barcode manually within an <Input field to find a certain product, selecting it and unloading it.
-
- Scan the barcode with the gun so that the <Input field is populated.
( which can be easily achieved with an input given that a barcode scanning is nothing more than a keyboard input followed by the enter submit.)
- Scan the barcode with the gun so that the <Input field is populated.
// this is how you can "Simulate" barcode gun scanning in a cypress test.
cy.get("body")
.type(product.barcode)
.trigger("keydown", { key: "Enter", which: 13 });
Now imagine a third scenario:
-
- Listen for a barcode input in any section of the application without the need of relying on a dedicated <Input field
- Listen for a barcode input in any section of the application without the need of relying on a dedicated <Input field
With this we can call the Unload API directly to speed up the unloading process or just open an unload modal everywhere and just unload our products! To cover this UX we need to develop a listener that does the barcode scanning process on a low level.
Implementing the barcode gun listener
Configuration
Before we dive into the implementation details, let’s take a look at the configuration options for our barcode scanner
import { useCallback, useEffect, useState } from "react";
import * as Beep from "./beep.mp3"; // Beep scanning sound
interface BufferCharacter {
time: number;
char: string;
}
interface BarcodeScannerConfig {
readonly timeToEvaluate?: number;
readonly averageWaitTime?: number;
readonly startCharacter?: ReadonlyArray<string>;
readonly endCharacter?: ReadonlyArray<string>;
readonly onComplete: (code: string) => void;
readonly onError?: (error: string) => void;
readonly minLength?: number;
readonly ignoreIfFocusOn?: Node;
readonly stopPropagation?: boolean;
readonly preventDefault?: boolean;
readonly acousticSignal?: boolean;
}
The useBarcodeScanner hook function
We’ll implement the barcode scanner as a custom React hook.
/**
* @info
* https://it.wikipedia.org/wiki/Global_Trade_Item_Number
*/
export const useBarcodeScanner = ({
timeToEvaluate = 100,
averageWaitTime = 50,
startCharacter = [],
endCharacter = ["escape", "enter"],
onComplete,
onError,
minLength = 1,
ignoreIfFocusOn,
stopPropagation = false,
preventDefault = false,
acousticSignal = true,
}: BarcodeScannerConfig): void => {
const [buffer, setBuffer] = useState<ReadonlyArray<BufferCharacter>>([]);
const [timeoutState, setTimeoutState] = useState<number>(0);
const clearBuffer = (): void => {
setBuffer([]);
};
const evaluateBuffer = useCallback((): void => {
clearTimeout(timeoutState);
const sum = buffer
.map(({ time }, k, arr) => (k > 0 ? time - arr[k - 1].time : 0))
.slice(1)
.reduce((total, delta) => total + delta, 0);
const avg = sum / (buffer.length - 1);
const code = buffer
.slice(startCharacter.length > 0 ? 1 : 0)
.map(({ char }) => char)
.join("");
const bufferLength = buffer.slice(startCharacter.length > 0 ? 1 : 0).length;
if (avg <= averageWaitTime && bufferLength >= minLength) {
if (isNaN(code as unknown as number)) return onError?.(code);
if (acousticSignal) {
const audio = new Audio(Beep);
audio.play();
}
onComplete(code);
} else {
if (avg <= averageWaitTime && !!onError) {
onError(code);
}
}
clearBuffer();
}, [
acousticSignal,
averageWaitTime,
buffer,
minLength,
onComplete,
onError,
startCharacter.length,
timeoutState,
]);
const onKeyDown = useCallback(
(event: KeyboardEvent) => {
if (event.currentTarget === ignoreIfFocusOn) return; // REF ?
const kbdKey = event.key.toLowerCase();
const isLastChar = endCharacter.includes(kbdKey);
if (isLastChar) evaluateBuffer();
if (
!isLastChar &&
(buffer.length > 0 ||
startCharacter.includes(kbdKey) ||
startCharacter.length === 0)
) {
clearTimeout(timeoutState);
setTimeoutState(
setTimeout(evaluateBuffer, timeToEvaluate) as unknown as number
);
setBuffer(prev => [...prev, { time: performance.now(), char: kbdKey }]);
}
if (stopPropagation) event.stopPropagation();
if (preventDefault) event.preventDefault();
},
[
ignoreIfFocusOn,
endCharacter,
buffer,
startCharacter,
stopPropagation,
preventDefault,
evaluateBuffer,
timeoutState,
timeToEvaluate,
]
);
useEffect(
() => (): void => {
clearTimeout(timeoutState);
},
[timeoutState]
);
useEffect(() => {
document.addEventListener("keydown", onKeyDown as EventListener);
return (): void => {
document.removeEventListener("keydown", onKeyDown as EventListener);
};
}, [onKeyDown]);
};
implementing the barcode gun listener
Now that we have our useBarcodeScanner hook, let’s see how we can use it to implement the barcode gun listener in our application:
useBarcodeScanner({
timeToEvaluate: 800,
averageWaitTime: 16,
stopPropagation: true,
acousticSignal: false,
onComplete: (
barcode // fired when detects a valid barcode
) =>
query(
// product lookup by barcode on the server
{ variables: { q: barcode } },
{
onError: console.error,
onCompleted: handleSelectedProduct; // handle the retrieved product
}
),
});
In this example, we pass the necessary configuration options to the useBarcodeScanner hook. We set timeToEvaluate to 800 milliseconds and averageWaitTime to 16 milliseconds, which determines the threshold for recognizing a valid barcode. When a valid barcode is detected, the onComplete callback is called, and we can perform the desired action, such as querying the server for the product details.