html5-qrcode icon indicating copy to clipboard operation
html5-qrcode copied to clipboard

Video duplication on page when using react strict mode

Open daviatorstorm opened this issue 2 years ago • 7 comments

Describe the bug I'm in progress to create a application for mobile and PWA tech. Using react.js in strict mode. I'm trying to create page with auto open camera to scan QR code but on the page I can see two previews of camera. Using a pro qr code mode. To Reproduce Steps to reproduce the behavior:

  1. Create react application with npx create-react-app my-app
  2. Write the code for pro qr code reader element
  3. Start the applciation
  4. See error on the UI

Expected behavior Detect if once Html5Qrcode beeing called do not created another video element Screenshots image image image image image

Desktop:

  • OS: Windows
  • Browser chrome
  • Version do not know

Smartphone: Didn't try still

daviatorstorm avatar Jul 03 '22 12:07 daviatorstorm

I'm using react 18, so it show me an error Uncaught (in promise) TypeError: react_dom_client__WEBPACK_IMPORTED_MODULE_1__.render is not a function at index.js:14:1

and the ReactDOM render function missing image

daviatorstorm avatar Jul 07 '22 11:07 daviatorstorm

Same problem. And thank you, I delete strict mode ,it works

Evanna51 avatar Jul 28 '22 03:07 Evanna51

Same problem on react: 18.2.0 with html5-qrcode: 2.2.1. Works after disabling strict mode.

MatthewTang avatar Aug 08 '22 14:08 MatthewTang

This is expected behaviour. You should clean up your useEffect properly by stopping the video stream, like so:

import { Box } from '@chakra-ui/react';
import { Html5Qrcode, Html5QrcodeSupportedFormats } from 'html5-qrcode';
import { useCallback, useEffect, useRef } from 'react';
import { BarcodeScannerProps } from './props';

export const BarcodeScanner: React.FC<BarcodeScannerProps> = ({
  onResult = () => {},
  onError = () => {},
}) => {
  const previewRef = useRef<HTMLDivElement>(null);
  const memoizedResultHandler = useRef(onResult);
  const memoizedErrorHandler = useRef(onError);

  useEffect(() => {
    memoizedResultHandler.current = onResult;
  }, [onResult]);

  useEffect(() => {
    memoizedErrorHandler.current = onError;
  }, [onError]);

  useEffect(() => {
    if (!previewRef.current) return;
    const html5QrcodeScanner = new Html5Qrcode(previewRef.current.id);
    const didStart = html5QrcodeScanner
      .start(
        { facingMode: 'environment' },
        { fps: 10 },
        (_, { result }) => {
          memoizedResultHandler.current(result);
        },
        (_, error) => {
          memoizedErrorHandler.current(error);
        }
      )
      .then(() => true);
    return () => {
      didStart
        .then(() => html5QrcodeScanner.stop())
        .catch(() => {
          console.log('Error stopping scanner');
        });
    };
  }, [previewRef, memoizedResultHandler, memoizedErrorHandler]);

  return (
    <div
      id="preview"
      ref={previewRef}
    />
  );
};

adamalfredsson avatar Aug 14 '22 12:08 adamalfredsson

@adamalfredsson thank you man!! you saved my code ❤️

danielthames360 avatar Jun 14 '23 13:06 danielthames360

Here is another workaround until the race condition is fixed. In this example I use ref to avoid initializing the scanner object twice. Additionally I check the DOM to avoid rendering the contents twice, and use timeout to avoid the race condition.

Using the ref object is also a workaround for scanner not stopping after 'clear' #796

const scanRegionId = "html5qr-code-full-region";

type scanProps = Html5QrcodeScannerConfig & {
  qrCodeSuccessCallback: QrcodeSuccessCallback;
  verbose?: boolean;
  qrCodeErrorCallback?: QrcodeErrorCallback;
};

export const Html5QrcodeScan = (props: scanProps) => {
  const { qrCodeSuccessCallback, qrCodeErrorCallback, verbose } = props;
  const ref = useRef<Html5QrcodeScanner | null>(null);

  useEffect(() => {
    // Use reference to avoid recreating the object when double rendered in Dev Strict Mode.
    if (ref.current === null) {
      ref.current = new Html5QrcodeScanner(scanRegionId, { ...props }, verbose);
    }
    const html5QrcodeScanner = ref.current;

    // Timeout to allow the clean-up function to finish in case of double render.
    setTimeout(() => {
      const container = document.getElementById(scanRegionId);
      if (html5QrcodeScanner && container?.innerHTML == "") {
        html5QrcodeScanner.render(qrCodeSuccessCallback, qrCodeErrorCallback);
      }
    }, 0);

    return () => {
      if (html5QrcodeScanner) {
        html5QrcodeScanner.clear();
      }
    };
    // Just once when the component mounts.
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  return <div id={scanRegionId} />;
};

rumyanar avatar Sep 08 '23 22:09 rumyanar

Getting the same error

import { Html5Qrcode } from "html5-qrcode";
import { ChangeEvent, useEffect, useRef, useState } from "react";

// assuming this is the correct path

type QrScannerConfig = {
  fps: number;
  qrbox: { width: number; height: number };
  aspectRatio: number;
};

type QrScannerErrorCallback = (errorMessage: string) => void;
type QrScannerSuccessCallback = (decodedText: string, result?: any) => void;

const useQrScanner = (
  config: QrScannerConfig,
  qrCodeSuccessCallback: QrScannerSuccessCallback,
  qrCodeErrorCallback?: QrScannerErrorCallback
) => {
  const [error, setError] = useState<string | null>(null);
  const previewRef = useRef<HTMLDivElement>(null);
  const html5QrCodeRef = useRef<Html5Qrcode | null>(null);

  useEffect(() => {
    if (!previewRef.current) return;
    Html5Qrcode.getCameras().then((devices) => {
      console.log(devices);
    });
    html5QrCodeRef.current = new Html5Qrcode(previewRef.current.id);
    const html5QrCode = html5QrCodeRef.current;

    html5QrCode
      .start(
        { facingMode: "environment" },
        config,
        qrCodeSuccessCallback,
        (errorMessage: string) => {
          qrCodeErrorCallback?.(errorMessage);
          // setError(errorMessage);
        }
      )
      .catch((error) => {
        console.error("QR Scanner initialization failed:", error);
        setError(error.message);
      });

    return () => {
      html5QrCode
        .stop()
        .then(() => {
          console.log("Scanner stopped successfully");
          html5QrCode.clear();
        })
        .catch((error) => {
          console.error("Error stopping scanner:", error);
        });
    };
  }, [previewRef, config, qrCodeErrorCallback, qrCodeSuccessCallback]);

  const handleFileUpload = async (e: ChangeEvent<HTMLInputElement>) => {
    const files = e.target.files;
    console.log({ e, files });
    if (!previewRef.current || !files || !html5QrCodeRef.current) return;

    if (!files || files.length === 0) {
      console.error("No file selected for scanning");
      setError("No file selected for scanning");
      return;
    }

    const file = files[0]; // Get the first file

    if (!(file instanceof File)) {
      console.error("Selected item is not a file");
      setError("Selected item is not a file");
      return;
    }

    const html5QrCode = html5QrCodeRef.current;

    try {
      // Stop the ongoing scan if any
      if (html5QrCode.isScanning) {
        await html5QrCode.stop();
      }
      const decodedText = await html5QrCode.scanFile(file, true);
      console.log({ decodedText });
      qrCodeSuccessCallback(decodedText);
    } catch (err) {
      console.error(`Error scanning file. Reason: ${err}`, err);
      setError(`Error scanning file.`);
    } finally {
      setTimeout(async () => {
        html5QrCode.clear();
        try {
          await html5QrCode
            .start(
              { facingMode: "environment" },
              config,
              qrCodeSuccessCallback,
              (errorMessage: string) => {
                qrCodeErrorCallback?.(errorMessage);
                // setError(errorMessage);
              }
            )
            .catch((error) => {
              console.error("QR Scanner initialization failed:", error);
              setError(error.message);
            });
        } catch (err) {
          console.error(`Error restarting scanner. Reason: ${err}`);
        }
      }, 1000);
    }
  };

  return { previewRef, handleFileUpload, error };
};

export default useQrScanner;

import { Box, Stack, Typography, useMediaQuery, useTheme } from "@mui/material";
import React from "react";

import { BackButton, UploadFile } from "shared/components";
import { getIcon } from "shared/icons";

import useQrScanner from "./hooks/useQrScanner";

const Cross = getIcon("close");

type Props = {
  qrCodeSuccessCallback: (decodedText: string, result?: any) => void;
  goBack: () => void;
};

const UpiQrScannermodule = ({ goBack, qrCodeSuccessCallback }: Props) => {
  const theme = useTheme();
  const isMobile = useMediaQuery(theme.breakpoints.down("md"));

  const config = {
    fps: 10,
    qrbox: { width: 250, height: 250 },
    aspectRatio: isMobile ? 0.5 : 2,
  };

  const { previewRef, handleFileUpload, error } = useQrScanner(
    config,
    qrCodeSuccessCallback
  );

  const close = () => {
    goBack();
  };

  return (
    <Box
      sx={{
        width: "100%",
        height: "100%",
        display: "flex",
        flexDirection: "column",
        alignItems: "center",
        justifyContent: "flex-start",
        margin: "0 auto",
        overflow: "hidden",
      }}
    >
      <Stack
        width={"100%"}
        alignItems={"center"}
        direction={"row"}
        justifyContent={"space-between"}
      >
        <Stack>
          <BackButton handleClick={close} size="small" />
        </Stack>
        <Typography variant="h6" sx={{ color: "gray.700", flex: 1 }}>
          Scan QR code
        </Typography>
      </Stack>
      {/* <Box sx={{ position: "relative" }}> */}
      <Box
        id="qrScanner"
        ref={previewRef}
        sx={{
          width: isMobile ? "100%" : "80%",
          border: "1px solid",
          borderColor: "grey.300",
          borderRadius: 1,
        }}
      >
        <video muted style={{ width: "100%", height: "100%" }} />
        {/* </Box> */}
      </Box>
      <Stack
        direction="column"
        justifyContent="center"
        alignItems="center"
        spacing={2}
        sx={{ width: "100%", mt: 2 }}
      >
        <UploadFile
          color="primary"
          handleFileUpload={handleFileUpload}
          label="Upload from gallery"
          acceptType=".pdf, .jpg, .jpeg, .png, .gif, .bmp, .tiff"
        />

        {error && (
          <Box
            sx={{
              display: "flex",
              flexDirection: "column",
              alignItems: "center",
              backgroundColor: "error.light",
              color: "error.contrastText",
              maxWidth: "30rem",
              p: 2,
              borderRadius: 1,
            }}
          >
            <Cross />
            <Typography variant="body1" fontWeight={700} p={2}>
              {error}
            </Typography>
          </Box>
        )}
      </Stack>
    </Box>
  );
};

export default UpiQrScannermodule;

vipansh avatar Jan 11 '24 19:01 vipansh