Documentation Index
Fetch the complete documentation index at: https://mintlify.com/adelpro/quran-search-engine/llms.txt
Use this file to discover all available pages before exploring further.
This example demonstrates a complete React + Vite application using the Quran Search Engine library with TypeScript.
Features
- Real-time search with debounced input
- Lemma, root, and fuzzy search options
- Highlighted search results by match type
- Pagination controls
- Match statistics and scoring
- TypeScript support
Run the development server
From the workspace root:pnpm -C examples/vite-react dev
Or from the example directory:cd examples/vite-react
pnpm dev
Open your browser
Navigate to http://localhost:5173
Project structure
examples/vite-react/
βββ src/
β βββ App.tsx # Main app component
β βββ components/
β β βββ VerseItem.tsx # Verse display with highlighting
β βββ useDebounce.ts # Debounce hook
β βββ main.tsx # Entry point
βββ package.json
βββ vite.config.ts
Loading data
Load all required datasets on app initialization:
import { useState, useEffect } from 'react';
import {
loadQuranData,
loadMorphology,
loadWordMap,
type QuranText,
type MorphologyAya,
type WordMap,
} from 'quran-search-engine';
function App() {
const [quranData, setQuranData] = useState<QuranText[]>([]);
const [morphologyMap, setMorphologyMap] = useState<Map<number, MorphologyAya> | null>(null);
const [wordMap, setWordMap] = useState<WordMap | null>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
async function init() {
try {
const [data, morphology, dictionary] = await Promise.all([
loadQuranData(),
loadMorphology(),
loadWordMap(),
]);
setQuranData(data);
setMorphologyMap(morphology);
setWordMap(dictionary);
} catch (error) {
console.error('Failed to load Quran data:', error);
} finally {
setLoading(false);
}
}
init();
}, []);
// ... rest of component
}
All data loaders return Promises and can be loaded in parallel using Promise.all() for optimal performance.
Search implementation
Implement debounced search with configurable options:
import { search, type SearchResponse } from 'quran-search-engine';
import { useDebounce } from './useDebounce';
function App() {
const [query, setQuery] = useState('');
const debouncedQuery = useDebounce(query, 300);
const [searchResponse, setSearchResponse] = useState<SearchResponse | null>(null);
const [options, setOptions] = useState({ lemma: true, root: true, fuzzy: true });
const [currentPage, setCurrentPage] = useState(1);
const PAGE_SIZE = 10;
useEffect(() => {
if (!loading && quranData.length > 0 && morphologyMap && wordMap && debouncedQuery.trim()) {
const response = search(debouncedQuery, quranData, morphologyMap, wordMap, options, {
page: currentPage,
limit: PAGE_SIZE,
});
setSearchResponse(response);
} else {
setSearchResponse(null);
}
}, [debouncedQuery, options, currentPage, quranData, morphologyMap, wordMap, loading]);
// Reset page when query or options change
useEffect(() => {
setCurrentPage(1);
}, [debouncedQuery, options]);
// ... rest of component
}
Use the useDebounce hook to avoid triggering searches on every keystroke, improving performance and user experience.
Debounce hook
The custom debounce hook delays state updates:
import { useState, useEffect } from 'react';
export function useDebounce<T>(value: T, delay: number): T {
const [debouncedValue, setDebouncedValue] = useState<T>(value);
useEffect(() => {
const handler = setTimeout(() => {
setDebouncedValue(value);
}, delay);
return () => {
clearTimeout(handler);
};
}, [value, delay]);
return debouncedValue;
}
Highlighting results
Render verses with highlighted matches using getHighlightRanges:
import {
type ScoredQuranText,
getHighlightRanges,
} from 'quran-search-engine';
import type { ReactNode } from 'react';
export function VerseItem({ verse }: { verse: ScoredQuranText }) {
function renderHighlightedVerse(): ReactNode {
const ranges = getHighlightRanges(verse.uthmani, verse.matchedTokens, verse.tokenTypes);
if (ranges.length === 0) return verse.uthmani;
const parts: ReactNode[] = [];
let cursor = 0;
for (let i = 0; i < ranges.length; i++) {
const range = ranges[i];
if (cursor < range.start) {
parts.push(verse.uthmani.slice(cursor, range.start));
}
const segment = verse.uthmani.slice(range.start, range.end);
parts.push(
<span
key={`${range.start}-${range.end}-${i}`}
className={`highlight highlight-${range.matchType}`}
>
{segment}
</span>,
);
cursor = range.end;
}
if (cursor < verse.uthmani.length) {
parts.push(verse.uthmani.slice(cursor));
}
return parts;
}
return (
<div className="verse-card">
<div className="verse-card-header">
<span>
{verse.sura_name} ({verse.sura_id}:{verse.aya_id})
</span>
<span className={`match-tag tag-${verse.matchType}`}>
{verse.matchType === 'none' ? 'fuzzy' : verse.matchType} (Score: {verse.matchScore})
</span>
</div>
<div className="verse-arabic">{renderHighlightedVerse()}</div>
</div>
);
}
The getHighlightRanges function returns UI-agnostic highlight ranges. You control how to render them in your component, avoiding dangerouslySetInnerHTML.
Implement pagination controls:
import { ChevronLeft, ChevronRight } from 'lucide-react';
{searchResponse && searchResponse.pagination.totalPages > 1 && (
<div className="pagination-controls">
<button
className="page-btn"
disabled={currentPage === 1}
onClick={() => setCurrentPage(currentPage - 1)}
>
<ChevronLeft size={20} />
</button>
<span>
Page {currentPage} of {searchResponse.pagination.totalPages}
</span>
<button
className="page-btn"
disabled={currentPage === searchResponse.pagination.totalPages}
onClick={() => setCurrentPage(currentPage + 1)}
>
<ChevronRight size={20} />
</button>
</div>
)}
Display search statistics
Show match counts by type:
{searchResponse && (
<div className="results-info">
<div className="results-count">
Found <strong>{searchResponse.pagination.totalResults}</strong> matches
</div>
<div className="results-stats">
<span className="stat-item">
<span className="indicator indicator-exact"></span>
<span className="stat-label">Exact:</span>
<span className="stat-value">{searchResponse.counts.simple}</span>
</span>
<span className="stat-item">
<span className="indicator indicator-lemma"></span>
<span className="stat-label">Lemma:</span>
<span className="stat-value">{searchResponse.counts.lemma}</span>
</span>
<span className="stat-item">
<span className="indicator indicator-root"></span>
<span className="stat-label">Root:</span>
<span className="stat-value">{searchResponse.counts.root}</span>
</span>
<span className="stat-item">
<span className="indicator indicator-fuzzy"></span>
<span className="stat-label">Fuzzy:</span>
<span className="stat-value">{searchResponse.counts.fuzzy}</span>
</span>
</div>
</div>
)}
Dependencies
The example uses these key dependencies:
{
"dependencies": {
"react": "^19.2.0",
"react-dom": "^19.2.0",
"quran-search-engine": "workspace:*",
"lucide-react": "^0.473.0"
},
"devDependencies": {
"@vitejs/plugin-react": "^5.1.1",
"typescript": "~5.9.3",
"vite": "^7.2.4"
}
}
This example uses workspace:* for the library dependency because itβs part of a pnpm workspace. In your own project, use the npm package: "quran-search-engine": "^1.0.0"
Key features demonstrated
- Type safety: Full TypeScript support with proper types for all API calls
- Performance: Debounced search input and paginated results
- User experience: Loading states, search options, and visual feedback
- Highlighting: Match-type-specific highlighting using the
getHighlightRanges utility
- Extensibility: Clean component structure for easy customization