Want to build a Book Library Barcode Scanner with ReactJS? Whether you’re:
- Managing a personal book collection
- Setting up a library management system
- Doing a fun side project
- Integrating the book scanning feature inside an e-commerce store
This guide will show you exactly how to turn your React app into a smart tool that scans book barcodes and fetches details instantly with book lookup using the Open Library API.
No mobile app. No bulky barcode reader. Just use your laptop or phone’s camera inside the browser.
The result? A clean, React app that lets you scan any ISBN barcode and instantly see the book’s title, author, and cover image.
Demo Video:
Prerequisite Required: It is important to read our previous guide on scanning barcodes in ReactJS. That will cover the installation of STRICH (which is an SDK for scanning barcodes within your web application).
Let’s get started.
Note: At this stage, I’m expecting that you have already read our barcode scanning tutorial and can scan barcodes. Because, from now on, you will scan the barcode of a book and learn to fetch book details from an external source (Open Library).
Fetch Book Details
Once the barcode scanner (STRICH) captures an ISBN of a book, the next step is to retrieve the book’s details, such as the title, author, and cover image. You’ve got a few different ways to make this work depending on your project needs:
Use an External API
The easiest option is to connect to a public API that already has a huge collection of books.
- Open Library API: Free and community-driven. Just send the ISBN and get back the book metadata.
- Google Books API: Great if you want richer details like descriptions, categories, and ratings.
- ISBNdb API: A paid option that offers reliable and extensive ISBN coverage.
This is the quickest way to get started, since you don’t have to maintain your own database.
Query Your Own Database
If you’re building a library management system or need more control, you can store book data in your own database (PostgreSQL, MySQL, MongoDB, etc.). When the scanner reads an ISBN, your app queries your database to fetch the details.
This works best if you already have a curated collection of books and don’t want to rely on external services.
Hybrid Approach
A powerful strategy is to combine both:
- First, check your own database.
- If the book isn’t there, fall back to an external API.
- Save the new details in your database for next time.
This approach keeps your app fast, reduces API calls, and ensures your library grows automatically as you scan more books.
Preloaded / Offline Data
For small projects or demos, you can preload a static JSON file with a list of books and their ISBNs. This way, the scanner can fetch details without any internet connection.
Of course, this won’t scale for large collections, but it’s a handy option for prototypes.
Fetch Book Details from Open Library
We’re using the Open Library API for fetching book details since it’s free, simple, and requires no setup. Later, you can always swap it out for your own database or even integrate Google Books for richer data.
The process is simple. We need an ISBN code to get the records from Open Library. Below are the details that we need to display in our application:
- Title
- Author
- Cover Image
You can explore Open Library API to fetch more advanced book details, such as publisher, subtitle, description, publication date, and more.
First, I’m creating a new lib folder inside the src directory of our application. Within the lib, create a new file called openLibrary.ts
Here are quick step-by-step instructions for code:
Type Definition
export type BookDetails = {
isbn: string;
title: string;
authors: string;
thumbnail: string | null;
};
- Defines a TypeScript type for book information
- isbn: The book’s ISBN (string)
- title: The book’s title (string)
- authors: Author names as a comma-separated string
- thumbnail: Book cover image URL, can be null if no image available
Note: If you are fetching more information from the library, create the type of that property here.
Function Declaration
export async function fetchFromOpenLibrary(
isbn: string
): Promise<BookDetails | null>
- Exports an async function named fetchFromOpenLibrary
- Takes one parameter: isbn (string)
- Returns a Promise that resolves to either the BookDetails object or null
- null means no book was found, or an error occurred
API Request
const response = await fetch(
`https://openlibrary.org/api/books?bibkeys=ISBN:${isbn}&format=json&jscmd=data`
);
const data = await response.json();
- Makes an HTTP request to the Open Library API
- URL breakdown:
- bibkeys=ISBN:${isbn}: Tells API to search by ISBN
- format=json: Want response in JSON format
- jscmd=data: Requests detailed book data
- Converts response to a JSON object
Data Processing Setup
const bookKey = `ISBN:${isbn}`;
if (data[bookKey]) {
const book = data[bookKey];
- Creates the key that Open Library uses: “ISBN:1234567890”
- Checks if the API returned data for this ISBN
- If found, extracts the book object for processing
Book Data Extraction
return {
isbn: isbn,
title: book.title || "Unknown Title",
authors: book.authors
? book.authors
.map((a: { name: string; url: string }) => a.name)
.join(", ")
: "Unknown Author",
thumbnail:
book.cover?.medium ||
book.cover?.small ||
"https://placeholdit.com/180x300/dddddd/999999",
};
ISBN: Returns the original ISBN that was searched
Title:
- Uses a book.title if available
- Falls back to “Unknown Title” if missing
Authors:
- If authors exist: extracts each author’s name and joins with commas
- Author format from API:
[{name: "Author Name", url: "..."}, ...]
- Maps to just the names: “Author 1, Author 2, Author 3”
- Falls back to “Unknown Author” if no authors
Thumbnail:
- Tries to get a medium-sized cover image first
- Falls back to small cover if medium is not available
- Uses placeholder image service if no covers exist
- The ?. operator safely checks if book.cover exists
Complete Code of openLibrary.ts
export type BookDetails = {
isbn: string;
title: string;
authors: string;
thumbnail: string | null;
};
export async function fetchFromOpenLibrary(
isbn: string
): Promise<BookDetails | null> {
try {
const response = await fetch(
`https://openlibrary.org/api/books?bibkeys=ISBN:${isbn}&format=json&jscmd=data`
);
const data = await response.json();
const bookKey = `ISBN:${isbn}`;
if (data[bookKey]) {
const book = data[bookKey];
return {
isbn: isbn,
title: book.title || "Unknown Title",
authors: book.authors
? book.authors
.map((a: { name: string; url: string }) => a.name)
.join(", ")
: "Unknown Author",
thumbnail:
book.cover?.medium ||
book.cover?.small ||
"https://placeholdit.com/180x300/dddddd/999999",
};
}
return null;
} catch (error) {
console.error("Open Library API error:", error);
return null;
}
}
Now, we can fetch the record of a book from Open Library. What’s next?
Extend Barcode Scanner
Building on our previous barcode scanning guide, we are now converting the code of the BarcodeScanner component into a book scanner.
Imports & Setup
import { BarcodeReader } from "@pixelverse/strichjs-sdk";
import { useEffect, useRef, useState } from "react";
import useStrichSDK from "../hooks/useStrichSDK";
import { fetchFromOpenLibrary, type BookDetails } from "../lib/openLibrary";
What’s new:
- Added import for the fetchFromOpenLibrary function we discussed earlier
- Imports the BookDetails type for TypeScript typing
State Management
const [books, setBooks] = useState<BookDetails[]>([]);
const lastIsbnRef = useRef<string | null>(null);
const readerRef = useRef<BarcodeReader | null>(null);
const scannerContainerRef = useRef<HTMLDivElement>(null);
const [isScanning, setIsScanning] = useState(false);
Key changes:
- books: Now stores an array of BookDetails objects instead of raw detections
- lastIsbnRef: Tracks the last scanned ISBN to prevent duplicate API calls
- Removed the generic detections state. Now we store complete book information.
Modified Detection Handler
instance.detected = (detections) => {
const latest = detections[0];
// Only fetch if new ISBN detected
if (latest?.data && latest.data !== lastIsbnRef.current) {
lastIsbnRef.current = latest.data;
fetchBookDetails(latest.data);
}
};
How it works:
- Takes the first/latest detection from the array
- Compares with lastIsbnRef to avoid duplicate API calls
- Only calls fetchBookDetails for new ISBNs
- Updates the reference to the current ISBN
Book Fetching Function
const fetchBookDetails = async (isbn: string) => {
try {
const bookData = await fetchFromOpenLibrary(isbn);
if (bookData) {
setBooks((prev) => {
if (prev.some((b) => b.isbn === bookData.isbn)) return prev;
return [...prev, bookData];
});
}
} catch (err) {
console.log(err);
}
};
What it does:
- Calls the Open Library API with the scanned ISBN
- If book data is found, adds it to the books array
- Duplicate prevention: Checks if the ISBN already exists before adding
- Error handling: Logs any errors but doesn’t crash the app
Updated Clear Function
const clearDetections = () => {
setBooks([]);
lastIsbnRef.current = null;
};
Changes:
- Now clears the books array instead of detections
- Resets the lastIsbnRef so the same ISBN can be scanned again
Improved UI Layout
<div>
<h2 className="text-xl font-bold mb-4">
Currently, Library has {books.length} books
</h2>
{books.length === 0 && <p>Books not found</p>}
<div className="flex flex-col gap-3 max-h-[260px] h-full overflow-y-scroll">
{books.map((book) => (
<div className="border rounded p-3 gap-3 flex border-gray-300" key={book.isbn}>
{book.thumbnail && (
<img className="flex h-12" src={book.thumbnail} alt={book.title} />
)}
<div className="flex flex-col">
<h2 className="font-bold text-xl flex">{book.title}</h2>
<p>Author: <span>{book.authors}</span></p>
</div>
</div>
))}
</div>
</div>
- Uses CSS Grid with two columns on medium screens and up
- Left/Top Side: Book library display
- Right/Bottom side: Scanner interface
- Dynamic counter: Shows the total number of books scanned
- Empty state: Shows “Books not found” when no books are scanned
- Scrollable list: Fixed height with scroll for many books
- Book cards: Each book shows:
- Thumbnail image (if available)
- Title in bold
- Author name(s)
- Responsive images: Thumbnails are fixed height (48px)
How It All Works Together
Initialization:
- SDK loads → Scanner initializes → Ready to scan
Scanning Process:
- User clicks “Start Scanner” → Camera activates
- User points the camera at the book barcode → ISBN detected
- System checks if this ISBN is new → Calls Open Library API
- Book data retrieved → Added to library display
Duplicate Handling:
- Same barcode scanned again → No API call made
- Prevents unnecessary network requests and duplicates
Library Management:
- All scanned books appear in the left panel
- Books persist until “Clear Results” is clicked
- Real-time counter shows library size
Complete Code of Updated BarcodeScanner Component
import { BarcodeReader } from "@pixelverse/strichjs-sdk";
import { useEffect, useRef, useState } from "react";
import useStrichSDK from "../hooks/useStrichSDK";
import { fetchFromOpenLibrary, type BookDetails } from "../lib/openLibrary";
const BarcodeScanner = () => {
const { isStrichInit, strichInitError } = useStrichSDK(
import.meta.env.VITE_STRICH_LICENSE_KEY
);
const [books, setBooks] = useState<BookDetails[]>([]);
const lastIsbnRef = useRef<string | null>(null);
const readerRef = useRef<BarcodeReader | null>(null);
const scannerContainerRef = useRef<HTMLDivElement>(null);
const [isScanning, setIsScanning] = useState(false);
useEffect(() => {
if (!isStrichInit || !scannerContainerRef.current) return;
const reader = new BarcodeReader({
selector: scannerContainerRef.current,
});
reader.initialize().then((instance) => {
readerRef.current = instance;
instance.detected = (detections) => {
const latest = detections[0];
// Only fetch if new ISBN detected
if (latest?.data && latest.data !== lastIsbnRef.current) {
lastIsbnRef.current = latest.data;
fetchBookDetails(latest.data);
}
};
});
return () => {
readerRef.current?.destroy();
readerRef.current = null;
};
}, [isStrichInit]);
if (strichInitError) {
return <div>Error initializing scanner: {strichInitError.message}</div>;
}
if (!isStrichInit) {
return <div>Initializing scanner...</div>;
}
const handleStart = async () => {
if (readerRef.current && !isScanning) {
try {
await readerRef.current.start();
setIsScanning(true);
} catch (error) {
console.error("Failed to start scanner:", error);
}
}
};
const handleStop = async () => {
if (readerRef.current && isScanning) {
try {
await readerRef.current.stop();
setIsScanning(false);
} catch (error) {
console.error("Failed to stop scanner:", error);
}
}
};
const clearDetections = () => {
setBooks([]);
lastIsbnRef.current = null;
};
const fetchBookDetails = async (isbn: string) => {
try {
const bookData = await fetchFromOpenLibrary(isbn);
if (bookData) {
setBooks((prev) => {
// Avoid duplicates by checking ISBN
if (prev.some((b) => b.isbn === bookData.isbn)) return prev;
return [...prev, bookData];
});
}
} catch (err) {
console.log(err);
}
};
return (
<div className="grid md:grid-cols-2 gap-4 items-start">
<div>
<h2 className="text-xl font-bold mb-4">
Currently, Library has {books.length} books
</h2>
{books.length === 0 && <p>Books not found</p>}
<div
className="flex flex-col gap-3 max-h-[260px] h-full overflow-y-scroll
"
>
{books.map((book) => (
<div
className="border rounded p-3 gap-3 flex border-gray-300"
key={book.isbn}
>
{book.thumbnail && (
<img
className="flex h-12"
src={book.thumbnail}
alt={book.title}
/>
)}
<div className="flex flex-col">
<h2 className="font-bold text-xl flex">{book.title}</h2>
<p>
Author: <span>{book.authors}</span>
</p>
</div>
</div>
))}
</div>
</div>
<div>
<h2 className="text-2xl font-bold mb-5">Barcode Scanner</h2>
<div className="flex flex-col">
<div
ref={scannerContainerRef}
id="scanner"
className="relative max-w-2xl min-h-40 border-2 border-gray-300 rounded"
></div>
<div className="actions mt-4 flex gap-2 justify-center">
<button
onClick={handleStart}
disabled={isScanning}
className="px-4 py-2 bg-green-500 text-white rounded disabled:bg-gray-300 disabled:cursor-not-allowed"
>
{isScanning ? "Scanning..." : "Start Scanner"}
</button>
<button
onClick={handleStop}
disabled={!isScanning}
className="px-4 py-2 bg-red-500 text-white rounded disabled:bg-gray-300 disabled:cursor-not-allowed"
>
Stop Scanner
</button>
<button
onClick={clearDetections}
className="px-4 py-2 bg-blue-500 text-white rounded"
>
Clear Results
</button>
</div>
</div>
</div>
</div>
);
};
export default BarcodeScanner;