Ajouter une recherche de lieux à React

Une boîte de recherche au-dessus d'une API de géocodage demande plus de travail qu'il n'y paraît : appliquer un anti-rebond à la saisie, annuler la requête déjà en vol, garder la liste navigable au clavier, et déplacer la carte quand un choix est fait. Le composant copié fait déjà tout cela. Le hook vous donne les appels et vous laisse l'interface.

Installation

npx @unmap/cli add geocoder

L'élément du registre est un Combobox construit à partir de vos propres primitives shadcn : il hérite donc de votre anneau de focus, de votre rayon et de votre palette au lieu d'arriver avec des idées arrêtées.

Le composant

Le <Geocoder> copié se place dans <Map>, et c'est ainsi qu'il trouve la carte à survoler : tous deux lisent le même map-context qu'add a installé à côté d'eux.

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="Rechercher un lieu au Canada" />
        </div>
      </Map>
    </div>
  );
}

L'anti-rebond vous revient sur la voie du paquet

L'extrait ci-dessus appelle l'API à chaque frappe, ce qui est faux d'une manière qu'il vaut mieux dire explicitement : chaque requête d'autocomplétion est un appel facturé. Taper « Calgary » sans limitation fait sept appels pour répondre à une seule question. Le composant copié applique un anti-rebond de 200 ms ; le hook ne le fait volontairement pas, car un hook qui déciderait de votre rythme serait un hook qu'il faudrait combattre.

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>
  );
}

Le seuil de trois caractères compte autant que le délai. Les requêtes d'une ou deux lettres correspondent à presque tout et ne valent rien pour la personne qui tape.

Ce qui revient

Chaque résultat porte name, lng, lat, un layer qui dit de quel type d'objet il s'agit, et une décomposition address quand la ligne en a une. Les lignes bilingues portent aussi names : une rue montréalaise peut donc afficher les deux formes sans seconde requête, et lang décide laquelle name contient.

Les résultats sont classés, pas filtrés. La liste est ce que le corpus juge le plus proche du texte, et pour une requête courte le premier résultat peut être une ville alors que l'utilisateur pensait à une rue qui s'y trouve.

Problèmes courants

Chaque frappe est une requête. Voir ci-dessus. C'est l'erreur la plus coûteuse disponible sur cette page.

La carte ne bouge pas sur la voie du registre. <Geocoder> est hors de <Map> : il n'a donc aucune carte à survoler. Il doit en être un enfant.

Les résultats semblent faux pour une rue. Ajoutez le reste de l'adresse. « 17 Ave SW » seul est ambigu à l'échelle du pays ; « 17 Ave SW Calgary » ne l'est pas.

Pour aller plus loin