Build a React/Next.js Address Autocomplete Component for Vietnam
A practical guide to building a reusable Vietnamese address-autocomplete component in React/Next.js: debounce, keyboard navigation, a key-hiding proxy, and wiring /v1/suggest to /v1/place/resolve for clean coordinates.
Almost every checkout, delivery, or sign-up form in Vietnam needs a smart address field. But the way many people build it — drop an <input> and call the API on every keystroke — fails in the places nobody looks: it leaks the API key to the browser, fires a request per character, has no debounce, and never groups keystrokes into one billable session.
This post builds a reusable<AddressAutocomplete> component in React/Next.js with debounce, keyboard navigation, a server-side proxy that hides your key, and the two GoGoDuk endpoints wired together — /v1/suggest for predictions and /v1/place/resolve to get clean coordinates for the address the user picks.
This post focuses on the implementation. If you want the UX design and the reasons to use autocomplete for Vietnamese addresses, read Vietnam Address Autocomplete API first. New to geocoding? Start with What is geocoding?.
A hand-rolled autocomplete field usually makes four mistakes:
It leaks the API key. Calling api.gogoduk.com directly from a component puts X-API-Key in client JavaScript and in the Network tab — anyone can copy it.
It spams requests. A request per character adds latency and burns quota without making the dropdown better.
No debounce. Fast typing creates a race of overlapping requests and a flickering result list.
No billing grouping. Each keystroke is billed as its own lookup instead of one autocomplete session.
The right shape: the browser calls your own Next.js route handler, and that route adds X-API-Key and calls GoGoDuk. The key never leaves the server.
Create app/api/address/suggest/route.ts. The /v1/suggest endpoint requires input of at least 2 characters, accepts lang (default vi), country (VN only), and optional focus.lat/focus.lon to bias results toward a location.
import { NextResponse } from "next/server";const API_URL = process.env.GOGODUK_API_URL ?? "https://api.gogoduk.com";export async function GET(req: Request) { const url = new URL(req.url); const input = url.searchParams.get("input")?.trim() ?? ""; // /v1/suggest needs at least 2 characters — short-circuit to avoid waste. if (input.length < 2) { return NextResponse.json({ predictions: [] }); } const upstream = new URL("/v1/suggest", API_URL); upstream.searchParams.set("input", input); upstream.searchParams.set("lang", "vi"); upstream.searchParams.set("country", "VN"); // Group keystrokes into one billable session: forward the sessionToken. const sessionToken = url.searchParams.get("sessionToken"); if (sessionToken) upstream.searchParams.set("sessionToken", sessionToken); const response = await fetch(upstream, { cache: "no-store", headers: { "X-API-Key": process.env.GOGODUK_API_KEY! }, }); return NextResponse.json(await response.json(), { status: response.status, });}
Store the key in a server-only environment variable:
The component is a controlled <input> that debounces ~250ms before calling the proxy route. Each /v1/suggest response returns a predictions array, where each item has placeId, text, mainText, secondaryText.
The role="combobox", aria-expanded, aria-activedescendant, and role="option" attributes above let screen readers understand the suggestion list — don't skip them.
The goal isn't a pretty dropdown — it's a stable record you can save against an order: the display text, the placeId, and the lat/lon. Once you have coordinates, your checkout can compute distance-based shipping fees, assign a delivery zone, or determine the ward — without making the user type again.
function CheckoutForm() { const [place, setPlace] = useState<ResolvedPlace | null>(null); return ( <form> <AddressAutocomplete onSelect={setPlace} /> {place && ( <input type="hidden" name="placeId" value={place.placeId} /> )} {/* place.lat / place.lon are ready for fee math or zone assignment */} </form> );}
You now have a Vietnamese address-autocomplete component that's safe (key hidden), efficient (debounce + one billing session), and keyboard-usable. From the lat/lon you get, go further:
Compute shipping fees from real route distance with the Directions API.