Custom hooks to easily integrate CountryDataAPI with your React application. No additional packages needed - just fetch and hooks!
This guide provides production-ready React hooks for:
All hooks include loading states, error handling, and TypeScript support.
No additional packages required! CountryDataAPI works with the native fetch API.
# Just install React (if you haven't already)
npm install react react-dom
First, define the TypeScript interfaces:
// types/address.ts
export interface Country {
id: string;
name: string;
}
export interface State {
id: string;
name: string;
}
export interface City {
id: string;
name: string;
}
export interface ApiResponse<T> {
success: boolean;
data: T[];
error?: {
code: string;
message: string;
};
}
export interface UseDataResult<T> {
data: T[];
loading: boolean;
error: string | null;
refetch: () => void;
}
Create a configuration file for your API settings:
// config/api.ts
export const API_CONFIG = {
baseUrl: 'https://api.countrydataapi.com/v1',
apiKey: process.env.REACT_APP_COUNTRY_API_KEY || '',
};
// Helper to build URL with params
export function buildApiUrl(
endpoint: string,
params: Record<string, string>
): string {
const url = new URL(`${API_CONFIG.baseUrl}${endpoint}`);
url.searchParams.set('apikey', API_CONFIG.apiKey);
Object.entries(params).forEach(([key, value]) => {
if (value) url.searchParams.set(key, value);
});
return url.toString();
}
Important: Store your API key in
.envfile asREACT_APP_COUNTRY_API_KEYand never commit it to version control!
Load all countries with optional caching:
// hooks/useCountries.ts
import { useState, useEffect, useCallback } from 'react';
import { Country, ApiResponse, UseDataResult } from '../types/address';
import { buildApiUrl } from '../config/api';
const CACHE_KEY = 'countrydataapi_countries';
const CACHE_DURATION = 7 * 24 * 60 * 60 * 1000; // 7 days
interface CachedData {
data: Country[];
timestamp: number;
}
export function useCountries(
lang: string = 'en',
enableCache: boolean = true
): UseDataResult<Country> {
const [data, setData] = useState<Country[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const fetchCountries = useCallback(async () => {
setLoading(true);
setError(null);
// Check cache first
if (enableCache) {
const cached = localStorage.getItem(CACHE_KEY);
if (cached) {
const { data: cachedData, timestamp }: CachedData = JSON.parse(cached);
if (Date.now() - timestamp < CACHE_DURATION) {
setData(cachedData);
setLoading(false);
return;
}
}
}
try {
const url = buildApiUrl('/select/countries', { lang });
const response = await fetch(url);
const result: ApiResponse<Country> = await response.json();
if (!result.success) {
throw new Error(result.error?.message || 'Failed to load countries');
}
setData(result.data);
// Cache the result
if (enableCache) {
const cacheData: CachedData = {
data: result.data,
timestamp: Date.now(),
};
localStorage.setItem(CACHE_KEY, JSON.stringify(cacheData));
}
} catch (err) {
const message = err instanceof Error ? err.message : 'Network error';
setError(message);
} finally {
setLoading(false);
}
}, [lang, enableCache]);
useEffect(() => {
fetchCountries();
}, [fetchCountries]);
return { data, loading, error, refetch: fetchCountries };
}
Load states filtered by country:
// hooks/useStates.ts
import { useState, useEffect, useCallback } from 'react';
import { State, ApiResponse, UseDataResult } from '../types/address';
import { buildApiUrl } from '../config/api';
export function useStates(
countryId: string | null,
lang: string = 'en'
): UseDataResult<State> {
const [data, setData] = useState<State[]>([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const fetchStates = useCallback(async () => {
if (!countryId) {
setData([]);
return;
}
setLoading(true);
setError(null);
try {
const url = buildApiUrl('/select/states', {
country: countryId,
lang,
});
const response = await fetch(url);
const result: ApiResponse<State> = await response.json();
if (!result.success) {
throw new Error(result.error?.message || 'Failed to load states');
}
setData(result.data);
} catch (err) {
const message = err instanceof Error ? err.message : 'Network error';
setError(message);
setData([]);
} finally {
setLoading(false);
}
}, [countryId, lang]);
useEffect(() => {
fetchStates();
}, [fetchStates]);
return { data, loading, error, refetch: fetchStates };
}
Load cities filtered by state:
// hooks/useCities.ts
import { useState, useEffect, useCallback } from 'react';
import { City, ApiResponse, UseDataResult } from '../types/address';
import { buildApiUrl } from '../config/api';
export function useCities(
stateId: string | null,
lang: string = 'en'
): UseDataResult<City> {
const [data, setData] = useState<City[]>([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const fetchCities = useCallback(async () => {
if (!stateId) {
setData([]);
return;
}
setLoading(true);
setError(null);
try {
const url = buildApiUrl('/select/cities', {
state: stateId,
lang,
});
const response = await fetch(url);
const result: ApiResponse<City> = await response.json();
if (!result.success) {
throw new Error(result.error?.message || 'Failed to load cities');
}
setData(result.data);
} catch (err) {
const message = err instanceof Error ? err.message : 'Network error';
setError(message);
setData([]);
} finally {
setLoading(false);
}
}, [stateId, lang]);
useEffect(() => {
fetchCities();
}, [fetchCities]);
return { data, loading, error, refetch: fetchCities };
}
Complete example component using all hooks:
// components/AddressForm.tsx
import React, { useState } from 'react';
import { useCountries } from '../hooks/useCountries';
import { useStates } from '../hooks/useStates';
import { useCities } from '../hooks/useCities';
import './AddressForm.css';
interface AddressFormData {
country: string;
state: string;
city: string;
street: string;
zipCode: string;
}
interface AddressFormProps {
onSubmit: (data: AddressFormData) => void;
initialData?: Partial<AddressFormData>;
lang?: string;
}
export function AddressForm({
onSubmit,
initialData,
lang = 'en',
}: AddressFormProps) {
const [formData, setFormData] = useState<Partial<AddressFormData>>(
initialData || {}
);
const {
data: countries,
loading: loadingCountries,
error: countriesError,
} = useCountries(lang);
const {
data: states,
loading: loadingStates,
error: statesError,
} = useStates(formData.country || null, lang);
const {
data: cities,
loading: loadingCities,
error: citiesError,
} = useCities(formData.state || null, lang);
const handleCountryChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
setFormData({
...formData,
country: e.target.value,
state: '',
city: '',
});
};
const handleStateChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
setFormData({
...formData,
state: e.target.value,
city: '',
});
};
const handleCityChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
setFormData({
...formData,
city: e.target.value,
});
};
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setFormData({
...formData,
[e.target.name]: e.target.value,
});
};
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (
formData.country &&
formData.state &&
formData.city &&
formData.street
) {
onSubmit(formData as AddressFormData);
}
};
return (
<form onSubmit={handleSubmit} className="address-form">
<div className="form-group">
<label htmlFor="country">
Country *
{loadingCountries && <span className="loading"> Loading...</span>}
</label>
<select
id="country"
name="country"
value={formData.country || ''}
onChange={handleCountryChange}
disabled={loadingCountries}
required
>
<option value="">Select country...</option>
{countries.map((country) => (
<option key={country.id} value={country.id}>
{country.name}
</option>
))}
</select>
{countriesError && (
<span className="error">{countriesError}</span>
)}
</div>
<div className="form-group">
<label htmlFor="state">
State/Province *
{loadingStates && <span className="loading"> Loading...</span>}
</label>
<select
id="state"
name="state"
value={formData.state || ''}
onChange={handleStateChange}
disabled={!formData.country || loadingStates}
required
>
<option value="">Select state...</option>
{states.map((state) => (
<option key={state.id} value={state.id}>
{state.name}
</option>
))}
</select>
{statesError && <span className="error">{statesError}</span>}
</div>
<div className="form-group">
<label htmlFor="city">
City *
{loadingCities && <span className="loading"> Loading...</span>}
</label>
<select
id="city"
name="city"
value={formData.city || ''}
onChange={handleCityChange}
disabled={!formData.state || loadingCities}
required
>
<option value="">Select city...</option>
{cities.map((city) => (
<option key={city.id} value={city.id}>
{city.name}
</option>
))}
</select>
{citiesError && <span className="error">{citiesError}</span>}
</div>
<div className="form-group">
<label htmlFor="street">Street Address *</label>
<input
type="text"
id="street"
name="street"
value={formData.street || ''}
onChange={handleInputChange}
placeholder="123 Main Street"
required
/>
</div>
<div className="form-group">
<label htmlFor="zipCode">ZIP Code</label>
<input
type="text"
id="zipCode"
name="zipCode"
value={formData.zipCode || ''}
onChange={handleInputChange}
placeholder="12345"
/>
</div>
<button
type="submit"
disabled={
!formData.country ||
!formData.state ||
!formData.city ||
!formData.street
}
>
Submit Address
</button>
</form>
);
}
/* AddressForm.css */
.address-form {
max-width: 600px;
margin: 0 auto;
}
.form-group {
margin-bottom: 20px;
}
.form-group label {
display: block;
margin-bottom: 8px;
font-weight: 600;
color: #1f2937;
font-size: 14px;
}
.form-group select,
.form-group input {
width: 100%;
padding: 10px 12px;
border: 2px solid #e5e7eb;
border-radius: 6px;
font-size: 16px;
background-color: white;
transition: border-color 0.2s;
}
.form-group select:focus,
.form-group input:focus {
outline: none;
border-color: #3b82f6;
}
.form-group select:disabled,
.form-group input:disabled {
background-color: #f9fafb;
cursor: not-allowed;
opacity: 0.6;
}
.loading {
color: #6b7280;
font-size: 12px;
font-weight: normal;
font-style: italic;
}
.error {
display: block;
margin-top: 6px;
color: #ef4444;
font-size: 13px;
}
button[type='submit'] {
width: 100%;
padding: 12px 24px;
background-color: #3b82f6;
color: white;
border: none;
border-radius: 6px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: background-color 0.2s;
}
button[type='submit']:hover:not(:disabled) {
background-color: #2563eb;
}
button[type='submit']:disabled {
background-color: #cbd5e0;
cursor: not-allowed;
}
// App.tsx
import React from 'react';
import { AddressForm } from './components/AddressForm';
function App() {
const handleAddressSubmit = (data: any) => {
console.log('Address submitted:', data);
// Send to your backend
// fetch('/api/save-address', {
// method: 'POST',
// headers: { 'Content-Type': 'application/json' },
// body: JSON.stringify(data),
// });
};
return (
<div className="app">
<h1>Delivery Address</h1>
<AddressForm onSubmit={handleAddressSubmit} lang="en" />
</div>
);
}
export default App;
For more complex scenarios, create a combined hook:
// hooks/useAddress.ts
import { useState, useCallback } from 'react';
import { useCountries } from './useCountries';
import { useStates } from './useStates';
import { useCities } from './useCities';
export function useAddress(lang: string = 'en') {
const [selectedCountry, setSelectedCountry] = useState<string | null>(null);
const [selectedState, setSelectedState] = useState<string | null>(null);
const [selectedCity, setSelectedCity] = useState<string | null>(null);
const countries = useCountries(lang);
const states = useStates(selectedCountry, lang);
const cities = useCities(selectedState, lang);
const selectCountry = useCallback((countryId: string) => {
setSelectedCountry(countryId);
setSelectedState(null);
setSelectedCity(null);
}, []);
const selectState = useCallback((stateId: string) => {
setSelectedState(stateId);
setSelectedCity(null);
}, []);
const selectCity = useCallback((cityId: string) => {
setSelectedCity(cityId);
}, []);
const reset = useCallback(() => {
setSelectedCountry(null);
setSelectedState(null);
setSelectedCity(null);
}, []);
return {
countries,
states,
cities,
selectedCountry,
selectedState,
selectedCity,
selectCountry,
selectState,
selectCity,
reset,
};
}
Pro Tip: Use React Context to provide the API key globally instead of importing from config in every hook. This makes testing easier and improves flexibility.