Skip to content Skip to navigation

React

To integrate Boox with a React application, you can use the useState and useEffect hooks to manage the search state and update the UI accordingly. Here's a basic example:

Project setup

Create a new React project using Vite with TypeScript template:

npm create vite@latest boox-react -- --template react-ts
BASH

Now run:

cd boox-react && npm install
BASH

Install Boox and other dependencies:

npm install -D boox use-debounce metaphone stemmer stopword @types/stopword
BASH

File structure

boox-react
├── src
│   ├── components
│   │   ├── Footer.tsx
│   │   ├── Search.tsx
│   │   ├── SearchInfo.tsx
│   │   ├── SearchResult.tsx
│   │   └── SearchResults.tsx
│   ├── App.tsx
│   ├── main.tsx
│   └── types.ts
├── index.html
└── vite.config.ts
TEXT

Did you know?

You can remove unnecessary files from the default Vite installation.

Code snippets

Kindly replicate the code snippets provided below:

<!doctype html>
<html lang="en" data-bs-theme="dark">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Basic usage - Boox</title>

    <link
      href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css"
      rel="stylesheet"
      integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH"
      crossorigin="anonymous"
    />
  </head>
  <body class="bg-body-secondary">
    <div id="root" class="vstack min-vh-100"></div>

    <script type="module" src="/src/main.tsx"></script>
  </body>
</html>
HTML
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'

export default defineConfig({
  plugins: [react()],
  build: {
    rollupOptions: { external: ['os'] }
  }
})
TS

Below is the content of the src/* files:

import Boox from 'boox'
import { metaphone } from 'metaphone'
import { stemmer } from 'stemmer'
import { removeStopwords } from 'stopword'
import Footer from './components/Footer'
import Search from './components/Search'
import type { Pokemon } from './types'

const App: React.FC = () => {
  const boox = new Boox<Pokemon>({
    features: ['name', 'caption', 'set_name'],
    attributes: ['hp', 'image_url'],
    modelOptions: {
      tokenizer(input) {
        return removeStopwords(Array.from(input.match(/\b\w+\b/g) || []))
      },
      stemmer: stemmer,
      phonetic: metaphone
    }
  })

  const fetchData = async () => {
    try {
      const response = await fetch(
        'https://stilearning.com/boox/demo/datasets/pokemon-100r.json',
        { cache: 'default' }
      )

      if (!response.ok) {
        throw new Error('Network response was not ok')
      }

      const pokemons: Pokemon[] = await response.json()
      await boox.addDocuments(pokemons)
    } catch (error) {
      console.error(error)
      throw error
    }
  }

  fetchData()

  return (
    <>
      <main
        className='mx-auto my-4 bg-body w-100 rounded-3 shadow'
        style={{ maxWidth: '30rem' }}
        role='main'
      >
        <Search model={boox} />
      </main>

      <Footer />
    </>
  )
}

export default App
TSX
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App'

ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
)
TSX
/**
 * Interface representing a Pokemon data object.
 */
export interface Pokemon {
  id: string
  image_url: string
  caption: string
  name: string
  hp: number
  set_name: string
}
TS

Below is the content of the src/components/* files:

const GITHUB_LINK = 'https://github.com/bent10/boox'
const BOOX_LOGO = (
  <svg
    xmlns='http://www.w3.org/2000/svg'
    viewBox='0 0 180 180'
    height='28'
    aria-hidden='true'
  >
    <g fill='none' fillRule='nonzero'>
      <path
        fill='currentColor'
        d='M111.223 75.983 91.937 66.34a3.214 3.214 0 0 0-2.877 0l-19.286 9.643A3.214 3.214 0 0 0 68 78.859v22.5c0 1.219.688 2.333 1.778 2.877l19.285 9.643a3.214 3.214 0 0 0 2.874 0l19.286-9.643A3.214 3.214 0 0 0 113 101.36v-22.5a3.214 3.214 0 0 0-1.777-2.876ZM90.5 72.81l12.099 6.05L90.5 84.908l-12.099-6.05L90.5 72.81ZM74.429 84.06l12.857 6.429v15.313l-12.857-6.429V84.06Zm19.285 21.742V90.489l12.857-6.429v15.313l-12.857 6.429Z'
      ></path>
      <path
        fill='var(--bs-secondary-color)'
        d='M180 90c0-19.343-18-36.203-44.692-45.307C126.203 18 109.343 0 90 0 61.005 0 37.5 40.297 37.5 90c-.01 9.195.825 18.371 2.498 27.412C24.323 110.063 15 99.75 15 90a23.678 23.678 0 0 1 8.288-16.335A154.035 154.035 0 0 1 27 52.552C10.343 62.079 0 75.33 0 90c0 19.343 18 36.203 44.693 45.308C53.797 162 70.657 180 90 180c14.67 0 27.922-10.343 37.448-27a154.035 154.035 0 0 1-21.113 3.683A23.677 23.677 0 0 1 90 165c-9.75 0-20.063-9.322-27.413-24.998A149.798 149.798 0 0 0 90 142.5c49.702 0 90-23.505 90-52.5ZM90 15c9.75 0 20.063 9.323 27.412 24.998A149.798 149.798 0 0 0 90 37.5c-3.06 0-6.08.09-9.06.27a64.89 64.89 0 0 0-7.77 15.75A140.557 140.557 0 0 1 90 52.5a131.4 131.4 0 0 1 33.345 4.155A131.4 131.4 0 0 1 127.5 90c0 5.625-.339 11.245-1.013 16.83a64.89 64.89 0 0 0 15.75-7.77c.175-2.985.263-6.005.263-9.06.01-9.195-.825-18.371-2.498-27.413C155.678 69.938 165 80.25 165 90c0 17.737-30.795 37.5-75 37.5a131.4 131.4 0 0 1-33.345-4.155A131.4 131.4 0 0 1 52.5 90c0-44.205 19.763-75 37.5-75Z'
      ></path>
    </g>
  </svg>
)

const Footer: React.FC = () => (
  <footer className='container mt-auto py-3' role='contentinfo'>
    <p className='d-flex justify-content-center text-muted'>
      Search by{' '}
      <a className='icon-link ms-2' href={GITHUB_LINK} target='_blank'>
        {BOOX_LOGO}
        <span className='fw-bold text-body'>Boox</span>
      </a>
    </p>
  </footer>
)

export default Footer
TSX
import { useState, useEffect } from 'react'
import Boox, { type SearchResult } from 'boox'
import { useDebounce } from 'use-debounce'
import type { Pokemon } from '../types'
import SearchInfo from './SearchInfo'
import SearchResults from './SearchResults'

const DEBOUNCE_DELAY = 300
const PLACEHOLDER_TEXT = 'Search (e.g. pikachu, water pwer)'

interface SearchProps {
  model: Boox<Pokemon>
}

const Search: React.FC<SearchProps> = ({ model }) => {
  const [input, setInput] = useState('')
  const [results, setResults] = useState<SearchResult<Pokemon>[]>([])
  const [query] = useDebounce(input, DEBOUNCE_DELAY)

  useEffect(() => {
    const performSearch = async () => {
      if (!query) {
        setResults([])
        return
      }

      const searchResults = await model.search(query)
      setResults(searchResults)
    }

    performSearch()
  }, [model, query])

  const totalDocuments = Object.keys(model.currentState.documents).length

  return (
    <>
      <form id='search-form' className='p-3' role='search'>
        <div className='form-group'>
          <input
            type='search'
            className='form-control form-control-lg'
            id='search-query'
            placeholder={PLACEHOLDER_TEXT}
            aria-label='Search pokemons'
            autoFocus
            value={input}
            onChange={e => setInput(e.target.value)}
          />

          <div className='form-text'>
            <strong>Note:</strong>
            <span> the </span>
            <code className='bg-body-secondary'>pwer</code>
            <span> demonstrates a typing error for </span>
            <code className='bg-body-secondary'>power</code>.
          </div>
        </div>
      </form>

      <SearchInfo results={results} totalDocuments={totalDocuments} />
      <SearchResults results={results} />
    </>
  )
}

export default Search
TSX
import type { SearchResult } from 'boox'
import type { Pokemon } from '../types'

interface SearchInfoProps {
  results: SearchResult<Pokemon>[]
  totalDocuments: number
}

const SearchInfo: React.FC<SearchInfoProps> = ({ results, totalDocuments }) => {
  const { length } = results

  return (
    <p
      id='results-length'
      className={`px-3 text-muted ${length ? '' : 'd-none'}`}
    >
      {length ? `Found ${length} of ${totalDocuments} pokemons` : ''}
    </p>
  )
}

export default SearchInfo
TSX
import type { SearchResult } from 'boox'
import type { Pokemon } from '../types'

interface SearchResultProps {
  result: SearchResult<Pokemon>
}

const SearchResultComponent: React.FC<SearchResultProps> = ({ result }) => {
  const {
    image_url,
    name: image_alt,
    hp
  } = result.attributes as unknown as Pokemon
  const { text: name } = result.context('name')
  const { text: set_name } = result.context('set_name')
  const { text: caption } = result.context('caption', 80)

  return (
    <a
      className='list-group-item list-group-item-action d-flex gap-3 py-3'
      href={String(image_url)}
      target='_blank'
      rel='noreferrer'
    >
      <img
        loading='lazy'
        src={image_url}
        alt={image_alt}
        width='32'
        height='32'
        className='rounded-circle flex-shrink-0'
      />
      <div className='d-flex gap-2 w-100 justify-content-between'>
        <div>
          <h6
            className='mb-1'
            dangerouslySetInnerHTML={{ __html: `${name} – ${set_name}` }}
          />
          <p
            className='mb-0 opacity-75'
            dangerouslySetInnerHTML={{ __html: `${caption}...` }}
          />
        </div>
        <small className='opacity-50 text-nowrap'>HP {hp}</small>
      </div>
    </a>
  )
}

export default SearchResultComponent
TSX
import type { SearchResult } from 'boox'
import type { Pokemon } from '../types'
import SearchResultComponent from './SearchResult'

interface SearchResultsProps {
  results: SearchResult<Pokemon>[]
}

const SearchResults: React.FC<SearchResultsProps> = ({ results }) => {
  return (
    <div
      id='results'
      className={`list-group list-group-flush overflow-auto rounded-3 ${
        results.length ? 'd-block' : 'd-none'
      }`}
    >
      {results.map(result => (
        <SearchResultComponent key={result.id} result={result} />
      ))}
    </div>
  )
}

export default SearchResults
TSX

Running the app

1. Start the development server:

npm run dev
BASH

Open http://localhost:5173 in your browser to see the search app in action.

2. Build for Production:

npm run build
BASH

Heads up

Remember to consult the documentation for Boox and other libraries for more advanced usage and configuration options.