How to Integrate Address Autocomplete in React, Vue, and Angular
A comprehensive guide to implementing address autocomplete with real-world code examples, performance optimization, and best practices for modern JavaScript frameworks.
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 token (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.
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('pointerdown', handleClickOutside);
return () => document.removeEventListener('pointerdown', 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>
);
}
Try it live
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('pointerdown', handleClickOutside);
});
onUnmounted(() => {
document.removeEventListener('pointerdown', 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 tokens 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
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.
Create your free Sthan.io account and get 100,000 free API calls per month. No credit card required.
Key Takeaways
- Always use a backend proxy to keep your API token secure -- never expose credentials in frontend code
- Debouncing (300ms) and minimum query length (3 chars) are essential for reducing unnecessary API calls
- All three frameworks (React, Vue, Angular) follow the same core pattern: custom hook/composable/service + UI component
- Implement keyboard navigation and ARIA attributes for accessibility compliance
- Consider client-side caching and request cancellation for production optimization
Share This Article
Frequently Asked Questions
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.
Ready to Get Started?
Get 100,000 free autocomplete requests per month. No credit card required.