This builds a debounced city autocomplete input in React, backed by Geomelon's free prefix-search endpoint (oneshot.geomelon.dev) — no API key, so it's safe to call directly from the browser with no backend proxy in the way.
This is the exact behavior the hook below implements, typing a French city:
// useCityAutocomplete.ts
import { useState, useEffect, useRef } from 'react';
interface City {
id: string;
name: string;
population: number | null;
emoji: string | null;
en?: string;
}
export function useCityAutocomplete(countryIso: string, lang: string, query: string) {
const [results, setResults] = useState<City[]>([]);
const [loading, setLoading] = useState(false);
const abortRef = useRef<AbortController | null>(null);
useEffect(() => {
const prefix = query.trim().toLowerCase();
if (!prefix) {
setResults([]);
return;
}
setLoading(true);
const timer = setTimeout(async () => {
abortRef.current?.abort();
const controller = new AbortController();
abortRef.current = controller;
try {
const res = await fetch(
`https://oneshot.geomelon.dev/${countryIso}/${lang}/${encodeURIComponent(prefix)}`,
{ signal: controller.signal },
);
setResults(res.ok ? await res.json() : []);
} catch (err) {
if ((err as Error).name !== 'AbortError') setResults([]);
} finally {
setLoading(false);
}
}, 150);
return () => clearTimeout(timer);
}, [countryIso, lang, query]);
return { results, loading };
}// CityAutocompleteInput.tsx
import { useState } from 'react';
import { useCityAutocomplete } from './useCityAutocomplete';
export function CityAutocompleteInput({ countryIso, lang }: { countryIso: string; lang: string }) {
const [query, setQuery] = useState('');
const { results, loading } = useCityAutocomplete(countryIso, lang, query);
return (
<div>
<input
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Start typing a city..."
autoComplete="off"
/>
{loading && <span>Searching…</span>}
<ul>
{results.map((city) => (
<li key={city.id} onClick={() => setQuery(city.name)}>
{city.emoji} {city.name}
{city.en && city.en !== city.name ? ` (${city.en})` : ''}
</li>
))}
</ul>
</div>
);
}For search across countries, sorting, pagination, or multilingual name resolution beyond this free endpoint, swap the fetch call for GET /cities/search on the main API (requires a free RapidAPI key) — same debounce pattern, different URL.