Documentação

Integração com React

Crie formulários de endereço React com hooks personalizados para CountryDataAPI

Hooks personalizados para integrar facilmente a CountryDataAPI com sua aplicação React. Nenhum pacote adicional necessário - apenas fetch e hooks!

Visão Geral

Este guia fornece hooks React prontos para produção para:

  • useCountries - Carregar todos os países com cache
  • useStates - Carregar estados por ID do país
  • useCities - Carregar cidades por ID do estado
  • useAddress - Hook combinado para formulários completos

Todos os hooks incluem estados de carregamento, tratamento de erros e suporte TypeScript.

Instalação

Nenhum pacote adicional necessário! A CountryDataAPI funciona com a API fetch nativa.

# Apenas instale o React (se ainda não instalou)
npm install react react-dom

Tipos TypeScript

Primeiro, defina as interfaces TypeScript:

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

Configuração

Crie um arquivo de configuração para as configurações da API:

// config/api.ts

export const API_CONFIG = {
  baseUrl: 'https://api.countrydataapi.com/v1',
  apiKey: process.env.REACT_APP_COUNTRY_API_KEY || '',
};

// Helper para construir URL com parâmetros
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();
}

Importante: Armazene sua chave de API no arquivo .env como REACT_APP_COUNTRY_API_KEY e nunca a commit no controle de versão!

Hooks Personalizados

Hook useCountries

Carregar todos os países com cache opcional:

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

interface CachedData {
  data: Country[];
  timestamp: number;
}

export function useCountries(
  lang: string = 'pt',
  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);

    // Verificar cache primeiro
    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 || 'Falha ao carregar países');
      }

      setData(result.data);

      // Armazenar resultado em cache
      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 : 'Erro de rede';
      setError(message);
    } finally {
      setLoading(false);
    }
  }, [lang, enableCache]);

  useEffect(() => {
    fetchCountries();
  }, [fetchCountries]);

  return { data, loading, error, refetch: fetchCountries };
}

Hook useStates

Carregar estados filtrados por país:

// 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 = 'pt'
): 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 || 'Falha ao carregar estados');
      }

      setData(result.data);
    } catch (err) {
      const message = err instanceof Error ? err.message : 'Erro de rede';
      setError(message);
      setData([]);
    } finally {
      setLoading(false);
    }
  }, [countryId, lang]);

  useEffect(() => {
    fetchStates();
  }, [fetchStates]);

  return { data, loading, error, refetch: fetchStates };
}

Hook useCities

Carregar cidades filtradas por estado:

// 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 = 'pt'
): 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 || 'Falha ao carregar cidades');
      }

      setData(result.data);
    } catch (err) {
      const message = err instanceof Error ? err.message : 'Erro de rede';
      setError(message);
      setData([]);
    } finally {
      setLoading(false);
    }
  }, [stateId, lang]);

  useEffect(() => {
    fetchCities();
  }, [fetchCities]);

  return { data, loading, error, refetch: fetchCities };
}

Componente AddressForm

Exemplo completo de componente usando todos os 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 = 'pt',
}: 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">
          País *
          {loadingCountries && <span className="loading"> Carregando...</span>}
        </label>
        <select
          id="country"
          name="country"
          value={formData.country || ''}
          onChange={handleCountryChange}
          disabled={loadingCountries}
          required
        >
          <option value="">Selecione o país...</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">
          Estado *
          {loadingStates && <span className="loading"> Carregando...</span>}
        </label>
        <select
          id="state"
          name="state"
          value={formData.state || ''}
          onChange={handleStateChange}
          disabled={!formData.country || loadingStates}
          required
        >
          <option value="">Selecione o estado...</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">
          Cidade *
          {loadingCities && <span className="loading"> Carregando...</span>}
        </label>
        <select
          id="city"
          name="city"
          value={formData.city || ''}
          onChange={handleCityChange}
          disabled={!formData.state || loadingCities}
          required
        >
          <option value="">Selecione a cidade...</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">Endereço *</label>
        <input
          type="text"
          id="street"
          name="street"
          value={formData.street || ''}
          onChange={handleInputChange}
          placeholder="Rua Principal, 123"
          required
        />
      </div>

      <div className="form-group">
        <label htmlFor="zipCode">CEP</label>
        <input
          type="text"
          id="zipCode"
          name="zipCode"
          value={formData.zipCode || ''}
          onChange={handleInputChange}
          placeholder="01310-100"
        />
      </div>

      <button
        type="submit"
        disabled={
          !formData.country ||
          !formData.state ||
          !formData.city ||
          !formData.street
        }
      >
        Enviar Endereço
      </button>
    </form>
  );
}

Exemplo de Uso

// App.tsx

import React from 'react';
import { AddressForm } from './components/AddressForm';

function App() {
  const handleAddressSubmit = (data: any) => {
    console.log('Endereço enviado:', data);

    // Enviar para seu backend
    // fetch('/api/save-address', {
    //   method: 'POST',
    //   headers: { 'Content-Type': 'application/json' },
    //   body: JSON.stringify(data),
    // });
  };

  return (
    <div className="app">
      <h1>Endereço de Entrega</h1>
      <AddressForm onSubmit={handleAddressSubmit} lang="pt" />
    </div>
  );
}

export default App;

Avançado: Hook useAddress Combinado

Para cenários mais complexos, crie um hook combinado:

// 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 = 'pt') {
  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,
  };
}

Próximos Passos

Precisa de Ajuda?


Dica Profissional: Use React Context para fornecer a chave de API globalmente em vez de importar da config em cada hook. Isso facilita os testes e melhora a flexibilidade.