html5-qrcode
html5-qrcode copied to clipboard
Video duplication on page when using react strict mode
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:
- Create react application with
npx create-react-app my-app
- Write the code for pro qr code reader element
- Start the applciation
- See error on the UI
Expected behavior
Detect if once Html5Qrcode beeing called do not created another video element
Screenshots
Desktop:
- OS: Windows
- Browser chrome
- Version do not know
Smartphone: Didn't try still
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
Same problem. And thank you, I delete strict mode ,it works
Same problem on react: 18.2.0
with html5-qrcode: 2.2.1
. Works after disabling strict mode.
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 thank you man!! you saved my code ❤️
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} />;
};
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;