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 = {
// The id in the register the address came from. In Australia it is the
// G-NAF Address Detail PID, sent as address_detail_pid as well.
id: string;
formatted: string;
lat: number;
lng: number;
mesh_block?: string; // Australian records only
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) {
// country_code says which country was searched; note says why the
// list is empty when empty is the right answer.
const { data } = (await res.json()) as {
data: Address[];
country_code?: string;
note?: string;
};
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 address 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.id}>
<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="address_id" value={picked?.id ?? ""} />
</div>
);
}Store id. It is the address's id in its country's register, which in Australia is the G-NAF Address Detail PID, sent under address_detail_pid as well. It is stable across releases for an address that has not changed, where the formatted line is not, so it is the thing to key your own records on rather than the text.
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.id, 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 React with the Locio package
- Address autocomplete in Vue with the Locio package
- Address autocomplete in Vue
- Stand up a React site with Australian address search
- Normalise shipping addresses at checkout
- Put a searched address on a map with MapLibre
- Validate and geocode Australian addresses in PHP
- Australian address autocomplete for WooCommerce
- Address validation and geocoding in Go
- 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.