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
useMemoabove 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