Introduction
Address autocomplete is a critical feature for modern web applications. Whether you're building an e-commerce checkout, a delivery platform, or a customer registration form, providing real-time address suggestions improves user experience and data accuracy. If you're new to address APIs, check out our Complete Guide to Address Verification APIs.
In this comprehensive guide, you'll learn how to integrate address autocomplete APIs into the three most popular JavaScript frameworks: React, Vue, and Angular. We'll cover:
- Complete implementation examples for each framework
- Performance optimization with debouncing
- Error handling and loading states
- TypeScript support
- Best practices and production tips
Prerequisites
Before we begin, make sure you have:
- Node.js (v16 or higher) and npm installed
- Basic knowledge of React, Vue, or Angular
- A Sthan.io API key (sign up for free)
- A backend API endpoint that securely handles Sthan.io authentication and forwards requests
- A text editor or IDE (VS Code recommended)
This tutorial focuses on frontend implementation using React, Vue, and Angular. For security reasons, you should never call the Sthan.io API directly from your frontend as this would expose your API credentials in the browser.
Recommended Architecture: Frontend → Your Backend → Sthan.io API
The code examples below call a generic backend endpoint (/api/address/autocomplete).
Your backend should handle authentication and forward requests to Sthan.io securely.
Read our upcoming Backend Integration Guide
for authentication and proxy setup instructions!
Implementation Examples
Choose your framework below to see the complete implementation with code examples. Each implementation follows the same patterns: debouncing, error handling, and keyboard navigation.
React Implementation
Let's build a reusable address autocomplete component in React using React hooks and TypeScript. This component will fetch suggestions as the user types and handle loading states gracefully.
Step 1: Create the Custom Hook
First, we'll create a custom hook to manage the autocomplete logic:
// hooks/useAddressAutocomplete.ts
import { useState, useEffect, useCallback } from 'react';
interface UseAddressAutocompleteResult {
suggestions: string[]; // Array of address strings
loading: boolean;
error: string | null;
fetchSuggestions: (query: string) => void;
}
export const useAddressAutocomplete = (
debounceMs: number = 300
): UseAddressAutocompleteResult => {
const [suggestions, setSuggestions] = useState<string[]>([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [query, setQuery] = useState('');
// Debounced fetch function
useEffect(() => {
if (!query || query.length < 3) {
setSuggestions([]);
return;
}
const timer = setTimeout(async () => {
setLoading(true);
setError(null);
try {
// Call YOUR backend API endpoint
const response = await fetch(
`/api/address/autocomplete?query=${encodeURIComponent(query)}`,
{
headers: {
'Content-Type': 'application/json'
}
}
);
if (!response.ok) {
throw new Error(`API error: ${response.status}`);
}
const data = await response.json();
// Your backend should return: { suggestions: string[] }
setSuggestions(data.suggestions || []);
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to fetch suggestions');
setSuggestions([]);
} finally {
setLoading(false);
}
}, debounceMs);
return () => clearTimeout(timer);
}, [query, debounceMs]);
const fetchSuggestions = useCallback((searchQuery: string) => {
setQuery(searchQuery);
}, []);
return { suggestions, loading, error, fetchSuggestions };
};
Step 2: Create the Autocomplete Component
Now let's build the UI component that uses our custom hook:
// components/AddressAutocomplete.tsx
import React, { useState, useRef, useEffect } from 'react';
import { useAddressAutocomplete } from '../hooks/useAddressAutocomplete';
interface AddressAutocompleteProps {
onSelect: (address: string) => void;
placeholder?: string;
}
export const AddressAutocomplete: React.FC<AddressAutocompleteProps> = ({
onSelect,
placeholder = 'Start typing an address...'
}) => {
const [inputValue, setInputValue] = useState('');
const [showDropdown, setShowDropdown] = useState(false);
const [selectedIndex, setSelectedIndex] = useState(-1);
const inputRef = useRef<HTMLInputElement>(null);
const dropdownRef = useRef<HTMLDivElement>(null);
const { suggestions, loading, error, fetchSuggestions } = useAddressAutocomplete();
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value;
setInputValue(value);
fetchSuggestions(value);
setShowDropdown(true);
setSelectedIndex(-1);
};
const handleSelect = (suggestion: string) => {
setInputValue(suggestion);
setShowDropdown(false);
onSelect(suggestion);
};
const handleKeyDown = (e: React.KeyboardEvent) => {
if (!showDropdown || suggestions.length === 0) return;
switch (e.key) {
case 'ArrowDown':
e.preventDefault();
setSelectedIndex(prev =>
prev < suggestions.length - 1 ? prev + 1 : prev
);
break;
case 'ArrowUp':
e.preventDefault();
setSelectedIndex(prev => prev > 0 ? prev - 1 : -1);
break;
case 'Enter':
e.preventDefault();
if (selectedIndex >= 0) {
handleSelect(suggestions[selectedIndex]);
}
break;
case 'Escape':
setShowDropdown(false);
break;
}
};
// Close dropdown when clicking outside
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (
dropdownRef.current &&
!dropdownRef.current.contains(event.target as Node) &&
!inputRef.current?.contains(event.target as Node)
) {
setShowDropdown(false);
}
};
document.addEventListener('mousedown', handleClickOutside);
return () => document.removeEventListener('mousedown', handleClickOutside);
}, []);
return (
<div className="address-autocomplete" style={{ position: 'relative' }}>
<input
ref={inputRef}
type="text"
value={inputValue}
onChange={handleInputChange}
onKeyDown={handleKeyDown}
onFocus={() => suggestions.length > 0 && setShowDropdown(true)}
placeholder={placeholder}
className="form-control"
aria-label="Address input"
aria-autocomplete="list"
aria-expanded={showDropdown}
/>
{loading && (
<div className="position-absolute end-0 top-50 translate-middle-y me-3">
<div className="spinner-border spinner-border-sm" role="status">
<span className="visually-hidden">Loading...</span>
</div>
</div>
)}
{showDropdown && !loading && suggestions.length > 0 && (
<div
ref={dropdownRef}
className="dropdown-menu show w-100"
style={{
maxHeight: '300px',
overflowY: 'auto',
position: 'absolute',
zIndex: 1000
}}
>
{suggestions.map((suggestion, index) => (
<button
key={index}
className={`dropdown-item ${index === selectedIndex ? 'active' : ''}`}
onClick={() => handleSelect(suggestion)}
type="button"
>
{suggestion}
</button>
))}
</div>
)}
{error && (
<div className="text-danger small mt-1">
<i className="fas fa-exclamation-circle me-1" aria-hidden="true"></i>
{error}
</div>
)}
</div>
);
};
Step 3: Usage Example
// App.tsx
import { AddressAutocomplete } from './components/AddressAutocomplete';
function App() {
const handleAddressSelect = (address: string) => {
console.log('Selected address:', address);
// Handle the selected address (e.g., save to form state)
};
return (
<div className="container mt-5">
<h2>Shipping Address</h2>
<AddressAutocomplete
onSelect={handleAddressSelect}
placeholder="Enter your shipping address"
/>
</div>
);
}
Vue 3 Implementation
Now let's implement the same functionality in Vue 3 using the Composition API and TypeScript. This approach provides excellent reactivity and reusability.
Step 1: Create the Composable
// composables/useAddressAutocomplete.ts
import { ref, watch } from 'vue';
export function useAddressAutocomplete(debounceMs: number = 300) {
const suggestions = ref<string[]>([]); // Array of address strings
const loading = ref(false);
const error = ref<string | null>(null);
const query = ref('');
let timeoutId: number | null = null;
const fetchSuggestions = async (searchQuery: string) => {
query.value = searchQuery;
if (!searchQuery || searchQuery.length < 3) {
suggestions.value = [];
return;
}
// Clear previous timeout
if (timeoutId) {
clearTimeout(timeoutId);
}
// Debounce the API call
timeoutId = window.setTimeout(async () => {
loading.value = true;
error.value = null;
try {
// Call YOUR backend API endpoint
const response = await fetch(
`/api/address/autocomplete?query=${encodeURIComponent(searchQuery)}`,
{
headers: {
'Content-Type': 'application/json'
}
}
);
if (!response.ok) {
throw new Error(`API error: ${response.status}`);
}
const data = await response.json();
// Your backend should return: { suggestions: string[] }
suggestions.value = data.suggestions || [];
} catch (err) {
error.value = err instanceof Error ? err.message : 'Failed to fetch suggestions';
suggestions.value = [];
} finally {
loading.value = false;
}
}, debounceMs);
};
return {
suggestions,
loading,
error,
fetchSuggestions
};
}
Step 2: Create the Component
<!-- components/AddressAutocomplete.vue -->
<template>
<div class="address-autocomplete" style="position: relative">
<input
v-model="inputValue"
@input="handleInput"
@keydown="handleKeyDown"
@focus="handleFocus"
type="text"
:placeholder="placeholder"
class="form-control"
aria-label="Address input"
aria-autocomplete="list"
:aria-expanded="showDropdown"
/>
<div
v-if="loading"
class="position-absolute end-0 top-50 translate-middle-y me-3"
>
<div class="spinner-border spinner-border-sm" role="status">
<span class="visually-hidden">Loading...</span>
</div>
</div>
<div
v-if="showDropdown && !loading && suggestions.length > 0"
ref="dropdownRef"
class="dropdown-menu show w-100"
style="max-height: 300px; overflow-y: auto; position: absolute; z-index: 1000"
>
<button
v-for="(suggestion, index) in suggestions"
:key="index"
:class="['dropdown-item', { active: index === selectedIndex }]"
@click="handleSelect(suggestion)"
type="button"
>
{{ suggestion }}
</button>
</div>
<div v-if="error" class="text-danger small mt-1">
<i class="fas fa-exclamation-circle me-1" aria-hidden="true"></i>
{{ error }}
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue';
import { useAddressAutocomplete } from '../composables/useAddressAutocomplete';
interface Props {
placeholder?: string;
}
interface Emits {
(e: 'select', address: string): void;
}
const props = withDefaults(defineProps<Props>(), {
placeholder: 'Start typing an address...'
});
const emit = defineEmits<Emits>();
const inputValue = ref('');
const showDropdown = ref(false);
const selectedIndex = ref(-1);
const dropdownRef = ref<HTMLElement | null>(null);
const { suggestions, loading, error, fetchSuggestions } = useAddressAutocomplete();
const handleInput = (event: Event) => {
const target = event.target as HTMLInputElement;
fetchSuggestions(target.value);
showDropdown.value = true;
selectedIndex.value = -1;
};
const handleSelect = (suggestion: string) => {
inputValue.value = suggestion;
showDropdown.value = false;
emit('select', suggestion);
};
const handleKeyDown = (event: KeyboardEvent) => {
if (!showDropdown.value || suggestions.value.length === 0) return;
switch (event.key) {
case 'ArrowDown':
event.preventDefault();
selectedIndex.value =
selectedIndex.value < suggestions.value.length - 1
? selectedIndex.value + 1
: selectedIndex.value;
break;
case 'ArrowUp':
event.preventDefault();
selectedIndex.value = selectedIndex.value > 0 ? selectedIndex.value - 1 : -1;
break;
case 'Enter':
event.preventDefault();
if (selectedIndex.value >= 0) {
handleSelect(suggestions.value[selectedIndex.value]);
}
break;
case 'Escape':
showDropdown.value = false;
break;
}
};
const handleFocus = () => {
if (suggestions.value.length > 0) {
showDropdown.value = true;
}
};
// Close dropdown on outside click
const handleClickOutside = (event: MouseEvent) => {
if (dropdownRef.value && !dropdownRef.value.contains(event.target as Node)) {
showDropdown.value = false;
}
};
onMounted(() => {
document.addEventListener('mousedown', handleClickOutside);
});
onUnmounted(() => {
document.removeEventListener('mousedown', handleClickOutside);
});
</script>
Step 3: Usage Example
<!-- App.vue -->
<template>
<div class="container mt-5">
<h2>Shipping Address</h2>
<AddressAutocomplete
placeholder="Enter your shipping address"
@select="handleAddressSelect"
/>
</div>
</template>
<script setup lang="ts">
import AddressAutocomplete from './components/AddressAutocomplete.vue';
const handleAddressSelect = (address: string) => {
console.log('Selected address:', address);
// Handle the selected address (e.g., save to form state)
};
</script>
Angular Implementation
Finally, let's implement address autocomplete in Angular using services, RxJS operators, and TypeScript. Angular's powerful dependency injection makes this implementation very maintainable.
Step 1: Create the Service
// services/address-autocomplete.service.ts
import { Injectable } from '@angular/core';
import { HttpClient, HttpHeaders } from '@angular/common/http';
import { Observable, of } from 'rxjs';
import { map, catchError, debounceTime, distinctUntilChanged, switchMap } from 'rxjs/operators';
@Injectable({
providedIn: 'root'
})
export class AddressAutocompleteService {
// Call YOUR backend API endpoint
private readonly apiUrl = '/api/address/autocomplete';
constructor(private http: HttpClient) {}
searchAddresses(query: string): Observable<string[]> {
if (!query || query.length < 3) {
return of([]);
}
const params = { query: query };
// Your backend handles authentication
return this.http.get<any>(this.apiUrl, { params }).pipe(
map(response => response.suggestions || []),
catchError(error => {
console.error('Address autocomplete error:', error);
return of([]);
})
);
}
}
Step 2: Create the Component
// components/address-autocomplete/address-autocomplete.component.ts
import { Component, EventEmitter, Input, Output, OnInit, OnDestroy } from '@angular/core';
import { FormControl } from '@angular/forms';
import { Subject, Subscription } from 'rxjs';
import { debounceTime, distinctUntilChanged, switchMap, tap } from 'rxjs/operators';
import { AddressAutocompleteService } from '../../services/address-autocomplete.service';
@Component({
selector: 'app-address-autocomplete',
templateUrl: './address-autocomplete.component.html',
styleUrls: ['./address-autocomplete.component.css']
})
export class AddressAutocompleteComponent implements OnInit, OnDestroy {
@Input() placeholder: string = 'Start typing an address...';
@Input() debounceMs: number = 300;
@Output() select = new EventEmitter<string>();
searchControl = new FormControl('');
suggestions: string[] = [];
loading = false;
showDropdown = false;
selectedIndex = -1;
private subscription?: Subscription;
constructor(private addressService: AddressAutocompleteService) {}
ngOnInit(): void {
this.subscription = this.searchControl.valueChanges.pipe(
tap(() => {
this.loading = true;
this.showDropdown = true;
this.selectedIndex = -1;
}),
debounceTime(this.debounceMs),
distinctUntilChanged(),
switchMap(query => this.addressService.searchAddresses(query || '')),
tap(() => this.loading = false)
).subscribe(suggestions => {
this.suggestions = suggestions;
});
}
ngOnDestroy(): void {
this.subscription?.unsubscribe();
}
onSelectSuggestion(suggestion: string): void {
this.searchControl.setValue(suggestion);
this.showDropdown = false;
this.select.emit(suggestion);
}
onKeyDown(event: KeyboardEvent): void {
if (!this.showDropdown || this.suggestions.length === 0) return;
switch (event.key) {
case 'ArrowDown':
event.preventDefault();
this.selectedIndex = this.selectedIndex < this.suggestions.length - 1
? this.selectedIndex + 1
: this.selectedIndex;
break;
case 'ArrowUp':
event.preventDefault();
this.selectedIndex = this.selectedIndex > 0 ? this.selectedIndex - 1 : -1;
break;
case 'Enter':
event.preventDefault();
if (this.selectedIndex >= 0) {
this.onSelectSuggestion(this.suggestions[this.selectedIndex]);
}
break;
case 'Escape':
this.showDropdown = false;
break;
}
}
onFocus(): void {
if (this.suggestions.length > 0) {
this.showDropdown = true;
}
}
onBlur(): void {
// Delay to allow click on dropdown item
setTimeout(() => this.showDropdown = false, 200);
}
}
Step 3: Component Template
<!-- address-autocomplete.component.html -->
<div class="address-autocomplete" style="position: relative">
<input
[formControl]="searchControl"
(keydown)="onKeyDown($event)"
(focus)="onFocus()"
(blur)="onBlur()"
type="text"
[placeholder]="placeholder"
class="form-control"
aria-label="Address input"
aria-autocomplete="list"
[attr.aria-expanded]="showDropdown"
/>
<div *ngIf="loading" class="position-absolute end-0 top-50 translate-middle-y me-3">
<div class="spinner-border spinner-border-sm" role="status">
<span class="visually-hidden">Loading...</span>
</div>
</div>
<div
*ngIf="showDropdown && !loading && suggestions.length > 0"
class="dropdown-menu show w-100"
style="max-height: 300px; overflow-y: auto; position: absolute; z-index: 1000"
>
<button
*ngFor="let suggestion of suggestions; let i = index"
[class.active]="i === selectedIndex"
(click)="onSelectSuggestion(suggestion)"
class="dropdown-item"
type="button"
>
{{ suggestion }}
</button>
</div>
</div>
Step 4: Usage Example
// app.component.ts
import { Component } from '@angular/core';
@Component({
selector: 'app-root',
template: `
<div class="container mt-5">
<h2>Shipping Address</h2>
<app-address-autocomplete
placeholder="Enter your shipping address"
(select)="onAddressSelect($event)"
></app-address-autocomplete>
</div>
`
})
export class AppComponent {
onAddressSelect(address: string): void {
console.log('Selected address:', address);
// Handle the selected address
}
}
Step 5: Module Configuration
// app.module.ts
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { HttpClientModule } from '@angular/common/http';
import { ReactiveFormsModule } from '@angular/forms';
import { AppComponent } from './app.component';
import { AddressAutocompleteComponent } from './components/address-autocomplete/address-autocomplete.component';
@NgModule({
declarations: [
AppComponent,
AddressAutocompleteComponent
],
imports: [
BrowserModule,
HttpClientModule,
ReactiveFormsModule
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
Performance Optimizations
1. Debouncing (Already Implemented)
All three implementations include debouncing (300ms default) to prevent excessive API calls as users type.
This significantly reduces API usage and improves performance. You can adjust this value by passing a different
debounceMs parameter. Check our rate limiting documentation
for more optimization tips.
2. Minimum Query Length (Already Implemented)
We only trigger searches when the query is at least 3 characters long. This prevents useless API calls
for very short queries that would return too many results. The examples check query.length < 3 before making any requests.
3. Request Cancellation (Angular Only)
Angular's implementation uses RxJS switchMap which automatically cancels pending requests when new ones are initiated.
For React/Vue, you can add this optimization using AbortController:
// React/Vue: Add request cancellation
const abortController = new AbortController();
fetch(url, { signal: abortController.signal })
.then(response => response.json())
.catch(err => {
if (err.name === 'AbortError') {
// Request was cancelled, ignore
return;
}
throw err;
});
// Cancel previous request when new one starts
abortController.abort();
4. Additional: Client-Side Caching
Optional enhancement: Consider adding client-side caching for repeated queries to further reduce API calls:
// Simple cache example
const cache = new Map<string, string[]>();
const getCachedOrFetch = async (query: string) => {
if (cache.has(query)) {
return cache.get(query);
}
const results = await fetchFromAPI(query);
cache.set(query, results);
return results;
};
Best Practices
Security
- Never expose API keys in frontend code - Use environment variables and proxy requests through your backend. Learn more in our API security documentation
- Implement rate limiting on your server to prevent API abuse
- Validate and sanitize all address data before storing
Accessibility
- Use proper ARIA attributes (
aria-label,aria-autocomplete,aria-expanded) following WCAG 2.1 guidelines - Support keyboard navigation (Arrow keys, Enter, Escape)
- Provide loading states and error messages
- Ensure sufficient color contrast for dropdown items
User Experience
- Show loading indicators during API requests
- Display helpful error messages when requests fail
- Highlight the selected item in the dropdown
- Close dropdown on outside click or Escape key
- Allow users to type freely without forcing autocomplete selection
Error Handling
- Handle network failures gracefully
- Provide fallback behavior when API is unavailable
- Log errors for debugging without exposing sensitive information to users
- Consider implementing retry logic for transient failures
Testing Your Implementation
Here are some key test scenarios to cover:
Functional Tests
- Test typing and receiving suggestions
- Test selecting a suggestion updates the input
- Test keyboard navigation (up/down arrows, enter, escape)
- Test dropdown closes on outside click
- Test minimum query length requirement
Edge Cases
- Empty results from API
- API errors and network failures
- Very long addresses
- Special characters in addresses
- Rapid typing and debouncing behavior
Conclusion
You now have complete, production-ready address autocomplete implementations for React, Vue, and Angular. Each implementation follows framework best practices and includes:
- ✅ TypeScript support for type safety
- ✅ Debouncing for performance optimization
- ✅ Keyboard navigation support
- ✅ Loading states and error handling
- ✅ Accessibility features
- ✅ Responsive design with Bootstrap
The patterns shown here can be adapted to any address autocomplete API, not just Sthan.io. Feel free to customize the styling, add additional features, or integrate with your existing form libraries.
Ready to Get Started?
Create your free Sthan.io account and get 10,000 free API calls per month. No credit card required.
Get Your Free API Key View PricingFrequently Asked Questions
For production applications, we strongly recommend using a backend proxy instead of calling the API directly from your frontend. This approach keeps your API key secure, allows you to implement rate limiting and request validation, and gives you better control over API usage. While direct frontend calls work for development and prototyping, exposing your API key in client-side code creates security risks. As mentioned in the Architecture & Security note above, your backend should handle authentication and forward requests to the Sthan.io API securely.
Sthan.io currently supports USA (full address autocomplete with USPS-verified data) and India (Pincode, City, and Locality autocomplete). For US addresses, you get complete street-level autocomplete with unit numbers and smart abbreviation handling. For Indian addresses, you can autocomplete Pincodes, Cities, and Localities with multiple display options. Additional countries and regions are being evaluated for future expansion. Check our products page for the latest coverage.
Always provide a manual entry fallback for addresses that don't appear in autocomplete suggestions. This is especially important for new constructions, rural addresses, or non-standard locations. Display a message like "Can't find your address? Enter it manually" and allow users to type freely without forcing autocomplete selection. You can also implement address verification on form submission to catch any typos or formatting issues in manually-entered addresses.
Address autocomplete helps users type addresses faster by suggesting valid addresses as they type, improving user experience during data entry. Address verification validates and corrects addresses after submission, ensuring the address is deliverable and properly formatted for shipping or mailing. Use autocomplete for better UX during form filling, and verification for data quality assurance before processing orders or sending mail. For a comprehensive overview of address verification APIs, check out our Complete Guide to Address Verification APIs.
Yes, the Sthan.io API accepts optional parameters to filter results by country, state, or region. This is particularly useful for applications that only ship to specific areas or need to collect addresses within certain jurisdictions. You can pass these parameters in your API request to limit suggestions to your target geography. Check our API documentation for the complete list of available filtering parameters and usage examples.
Sthan.io maintains a proprietary address database sourced from multiple authoritative government and commercial data providers, with daily updates to reflect new construction, postal changes, and geographic updates. Our database is continuously refreshed to ensure you have access to the most current address information available. This comprehensive approach ensures high accuracy and coverage for addresses across our supported regions.
Absolutely! The autocomplete components shown in this guide integrate seamlessly with popular form libraries.
For React Hook Form, you can use the register function or Controller component to connect our autocomplete to your form state.
With Formik, you can use Field component with a custom render function or connect it via setFieldValue.
The same patterns work for Angular Reactive Forms (using formControlName) and Vue form libraries. The key is treating the autocomplete as a controlled component that updates your form's state.
Related Articles
Complete Guide to Address Verification APIs
Learn everything about address verification APIs and how to choose the right one for your application.
E-commerce Cart Abandonment Guide Coming Soon
Discover how address validation can reduce cart abandonment rates by 35%.