Dokumentation

React Integration

Erstellen Sie React-Adressformulare mit benutzerdefinierten Hooks für CountryDataAPI

Benutzerdefinierte Hooks zur einfachen Integration von CountryDataAPI in Ihre React-Anwendung. Keine zusätzlichen Pakete erforderlich - nur fetch und Hooks!

Überblick

Diese Anleitung bietet produktionsreife React Hooks für:

  • useCountries - Alle Länder mit Caching laden
  • useStates - Bundesländer nach Land-ID laden
  • useCities - Städte nach Bundesland-ID laden
  • useAddress - Kombinierter Hook für vollständige Formulare

Alle Hooks beinhalten Ladezustände, Fehlerbehandlung und TypeScript-Unterstützung.

Installation

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

TypeScript-Typen

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;
}

Konfiguration

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 als REACT_APP_COUNTRY_API_KEY und committen Sie ihn niemals in die Versionskontrolle!

Benutzerdefinierte Hooks

useCountries Hook

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 };
}

useStates Hook

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 };
}

useCities Hook

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 };
}

AddressForm-Komponente

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>
  );
}

Verwendungsbeispiel

// 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;

Fortgeschritten: Kombinierter useAddress Hook

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,
  };
}

Nächste Schritte

Brauchen Sie Hilfe?


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.