Xây dựng component tự động hoàn thành địa chỉ trong React/Next.js cho Việt Nam
Hướng dẫn dựng một component ô nhập địa chỉ Việt Nam tái dùng được trong React/Next.js: debounce, điều hướng bàn phím, proxy giấu API key, và ghép /v1/suggest với /v1/place/resolve để lấy toạ độ chuẩn.
Gần như mọi form checkout, giao hàng hay đăng ký ở Việt Nam đều cần một ô nhập địa chỉ thông minh. Nhưng cách nhiều người làm — đặt một thẻ <input> rồi gọi thẳng API mỗi lần gõ phím — lại sai ở những chỗ ít ai để ý: lộ API key ra trình duyệt, bắn request mỗi ký tự, không debounce, và không gộp các lần gõ thành một phiên billing.
Bài này dựng một component <AddressAutocomplete>tái dùng được trong React/Next.js: có debounce, điều hướng bàn phím, proxy phía server để giấu key, và ghép hai endpoint của GoGoDuk — /v1/suggest để gợi ý và /v1/place/resolve để lấy toạ độ chuẩn cho địa chỉ người dùng chọn.
Bài này tập trung vào phần triển khai code. Nếu bạn cần phần thiết kế trải nghiệm và lý do nên dùng autocomplete cho địa chỉ Việt Nam, đọc trước Vietnam Address Autocomplete API. Còn chưa rõ geocoding là gì thì bắt đầu ở Geocoding là gì?.
Trình duyệt (<AddressAutocomplete/>) -> Next.js /api/address/suggest (proxy, gắn X-API-Key) -> GoGoDuk /v1/suggest (gợi ý theo input) -> người dùng chọn 1 prediction (placeId) -> Next.js /api/address/resolve (proxy) -> GoGoDuk /v1/place/resolve (lấy lat/lon + địa chỉ chuẩn) -> trả về giá trị ổn định để lưu / geocode
Tạo app/api/address/suggest/route.ts. Endpoint /v1/suggest yêu cầu input tối thiểu 2 ký tự, nhận lang (mặc định vi), country (chỉ VN), và tuỳ chọn focus.lat/focus.lon để ưu tiên kết quả gần một vị trí.
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 cần tối thiểu 2 ký tự — chặn sớm để khỏi gọi thừa. 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"); // Gộp các lần gõ thành một phiên billing: truyền sessionToken xuống. 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, });}
Component là một <input> controlled, có debounce ~250ms trước khi gọi route proxy. Mỗi phản hồi /v1/suggest trả về mảng predictions, mỗi phần tử có placeId, text, mainText, secondaryText.
Các thuộc tính role="combobox", aria-expanded, aria-activedescendant và role="option" ở trên giúp trình đọc màn hình hiểu được danh sách gợi ý — đừng bỏ qua chúng.
Mục tiêu cuối không phải là một dropdown đẹp, mà là một bản ghi ổn định bạn lưu được vào đơn hàng: text hiển thị, placeId, và lat/lon. Khi đã có toạ độ, form checkout của bạn có thể tính phí ship theo khoảng cách, gán vùng giao hàng, hay xác định phường/xã — mà không cần người dùng gõ lại.
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 đã sẵn sàng để tính ship hoặc gán vùng */} </form> );}
Bạn vừa có một component autocomplete địa chỉ Việt Nam an toàn (giấu key), gọn (debounce + một phiên billing) và dùng được bằng bàn phím. Từ lat/lon lấy được, hãy đi tiếp: