Address autocomplete in React
About 15 minutes. Updated 11 September 2026.
A small hook and a component that give a React form Australian address autocomplete from the Locio API, with debouncing, request cancellation and the picked address handed to the parent as a typed object.
Two files: a hook that turns a query into results, and a component that renders them. The hook owns the debounce and the cancellation, which is where autocomplete usually goes wrong, so the component stays a plain form control.
01The hook
useEffect with cleanup is exactly the shape this needs. Each change to the query starts a timer; the cleanup for the previous run clears its timer and aborts its request, so only the latest answer can reach state.
// useAddressSearch.ts
import { useEffect, useState } from "react";
export type Address = {
gnaf_pid: string;
formatted: string;
lat: number;
lng: number;
mesh_block?: string;
components: Record<string, string>;
};
const KEY = import.meta.env.VITE_LOCIO_KEY as string; // a public key
const API = "https://api.locio.com.au";
export function useAddressSearch(query: string) {
const [results, setResults] = useState<Address[]>([]);
const [busy, setBusy] = useState(false);
useEffect(() => {
const q = query.trim();
if (q.length < 3) {
setResults([]);
return;
}
const controller = new AbortController();
// 200ms of quiet, then one request. Cleanup cancels both the timer and
// the fetch, so a stale answer can never overwrite a newer one.
const timer = setTimeout(async () => {
setBusy(true);
try {
const url = new URL(`${API}/v1/addresses`);
url.searchParams.set("q", q);
url.searchParams.set("limit", "8");
const res = await fetch(url, {
headers: { Authorization: `Bearer ${KEY}` },
signal: controller.signal,
});
if (res.ok) {
const { data } = (await res.json()) as { data: Address[] };
setResults(data);
}
} catch (err) {
if ((err as Error).name !== "AbortError") console.error(err);
} finally {
if (!controller.signal.aborted) setBusy(false);
}
}, 200);
return () => {
clearTimeout(timer);
controller.abort();
};
}, [query]);
return { results, busy };
}02The component
A labelled input, a list of buttons, and a hidden field carrying the G-NAF id. Once an address is picked, the hook is handed an empty query so the list closes and no further requests are made until the person types again.
// AddressSearch.tsx
import { useId, useState } from "react";
import { useAddressSearch, type Address } from "./useAddressSearch";
export function AddressSearch({ onPick }: { onPick: (address: Address) => void }) {
const id = useId();
const [query, setQuery] = useState("");
const [picked, setPicked] = useState<Address | null>(null);
const { results, busy } = useAddressSearch(picked ? "" : query);
return (
<div>
<label htmlFor={id}>Delivery address</label>
<input
id={id}
type="search"
autoComplete="off"
placeholder="145 sydney road"
value={query}
onChange={(event) => {
setPicked(null);
setQuery(event.target.value);
}}
/>
{busy && <p>Searching…</p>}
{results.length > 0 && !picked && (
<ul role="listbox">
{results.map((address) => (
<li key={address.gnaf_pid}>
<button
type="button"
onClick={() => {
setPicked(address);
setQuery(address.formatted);
onPick(address);
}}
>
{address.formatted}
</button>
</li>
))}
</ul>
)}
{/* The id is what your form should submit, not the text. */}
<input type="hidden" name="gnaf_pid" value={picked?.gnaf_pid ?? ""} />
</div>
);
}03Use it
The parent gets the whole address: id, coordinates, mesh block and every G-NAF component, from one call.
import { AddressSearch } from "./AddressSearch";
export function CheckoutForm() {
return (
<form method="post" action="/checkout">
<AddressSearch
onPick={(address) => {
console.log(address.gnaf_pid, address.lat, address.lng, address.components.postcode);
}}
/>
<button type="submit">Continue</button>
</form>
);
}Next
Starting from nothing? Stand up a React site with address search walks from an empty folder to a deployed page using these two files.
Related
- Address autocomplete in plain JavaScript
- Address autocomplete in Vue
- Stand up a React site with Australian address search
- Put a searched address on a map with MapLibre
- Validate and geocode Australian addresses in PHP
- Use Locio from Claude Code, Claude Desktop and Cursor
- Give an agent address tools with the Anthropic API
- Clean a spreadsheet of addresses with an agent
- The API reference, with every call in curl, Node, Python, Ruby, Go and PHP.