Benutzerdefinierte Hooks zur einfachen Integration von CountryDataAPI in Ihre React-Anwendung. Keine zusätzlichen Pakete erforderlich - nur fetch und Hooks!
Diese Anleitung bietet produktionsreife React Hooks für:
Alle Hooks beinhalten Ladezustände, Fehlerbehandlung und TypeScript-Unterstützung.
Keine zusätzlichen Pakete erforderlich! CountryDataAPI funktioniert mit der nativen fetch API.
# Installieren Sie einfach React (falls noch nicht geschehen)
npm install react react-dom
Definieren Sie zunächst die 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;
}
Erstellen Sie eine Konfigurationsdatei für Ihre API-Einstellungen:
// config/api.ts
export const API_CONFIG = {
baseUrl: 'https://api.countrydataapi.com/v1',
apiKey: process.env.REACT_APP_COUNTRY_API_KEY || '',
};
// Hilfsfunktion zum Erstellen der URL mit Parametern
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();
}
Wichtig: Speichern Sie Ihren API-Schlüssel in der
.env-Datei alsREACT_APP_COUNTRY_API_KEYund committen Sie ihn niemals in die Versionskontrolle!
Alle Länder mit optionalem Caching laden:
// 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 Tage
interface CachedData {
data: Country[];
timestamp: number;
}
export function useCountries(
lang: string = 'de',
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);
// Zuerst Cache prüfen
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 || 'Fehler beim Laden der Länder');
}
setData(result.data);
// Ergebnis zwischenspeichern
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 : 'Netzwerkfehler';
setError(message);
} finally {
setLoading(false);
}
}, [lang, enableCache]);
useEffect(() => {
fetchCountries();
}, [fetchCountries]);
return { data, loading, error, refetch: fetchCountries };
}
Bundesländer nach Land gefiltert laden:
// 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 = 'de'
): 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 || 'Fehler beim Laden der Bundesländer');
}
setData(result.data);
} catch (err) {
const message = err instanceof Error ? err.message : 'Netzwerkfehler';
setError(message);
setData([]);
} finally {
setLoading(false);
}
}, [countryId, lang]);
useEffect(() => {
fetchStates();
}, [fetchStates]);
return { data, loading, error, refetch: fetchStates };
}
Städte nach Bundesland gefiltert laden:
// 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 = 'de'
): 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 || 'Fehler beim Laden der Städte');
}
setData(result.data);
} catch (err) {
const message = err instanceof Error ? err.message : 'Netzwerkfehler';
setError(message);
setData([]);
} finally {
setLoading(false);
}
}, [stateId, lang]);
useEffect(() => {
fetchCities();
}, [fetchCities]);
return { data, loading, error, refetch: fetchCities };
}
Vollständiges Beispielkomponent mit allen 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 = 'de',
}: 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">
Land *
{loadingCountries && <span className="loading"> Wird geladen...</span>}
</label>
<select
id="country"
name="country"
value={formData.country || ''}
onChange={handleCountryChange}
disabled={loadingCountries}
required
>
<option value="">Land auswählen...</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">
Bundesland *
{loadingStates && <span className="loading"> Wird geladen...</span>}
</label>
<select
id="state"
name="state"
value={formData.state || ''}
onChange={handleStateChange}
disabled={!formData.country || loadingStates}
required
>
<option value="">Bundesland auswählen...</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">
Stadt *
{loadingCities && <span className="loading"> Wird geladen...</span>}
</label>
<select
id="city"
name="city"
value={formData.city || ''}
onChange={handleCityChange}
disabled={!formData.state || loadingCities}
required
>
<option value="">Stadt auswählen...</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">Straße *</label>
<input
type="text"
id="street"
name="street"
value={formData.street || ''}
onChange={handleInputChange}
placeholder="Hauptstraße 123"
required
/>
</div>
<div className="form-group">
<label htmlFor="zipCode">PLZ</label>
<input
type="text"
id="zipCode"
name="zipCode"
value={formData.zipCode || ''}
onChange={handleInputChange}
placeholder="10115"
/>
</div>
<button
type="submit"
disabled={
!formData.country ||
!formData.state ||
!formData.city ||
!formData.street
}
>
Adresse Absenden
</button>
</form>
);
}
// App.tsx
import React from 'react';
import { AddressForm } from './components/AddressForm';
function App() {
const handleAddressSubmit = (data: any) => {
console.log('Adresse übermittelt:', data);
// An Ihr Backend senden
// fetch('/api/save-address', {
// method: 'POST',
// headers: { 'Content-Type': 'application/json' },
// body: JSON.stringify(data),
// });
};
return (
<div className="app">
<h1>Lieferadresse</h1>
<AddressForm onSubmit={handleAddressSubmit} lang="de" />
</div>
);
}
export default App;
Für komplexere Szenarien erstellen Sie einen kombinierten 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 = 'de') {
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,
};
}
Profi-Tipp: Verwenden Sie React Context, um den API-Schlüssel global bereitzustellen, anstatt ihn in jedem Hook aus der Config zu importieren. Das erleichtert das Testen und verbessert die Flexibilität.