API Documentation - Endpoints & Examples

React Integration

Build React address forms with custom hooks for CountryDataAPI

Custom hooks to easily integrate CountryDataAPI with your React application. No additional packages needed - just fetch and hooks!

Overview

This guide provides production-ready React hooks for:

  • useCountries - Load all countries with caching
  • useStates - Load states by country ID
  • useCities - Load cities by state ID
  • useAddress - Combined hook for complete forms

All hooks include loading states, error handling, and TypeScript support.

Installation

No additional packages required! CountryDataAPI works with the native fetch API.

# Just install React (if you haven't already)
npm install react react-dom

TypeScript Types

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

Configuration

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 .env file as REACT_APP_COUNTRY_API_KEY and never commit it to version control!

Custom Hooks

useCountries Hook

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

useStates Hook

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

useCities Hook

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

AddressForm Component

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

Component Styles

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

Usage Example

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

Advanced: useAddress Combined Hook

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

Next Steps

Need Help?


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.