Search docs

React

An address autocomplete input in React (19) with api.geolith.app: debounced requests, request cancelling, keyboard-friendly list and attribution.

By · Updated

A self-contained autocomplete component. It debounces typing, cancels stale requests with AbortController, and calls onSelect with the chosen GeoJSON feature. Use a publishable key whose allowed origins include your site (and http://localhost:5173 on a development key).

The component

// AddressAutocomplete.tsx
import { useEffect, useId, useRef, useState } from 'react';

export interface Place {
  type: 'Feature';
  geometry: { type: 'Point'; coordinates: [number, number] };
  properties: { gid: string; label: string; postalcode?: string; locality?: string; region?: string; country_code?: string };
}

const KEY = import.meta.env.VITE_GEOLITH_KEY as string; // gc_pk_…

export function AddressAutocomplete({ onSelect }: { onSelect: (place: Place) => void }) {
  const [text, setText] = useState('');
  const [places, setPlaces] = useState<Place[]>([]);
  const listId = useId();
  const picked = useRef('');

  useEffect(() => {
    if (text.trim().length < 3 || text === picked.current) {
      setPlaces([]);
      return;
    }
    const controller = new AbortController();
    const timer = setTimeout(async () => {
      const url = new URL('https://api.geolith.app/v1/autocomplete');
      url.searchParams.set('text', text);
      url.searchParams.set('size', '5');
      url.searchParams.set('api_key', KEY);
      try {
        const res = await fetch(url, { signal: controller.signal });
        if (!res.ok) return;
        const body = (await res.json()) as { features: Place[] };
        setPlaces(body.features);
      } catch (error) {
        if ((error as Error).name !== 'AbortError') throw error;
      }
    }, 200);
    return () => {
      clearTimeout(timer);
      controller.abort();
    };
  }, [text]);

  return (
    <div>
      <input
        type="search"
        value={text}
        onChange={(event) => setText(event.target.value)}
        placeholder="Start typing an address"
        aria-controls={listId}
        autoComplete="off"
      />
      <ul id={listId} role="listbox">
        {places.map((place) => (
          <li key={place.properties.gid} role="option" aria-selected={false}>
            <button
              type="button"
              onClick={() => {
                picked.current = place.properties.label;
                setText(place.properties.label);
                setPlaces([]);
                onSelect(place);
              }}
            >
              {place.properties.label}
            </button>
          </li>
        ))}
      </ul>
      <small>© OpenStreetMap contributors (ODbL), Overture Maps Foundation</small>
    </div>
  );
}

Use it

// App.tsx
import { AddressAutocomplete, type Place } from './AddressAutocomplete';

export default function App() {
  return (
    <AddressAutocomplete
      onSelect={(place: Place) => {
        const [lon, lat] = place.geometry.coordinates;
        console.log(place.properties.label, place.properties.postalcode, lat, lon);
      }}
    />
  );
}

Put the key in .env.local as VITE_GEOLITH_KEY=gc_pk_… (Vite). In Next.js, use NEXT_PUBLIC_GEOLITH_KEY and process.env.NEXT_PUBLIC_GEOLITH_KEY, and add 'use client' at the top of the component file.