Skip to main content

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

Setup

1

Install dependencies

pnpm install
2

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
3

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:
App.tsx
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:
App.tsx
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:
useDebounce.ts
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:
VerseItem.tsx
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.

Pagination

Implement pagination controls:
App.tsx
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:
App.tsx
{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:
package.json
{
  "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

Build docs developers (and LLMs) love