Add a place search box to React
A search box over a geocoding API is more work than it looks: debounce the input, cancel the request that is already in flight, keep the list navigable by keyboard, and move the map when something is chosen. The copied component has all of that already. The hook gives you the calls and leaves the interface to you.
Install
npx @unmap/cli add geocodernpm i @unmap/reactThe registry item is a Combobox built from your own shadcn primitives, so it inherits your focus ring, your radius and your palette rather than arriving with opinions.
The component
The copied <Geocoder> goes inside <Map>, which is how it finds the map to fly: both read the
same map-context that add installed alongside them.
import { Map } from "@/components/ui/map";
import { Geocoder } from "@/components/ui/geocoder";
export default function App() {
const apiKey = import.meta.env.VITE_UNMAP_KEY as string;
return (
<div className="h-dvh">
<Map apiKey={apiKey} center={[-114.07, 51.05]} zoom={11} className="h-full">
<div className="absolute left-3 top-3 z-10 w-72">
<Geocoder apiKey={apiKey} placeholder="Search Canadian places" />
</div>
</Map>
</div>
);
}import { useState } from "react";
import { useGeocoder } from "@unmap/react";
import type { GeocodeResult } from "@unmap/sdk";
export default function Search() {
const { autocomplete, results, loading } = useGeocoder({
apiKey: import.meta.env.VITE_UNMAP_KEY as string,
});
const [chosen, setChosen] = useState<GeocodeResult | null>(null);
return (
<div>
<input
onChange={(event) => void autocomplete(event.target.value, { limit: 5 })}
placeholder="Search Canadian places"
/>
{loading && <span>Searching…</span>}
<ul>
{results.map((result) => (
<li key={result.id}>
<button onClick={() => setChosen(result)}>{result.name}</button>
</li>
))}
</ul>
{chosen && <p>{chosen.lng}, {chosen.lat}</p>}
</div>
);
}Debouncing is your job on the package path
The snippet above calls the API on every keystroke, which is wrong in a way worth being explicit about: every autocomplete request is one metered call. Typing "Calgary" unthrottled is seven calls to answer one question. The copied component debounces at 200ms; the hook deliberately does not, because a hook that decided your timing would be a hook you had to fight.
import { useEffect, useRef, useState } from "react";
import { useGeocoder } from "@unmap/react";
export default function DebouncedSearch() {
const { autocomplete, results } = useGeocoder({
apiKey: import.meta.env.VITE_UNMAP_KEY as string,
});
const [query, setQuery] = useState("");
const timer = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);
useEffect(() => {
if (query.length < 3) return;
clearTimeout(timer.current);
timer.current = setTimeout(() => void autocomplete(query, { limit: 5 }), 200);
return () => clearTimeout(timer.current);
}, [query, autocomplete]);
return (
<div>
<input value={query} onChange={(event) => setQuery(event.target.value)} />
<ul>
{results.map((result) => (
<li key={result.id}>{result.name}</li>
))}
</ul>
</div>
);
}The three-character floor matters as much as the delay. One- and two-letter queries match almost everything and are worth nothing to the person typing.
What comes back
Each result carries name, lng, lat, a layer saying what kind of thing it is, and an
address breakdown when the row has one. Bilingual rows also carry names, so a Montréal street
can show both forms without a second lookup, and lang decides which one name holds.
Results are ranked, not filtered. The list is what the corpus thinks best matches the text, and for a short query the top result can be a city when the user meant a street in it.
Common problems
Every keystroke is a request. See above. This is the single most expensive mistake available on this page.
The map does not move on the registry path. <Geocoder> is outside <Map>, so it has no map
to fly. It must be a child.
Results feel wrong for a street query. Add more of the address. "17 Ave SW" alone is ambiguous across the country; "17 Ave SW Calgary" is not.
Related
- Autocomplete for the framework-free version
- Geocoding API for the four endpoints and the ranking rules
- Add a map to React for the map this sits on