Skip to content

QR Scanner Hook with Simulated QR Codes

Implementation Example

tsx
import React from "react";
import useQRCodeReader from ".../useQRCodeReader";

const codes = ["QR1", "QR2", "QR3"];

const QRScannerExample: React.FC = () => {
  const { qrCodeData, resetQRCodeData, QRCodeReader } = useQRCodeReader({
    qrCodeParser: decodeQRCodeText, // Function to parse the scanned QR code text
    simulatedQR: true, // Enable simulated QR codes. Hit Enter to cycle through the codes
    qrCodes: codes, // Array of simulated QR codes
  });

  return (
    <>
      {/* Display a warning when the page is not focused */}
      <QRCodeReader />
    </>
  );
};

export default QRScannerExample;

QR Scanner Hook definition

typescript
import { useEffect, useState, useCallback } from "react";

type ParserFunction= (text: string) => any

export type QRCodeReaderHookProps<T extends ParserFunction> = {
  qrCodeParser: T
  qrCodes?: string[];
  throttleTime?: number;
  simulatedQR?: boolean;
};

/**
 * Custom hook to handle QR code reading functionality, including support for simulated QR codes.
 *
 * @param {function} qrCodeParser - Function to parse the scanned QR code text into the desired format.
 * @param {number} [throttleTime=500] - Optional throttle time (in milliseconds) to limit the frequency of QR code scans.
 * @param {boolean} [simulatedQR=false] - Optional flag to enable or disable the simulated QR code reader.
 * @param {Array<string>} [qrCodes=[]] - Optional array of simulated QR codes for testing or development purposes.
 *
 * @returns {Object} An object containing:
 * - `qrCodeData`: The most recently parsed QR code data.
 * - `resetQRCodeData`: A function to reset the parsed QR code data to null.
 * - `QRCodeReader`: A React component to display a warning when the page is not focused.
 *
 * @example
 * const { QRCodeReader, resetQRCodeData, qrCodeData } = useQRCodeReader({
 *   qrCodeParser: parseQRCode,
 *   throttleTime: 300,
 *   simulatedQR: true,
 *   qrCodes: ['QR1', 'QR2'],
 * });
 */
const useQRCodeReader = <T extends ParserFunction>({
  qrCodeParser,
  qrCodes = [],
  simulatedQR = false,
  throttleTime = 500,
}: QRCodeReaderHookProps<T>) => {
  const [isFocused, setIsFocused] = useState(false);
  const [qrCodeData, setQRCodeData] =
    useState<ReturnType<typeof qrCodeParser> | null>(null);

  const resetQRCodeData = useCallback(() => setQRCodeData(null), []);

  const throttledOnRead = useCallback(
    throttle((data: string) => {
      const decodedData = qrCodeParser(data);
      decodedData && setQRCodeData(decodedData);
    }, throttleTime),
    [qrCodeParser, throttleTime]
  );

  const readSimulatedQR = useSimulatedQRReader({
    enabled: simulatedQR,
    qrCodes,
  });

  // Handle QR Code reads
  useEffect(() => {
    let data = "";
    const listenQRCodeRead = (e: KeyboardEvent) => {
      if (e.key === "Enter") {
        throttledOnRead(readSimulatedQR() ?? data);
        data = "";
        return;
      }
      data += e.key;
    };
    document.addEventListener("keypress", listenQRCodeRead);
    return () => document.removeEventListener("keypress", listenQRCodeRead);
  }, [throttledOnRead, readSimulatedQR]);

  // Check if the document has focus so the USB scanner can read correctly
  useEffect(() => {
    const onFocus = () => setIsFocused(true);
    const onBlur = () => setIsFocused(false);
    setIsFocused(document.hasFocus());
    window.addEventListener("focus", onFocus);
    window.addEventListener("blur", onBlur);
    return () => {
      window.removeEventListener("focus", onFocus);
      window.removeEventListener("blur", onBlur);
    };
  }, []);

  // Define the QRCodeReader component
  const QRCodeReader = () => {
    if (isFocused) return null;

    return (
      <div className="absolute bottom-10 left-10 z-50 w-fit rounded-lg border border-red bg-white p-10 text-red">
        {"Page is not focused. QR code reader won't scan any data."}
      </div>
    );
  };

  return { qrCodeData, resetQRCodeData, QRCodeReader };
};

/**
 * Throttle function to limit the frequency of a given function's execution.
 *
 * @param {function} func - The function to be throttled.
 * @param {number} delay - The delay in milliseconds between function executions.
 *
 * @returns {function} A throttled version of the input function.
 */
export const throttle = <T extends any[]>(
  func: (...args: T) => any,
  delay: number
) => {
  let wait = false;
  return (...args: T) => {
    if (!wait) {
      func(...args);
      wait = true;
      setTimeout(() => {
        wait = false;
      }, delay);
    }
  };
};

interface SimulatedQRReaderProps {
  enabled?: boolean;
  qrCodes?: string[];
}

/**
 * Custom hook to simulate QR code reading using a predefined list of QR codes.
 *
 * @param {boolean} [enabled=true] - Flag to enable or disable the simulated QR code reader.
 * @param {Array<string>} [qrCodes=[]] - Array of simulated QR codes to be used by the reader.
 *
 * @returns {function} A function to simulate reading the next QR code from the list.
 */
export const useSimulatedQRReader = ({
  enabled = true,
  qrCodes = [],
}: SimulatedQRReaderProps) => {
  const [currentIndex, setCurrentIndex] = useState(0);

  useEffect(() => {
    if (enabled && qrCodes.length === 0) {
      console.error("No simulated QR codes provided.");
    }
  }, [qrCodes]);

  const readSimulatedQR = () => {
    if (!enabled) return null;
    const currentCode = qrCodes[currentIndex];
    const nextIndex = (currentIndex + 1) % qrCodes.length;
    setCurrentIndex(nextIndex);

    console.log("Reading simulated QR code", currentCode);
    return currentCode;
  };

  return readSimulatedQR;
};

export default useQRCodeReader;