Implementing a QR Code Scanner in React

Surajan Shrestha
readytowork, Inc.
Published in
9 min readJan 7, 2024
QR Code Scanner in React

QR Code Scanners are extremely popular and here’s how to implement it in React. It is simple enough to implement but if building from scratch, it might take your whole day.

Luckily, I’m here to help you out 🙌.

Problem 😵‍💫: Although we have a lot of plug-in-play types of QR Code Scanner packages dedicated to React, many are not that stable, some have bugs specific to a certain browser, some don’t have enough flexibility to use and some just lack certain features.

Solution ✅: I found out that QR Code Scanner packages that have a direct implementation with JavaScript, rather than specialize with a certain JS Framework, are much better and stable.

Package Used

I’ll be using the qr-scanner package which is pretty stable and is sponsored by nimiq (which is a browser-based blockchain and i guess they use this package in their application too).

Let’s Code

a. Folder Structure

I’m using vite to setup development environment. But, you can use anything of your choice, even create-react-app 😉.

-node_modules and others
-package.json
-src
-assets
-qr-frame.svg
-components
-QrReader.tsx
-QrStyles.css
-App.tsx
-main.tsx(or index.tsx if you're using create-react-app)
-App.css(this is an empty file)
-index.css(this is an empty file)

b. Install Package

For npm:

npm i qr-scanner

For yarn:

yarn add qr-scanner

c. Clean Unwanted CSS

I’ve deleted all the css from App.css and index.css so that i’ll have a clean slate to work on and also so that css styles don’t conflict each other.

Removing styles in bothApp.css and index.css👇:

/* No CSS Styles here */

For the purpose of simplifying css styles, we’ll only use one css file located in /components/QrStyles.css. This is all the css styles we’ll need for our QR Scanner👇:

.qr-reader {
width: 430px;
height: 100vh;
margin: 0 auto;
position: relative;
}

.qr-reader video {
width: 100%;
height: 100%;
object-fit: cover;
}

.qr-reader .qr-box {
width: 100% !important;
left: 0 !important;
}

.qr-reader .qr-frame {
position: absolute;
fill: none;
left: 50%;
top: 50%;
transform: translateX(-50%) translateY(-50%);
}

/* Media Queries for mobile screens */
@media (max-width: 426px) {
.qr-reader {
width: 100%;
}
}

d. Import Dependencies

Let’s make a QrReader component in the following route /components/QrReader.tsx and import qr-scanner, QrFrame and QrStyles.css:

// Styles
import "./QrStyles.css";

// Qr Scanner
import QrScanner from "qr-scanner";
import QrFrame from "../assets/qr-frame.svg";

const QrReader = () => {

}

export default QrReader;

e. Mount JSX Elements needed by our Qr Scanner

We’ll mainly be needing a <video> element where the camera renders and an <img> element to show our QrFrame.svg.

We’ll attach the videoEl and qrBoxEl that we got from useRef into their respective elements.

Apart from that we’ll be needing a bunch of <div> elements in the exact order and having the exact css classes as given below 👇:

import { useEffect, useRef, useState } from "react";

// Styles
import "./QrStyles.css";

// Qr Scanner
import QrScanner from "qr-scanner";
import QrFrame from "../assets/qr-frame.svg";

const QrReader = () => {
// QR States
const videoEl = useRef<HTMLVideoElement>(null);
const qrBoxEl = useRef<HTMLDivElement>(null);

return (
<div className="qr-reader">
{/* QR */}
<video ref={videoEl}></video>
<div ref={qrBoxEl} className="qr-box">
<img
src={QrFrame}
alt="Qr Frame"
width={256}
height={256}
className="qr-frame"
/>
</div>
</div>
);
}

export default QrReader;

f. Initialize QR Scanner

For initializing our Qr Scanner, we’ll need a scanner element from useRef and qrOn state & setQrOn state handler.

We’ll initialize the Qr Scanner in our useEffect. To do so, we’ll use the constructor function given by qr-scanner package i.e. new QrScanner() .

The 1st parameter in new QrScanner() is videoEl?.current where our camera renders, 2nd is onScanSuccess (we’ll get to that in next section) and 3rd is the options object containing properties like: onDecodeError, preferredCamera, highlightScanRegion, highlightCodeOutline and overlay . What each of these options do, is provided in the comments inside the code below 👇.

We start our Qr Scanner, using the scanner?.current promise function. It will either set qrOn state to true, if qr scanner is started successfully ✅, or will set it to false, if there was a problem in starting the scanner ❌.

We’ll stop our Qr Scanner when user goes to another page or our QrReader component unmounts (is removed from JavaScript’s DOM tree). When Qr Scanner is stopped, it will stop using the device’s camera 🎬. We’ll do it using the scanner?.current?.stop() function inside our useEffect’s cleanup function.

import { useEffect, useRef, useState } from "react";

// Styles
import "./QrStyles.css";

// Qr Scanner
import QrScanner from "qr-scanner";
import QrFrame from "../assets/qr-frame.svg";

const QrReader = () => {
// QR States
const scanner = useRef<QrScanner>();
const videoEl = useRef<HTMLVideoElement>(null);
const qrBoxEl = useRef<HTMLDivElement>(null);
const [qrOn, setQrOn] = useState<boolean>(true);

useEffect(() => {
if (videoEl?.current && !scanner.current) {
// 👉 Instantiate the QR Scanner
scanner.current = new QrScanner(videoEl?.current, onScanSuccess, {
onDecodeError: onScanFail,
// 📷 This is the camera facing mode. In mobile devices, "environment" means back camera and "user" means front camera.
preferredCamera: "environment",
// 🖼 This will help us position our "QrFrame.svg" so that user can only scan when qr code is put in between our QrFrame.svg.
highlightScanRegion: true,
// 🔥 This will produce a yellow (default color) outline around the qr code that we scan, showing a proof that our qr-scanner is scanning that qr code.
highlightCodeOutline: true,
// 📦 A custom div which will pair with "highlightScanRegion" option above 👆. This gives us full control over our scan region.
overlay: qrBoxEl?.current || undefined,
});

// 🚀 Start QR Scanner
scanner?.current
?.start()
.then(() => setQrOn(true))
.catch((err) => {
if (err) setQrOn(false);
});
}

// 🧹 Clean up on unmount.
// 🚨 This removes the QR Scanner from rendering and using camera when it is closed or removed from the UI.
return () => {
if (!videoEl?.current) {
scanner?.current?.stop();
}
};
}, []);

return (
<div className="qr-reader">
{/* QR */}
<video ref={videoEl}></video>
<div ref={qrBoxEl} className="qr-box">
<img
src={QrFrame}
alt="Qr Frame"
width={256}
height={256}
className="qr-frame"
/>
</div>
</div>
);
}

export default QrReader;

e. Handle Scanned Result and Failures

We’ll handle our scanned result using the onScanSuccess function which is passed as a 2nd parameter in the new Scanner() constructor function. When our Qr Scanner has successfully extracted the data from given Qr Code, it is passed in the result parameter in onScanSuccess function.

We’ll console.log the result (for debugging purposes) and also set the scannedResult state with our result’s data ✅. We’ll then show the scannedResult as an absolutely positioned <p> element that hovers on top of our camera view.

For handling failures, we’ll use the onScanFail function where we console.log our error ❌. The onScanFail function is passed inside the 3rd parameter object’s 1st property in new Scanner() the constructor function.

import { useEffect, useRef, useState } from "react";

// Styles
import "./QrStyles.css";

// Qr Scanner
import QrScanner from "qr-scanner";
import QrFrame from "../assets/qr-frame.svg";

const QrReader = () => {
// QR States
const scanner = useRef<QrScanner>();
const videoEl = useRef<HTMLVideoElement>(null);
const qrBoxEl = useRef<HTMLDivElement>(null);
const [qrOn, setQrOn] = useState<boolean>(true);

// Result
const [scannedResult, setScannedResult] = useState<string | undefined>("");

// Success
const onScanSuccess = (result: QrScanner.ScanResult) => {
// 🖨 Print the "result" to browser console.
console.log(result);
// ✅ Handle success.
// 😎 You can do whatever you want with the scanned result.
setScannedResult(result?.data);
};

// Fail
const onScanFail = (err: string | Error) => {
// 🖨 Print the "err" to browser console.
console.log(err);
};

useEffect(() => {
if (videoEl?.current && !scanner.current) {
// 👉 Instantiate the QR Scanner
scanner.current = new QrScanner(videoEl?.current, onScanSuccess, {
onDecodeError: onScanFail,
// 📷 This is the camera facing mode. In mobile devices, "environment" means back camera and "user" means front camera.
preferredCamera: "environment",
// 🖼 This will help us position our "QrFrame.svg" so that user can only scan when qr code is put in between our QrFrame.svg.
highlightScanRegion: true,
// 🔥 This will produce a yellow (default color) outline around the qr code that we scan, showing a proof that our qr-scanner is scanning that qr code.
highlightCodeOutline: true,
// 📦 A custom div which will pair with "highlightScanRegion" option above 👆. This gives us full control over our scan region.
overlay: qrBoxEl?.current || undefined,
});

// 🚀 Start QR Scanner
scanner?.current
?.start()
.then(() => setQrOn(true))
.catch((err) => {
if (err) setQrOn(false);
});
}

// 🧹 Clean up on unmount.
// 🚨 This removes the QR Scanner from rendering and using camera when it is closed or removed from the UI.
return () => {
if (!videoEl?.current) {
scanner?.current?.stop();
}
};
}, []);

return (
<div className="qr-reader">
{/* QR */}
<video ref={videoEl}></video>
<div ref={qrBoxEl} className="qr-box">
<img
src={QrFrame}
alt="Qr Frame"
width={256}
height={256}
className="qr-frame"
/>
</div>

{/* Show Data Result if scan is success */}
{scannedResult && (
<p
style={{
position: "absolute",
top: 0,
left: 0,
zIndex: 99999,
color: "white",
}}
>
Scanned Result: {scannedResult}
</p>
)}
</div>
);
};

export default QrReader;

f. The Full Code 👇
(And, handling errors when Camera is not allowed in Browser Permissions)

If user has blocked camera permissions in their browser, we’ll show a JavaScript alert using the alert() function 😎.

The Final Code is given below 👇:

import { useEffect, useRef, useState } from "react";

// Styles
import "./QrStyles.css";

// Qr Scanner
import QrScanner from "qr-scanner";
import QrFrame from "../assets/qr-frame.svg";

const QrReader = () => {
// QR States
const scanner = useRef<QrScanner>();
const videoEl = useRef<HTMLVideoElement>(null);
const qrBoxEl = useRef<HTMLDivElement>(null);
const [qrOn, setQrOn] = useState<boolean>(true);

// Result
const [scannedResult, setScannedResult] = useState<string | undefined>("");

// Success
const onScanSuccess = (result: QrScanner.ScanResult) => {
// 🖨 Print the "result" to browser console.
console.log(result);
// ✅ Handle success.
// 😎 You can do whatever you want with the scanned result.
setScannedResult(result?.data);
};

// Fail
const onScanFail = (err: string | Error) => {
// 🖨 Print the "err" to browser console.
console.log(err);
};

useEffect(() => {
if (videoEl?.current && !scanner.current) {
// 👉 Instantiate the QR Scanner
scanner.current = new QrScanner(videoEl?.current, onScanSuccess, {
onDecodeError: onScanFail,
// 📷 This is the camera facing mode. In mobile devices, "environment" means back camera and "user" means front camera.
preferredCamera: "environment",
// 🖼 This will help us position our "QrFrame.svg" so that user can only scan when qr code is put in between our QrFrame.svg.
highlightScanRegion: true,
// 🔥 This will produce a yellow (default color) outline around the qr code that we scan, showing a proof that our qr-scanner is scanning that qr code.
highlightCodeOutline: true,
// 📦 A custom div which will pair with "highlightScanRegion" option above 👆. This gives us full control over our scan region.
overlay: qrBoxEl?.current || undefined,
});

// 🚀 Start QR Scanner
scanner?.current
?.start()
.then(() => setQrOn(true))
.catch((err) => {
if (err) setQrOn(false);
});
}

// 🧹 Clean up on unmount.
// 🚨 This removes the QR Scanner from rendering and using camera when it is closed or removed from the UI.
return () => {
if (!videoEl?.current) {
scanner?.current?.stop();
}
};
}, []);

// ❌ If "camera" is not allowed in browser permissions, show an alert.
useEffect(() => {
if (!qrOn)
alert(
"Camera is blocked or not accessible. Please allow camera in your browser permissions and Reload."
);
}, [qrOn]);

return (
<div className="qr-reader">
{/* QR */}
<video ref={videoEl}></video>
<div ref={qrBoxEl} className="qr-box">
<img
src={QrFrame}
alt="Qr Frame"
width={256}
height={256}
className="qr-frame"
/>
</div>

{/* Show Data Result if scan is success */}
{scannedResult && (
<p
style={{
position: "absolute",
top: 0,
left: 0,
zIndex: 99999,
color: "white",
}}
>
Scanned Result: {scannedResult}
</p>
)}
</div>
);
};

export default QrReader;

Results

For the purpose of seeing if our QR Scanner works, i’ve used a QR Code which when scanned should extract my Github profile’s link 👇: https://github.com/SurajanShrestha

a. On a Desktop/Laptop Browser 💻

On Success ✅:

QR Scan Success

When the camera is not allowed in Browser Permissions ❌:

Show alert when camera is not allowed in Browser Permissions

b. On a Mobile Browser 📱

QR Scan in Mobile Phone

If you’re at this section i.e. the end, I want you to know that I’m forever grateful for giving my article your valuable time and energy. If this article was helpful to you, please feel free to clap for it 👏.

Again, thank you, and keep coding 😎.

Quote: Every man has two lives. The second one begins, when he realizes that he only has one.

--

--

Surajan Shrestha
readytowork, Inc.

A Software Engineer 💻 | Codes in 👨‍💻: JavaScript, TypeScript & Go | Stacks 🧠: React, React Native, Next.js, Node, Express, Go-Gin, SQL, NoSQL and AWS.