npm.io
0.1.0 • Published 2d ago

geomelon-react

Licence
MIT
Version
0.1.0
Deps
1
Size
43 kB
Vulns
0
Weekly
0
Stars
1

Geomelon React

Headless React hook and component for city autocomplete, built on the Geomelon API's free, keyless oneshot prefix search (oneshot.geomelon.dev) — no API key, no backend proxy required.

Looking for other ways to integrate? See all official libraries at geomelon.dev/libraries.

Installation

npm install geomelon-react

react (>=16.8, for hooks) is a peer dependency — bring your own version. This package depends on geomelon, the zero-dependency Geomelon TypeScript client, for the actual fetch/retry/error logic.

Quick start

The hook
import { useState } from 'react';
import { useCityAutocomplete } from 'geomelon-react';

function CityField() {
  const [query, setQuery] = useState('');
  const { results, loading, error } = useCityAutocomplete({
    countryIso: 'es',
    lang: 'es',
    query,
  });

  return (
    <div>
      <input value={query} onChange={(e) => setQuery(e.target.value)} placeholder="Start typing a city..." />
      {loading && <span>Searching…</span>}
      {error && <span>{error.message}</span>}
      <ul>
        {results.map((city) => (
          <li key={city.id}>{city.emoji} {city.name}</li>
        ))}
      </ul>
    </div>
  );
}

useCityAutocomplete debounces (150ms default), cancels the in-flight request whenever countryIso/lang/query change again, and never fires below minLength (default 1). Errors are typed GeomelonError, not silently swallowed into an empty result list.

The component

For a ready-made input + dropdown with keyboard navigation and ARIA combobox semantics, use <CityAutocomplete> — it's headless (no shipped CSS), so style it with className/inputClassName/listClassName/optionClassName:

import { CityAutocomplete } from 'geomelon-react';

<CityAutocomplete
  countryIso="es"
  lang="es"
  placeholder="Start typing a city..."
  onSelect={(city) => console.log(city.name)}
  inputClassName="my-input"
  listClassName="my-dropdown"
  optionClassName={(highlighted) => (highlighted ? 'my-option my-option--active' : 'my-option')}
/>

Keyboard: ArrowUp/ArrowDown move the highlighted option, Enter selects it, Escape closes the list. Mouse click and hover work the same way. renderOption(city, isHighlighted) overrides the default "🇪🇸 Barcelona (Barcelona)"-style row.

Configuration

Both the hook and the component accept:

Option Default Notes
countryIso ISO 3166-1 alpha-2 country code, e.g. "es"
lang BCP 47 language code, e.g. "es"
debounceMs 150 Delay before firing a request after the query stops changing
minLength 1 Minimum trimmed query length before a request fires
apiKey Optional RapidAPI key — omit to use the free keyless host
freeOneshot false Route to the free keyless host even when apiKey is set

<CityAutocomplete> additionally takes onSelect, placeholder, renderOption, and the four *ClassName styling props above.

Accessibility

<CityAutocomplete> follows the ARIA combobox pattern: role="combobox" on the input with aria-expanded/aria-controls/aria-activedescendant, role="listbox"/role="option" on the results. If you build your own UI on top of the raw hook, replicate these roles yourself — useCityAutocomplete only manages data, not markup.

Recipes

These are copy-paste starting points, not shipped code — no shadcn/ui or react-hook-form dependency is added by this package.

shadcn/ui combobox
import { useState } from 'react';
import { useCityAutocomplete } from 'geomelon-react';
import { Command, CommandInput, CommandList, CommandItem, CommandEmpty } from '@/components/ui/command';
import { Popover, PopoverTrigger, PopoverContent } from '@/components/ui/popover';

function CityCombobox({ onSelect }: { onSelect: (name: string) => void }) {
  const [open, setOpen] = useState(false);
  const [query, setQuery] = useState('');
  const { results, loading } = useCityAutocomplete({ countryIso: 'es', lang: 'es', query });

  return (
    <Popover open={open} onOpenChange={setOpen}>
      <PopoverTrigger asChild>
        <button role="combobox" aria-expanded={open}>{query || 'Select a city...'}</button>
      </PopoverTrigger>
      <PopoverContent className="p-0">
        <Command shouldFilter={false}>
          <CommandInput value={query} onValueChange={setQuery} placeholder="Search cities..." />
          <CommandList>
            {!loading && results.length === 0 && <CommandEmpty>No cities found.</CommandEmpty>}
            {results.map((city) => (
              <CommandItem
                key={city.id}
                onSelect={() => {
                  setQuery(city.name);
                  setOpen(false);
                  onSelect(city.name);
                }}
              >
                {city.emoji} {city.name}
              </CommandItem>
            ))}
          </CommandList>
        </Command>
      </PopoverContent>
    </Popover>
  );
}
react-hook-form
import { Controller, useForm } from 'react-hook-form';
import { CityAutocomplete } from 'geomelon-react';

function Form() {
  const { control, handleSubmit } = useForm({ defaultValues: { city: '' } });

  return (
    <form onSubmit={handleSubmit((data) => console.log(data))}>
      <Controller
        name="city"
        control={control}
        render={({ field }) => (
          <CityAutocomplete
            countryIso="es"
            lang="es"
            onSelect={(city) => field.onChange(city.name)}
          />
        )}
      />
      <button type="submit">Submit</button>
    </form>
  );
}

Example

A minimal runnable app lives in examples/vite-react — clone the repo and npm install && npm run dev, or open it directly in StackBlitz.

Error handling

GeomelonError (re-exported from geomelon) is set on the hook's error field — status, body, url, and an isRateLimited getter (status === 429) are available for any non-OK response or network failure.

Types

OneshotCityDto (re-exported from geomelon): { id, name, population, emoji, en? }en is omitted when the requested language is English or no English translation exists.

Build

npm install
npm run build   # dual CJS + ESM via tsc
npm test        # node:test + @testing-library/react, jsdom via global-jsdom

License

MIT

Keywords