ReactTutorialPhone Codes

How to Build a Phone + Flag Selector in React (with Country Codes)

ApogeoAPI7 min read

Phone number inputs with country codes are one of those deceptively tricky UI components. Here's how to build a polished one with flag images and dial codes using ApogeoAPI.

What We're Building

A phone number input that:

  • Shows a flag + country dial code in a dropdown
  • Lets the user search countries by name
  • Combines the dial code with the number the user types
  • Is fully TypeScript-typed

Step 1: Fetch Countries with Phone Codes

The ApogeoAPI countries endpoint returns phone_code and flag_url for every country.

// types.ts
export interface Country {
  iso2: string;
  name: string;
  phone_code: string;
  flag_url: string;
}

// useCountries.ts
import { useEffect, useState } from 'react';

export function useCountries() {
  const [countries, setCountries] = useState<Country[]>([]);

  useEffect(() => {
    fetch('https://api.apogeoapi.com/v1/countries', {
      headers: { 'X-API-Key': process.env.NEXT_PUBLIC_APOGEO_KEY! },
    })
      .then(r => r.json())
      .then(setCountries);
  }, []);

  return countries;
}

Step 2: Build the PhoneInput Component

'use client';
import { useState, useMemo } from 'react';
import { useCountries } from './useCountries';

interface Props {
  value: string;
  onChange: (fullNumber: string) => void;
}

export function PhoneInput({ value, onChange }: Props) {
  const countries = useCountries();
  const [selectedIso, setSelectedIso] = useState('US');
  const [number, setNumber] = useState('');
  const [search, setSearch] = useState('');
  const [open, setOpen] = useState(false);

  const selected = countries.find(c => c.iso2 === selectedIso);

  const filtered = useMemo(() =>
    countries.filter(c =>
      c.name.toLowerCase().includes(search.toLowerCase())
    ), [countries, search]);

  const handleSelect = (country: Country) => {
    setSelectedIso(country.iso2);
    setOpen(false);
    setSearch('');
    onChange(`+${country.phone_code}${number}`);
  };

  const handleNumber = (e: React.ChangeEvent<HTMLInputElement>) => {
    setNumber(e.target.value);
    onChange(`+${selected?.phone_code ?? ''}${e.target.value}`);
  };

  return (
    <div className="flex gap-2 relative">
      {/* Dial code selector */}
      <button type="button" onClick={() => setOpen(!open)}
        className="flex items-center gap-1 border rounded px-3 py-2 min-w-[90px]">
        {selected && (
          <>
            <img src={selected.flag_url} alt="" className="w-5 h-4 object-cover rounded-sm" />
            <span className="text-sm">+{selected.phone_code}</span>
          </>
        )}
      </button>

      {/* Dropdown */}
      {open && (
        <div className="absolute top-full left-0 z-20 w-64 bg-white border rounded shadow-lg">
          <input autoFocus placeholder="Search..." value={search}
            onChange={e => setSearch(e.target.value)}
            className="w-full px-3 py-2 border-b text-sm outline-none" />
          <ul className="max-h-52 overflow-y-auto">
            {filtered.map(c => (
              <li key={c.iso2} onClick={() => handleSelect(c)}
                className="flex items-center gap-2 px-3 py-2 hover:bg-gray-50 cursor-pointer text-sm">
                <img src={c.flag_url} alt="" className="w-5 h-4 object-cover rounded-sm" />
                <span>{c.name}</span>
                <span className="ml-auto text-gray-400">+{c.phone_code}</span>
              </li>
            ))}
          </ul>
        </div>
      )}

      {/* Number input */}
      <input type="tel" value={number} onChange={handleNumber}
        placeholder="Phone number"
        className="flex-1 border rounded px-3 py-2" />
    </div>
  );
}

Step 3: Handle the Combined Value

The onChange callback fires with the full international number: +1 4155552671. Store it in your form state and send it as-is to your backend.

Optimizations

  • Cache the country list: Countries change rarely. Use SWR with a 24-hour stale time or cache in localStorage.
  • Default to user's country: Combine with the IP geolocation endpoint — GET /v1/geolocate/auto — to pre-select the right dial code automatically.
  • Memoize filtered list: The useMemo above ensures filtering is fast even with 250 countries.

Try ApogeoAPI free

1,000 requests/month forever. 14-day full-access trial. No credit card.

Get your free API key