Form inputs for phone numbers are notoriously annoying to handle. If your audience is international, your phone number input field must be adaptable to handle calling codes for different countries. For accuracy, best practice also requires adding masking, which automatically formats a phone number in the layout specific to the country it is calling from.
In this article, we’ll learn how we can achieve all of the above in React using a package called react-phone-number-input. We’ll also be utilizing the Navigator API in the browser and the Google Maps API.
To follow along with this tutorial, you can access the project’s source code on my GitHub repository. Let’s get started!
To get started, we’ll set up a new React project:
npx create-react-app
Next, we’ll install the required packages for our project, including react-phone-number-input. We’ll also install styled-components to add styling, however, this is optional:
npm install --save styled-components npm i react-phone-number-input
With React and our required packages installed, next, we’ll get a free API key from Google, allowing us to use the Google Maps API later on in the project. Head to the Getting Started page; Google provides a $200 monthly credit for the Google Maps API, so you’ll be able to follow this tutorial without having to spend anything.
Now, let’s define our form. We’ll create a basic form with an input field, a select button, and a few labels. Below is the code for our basic form prior to adding in styles:
<div> <form> <div> <label htmlFor="countrySelect">Country Select</label> <select name="countrySelect"/> </div> <div> <label htmlFor="phoneNumber">Phone Number</label> <input placeholder="Enter phone number" name="phoneNumber" /> </div> </form> </div>
Let’s use styled-components to create two components, StyledPage
for the container and StyledForm
for the form:
<StyledPage> <StyledForm> <div> <label htmlFor="countrySelect">Country Select</label> <select name="countrySelect"/> </div> <div> <label htmlFor="phoneNumber">Phone Number</label> <input placeholder="Enter phone number" name="phoneNumber" /> </div> <StyledForm> <StyledPage>
Now, we’ll add styles to both components, respectively:
const StyledPage = styled.div` display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 2rem; height: 100vh; background-color: hsl(15, 67%, 99%); & label, p { font-size: 20px; font-weight: 300; margin: 0; & > span { font-weight: 400; } } `; const StyledForm = styled.form` display: flex; flex-direction: column; gap: 2rem; & > div { display: flex; flex-direction: column; gap: 0.5rem; width: 400px; & > input, select { padding: 1rem; border-radius: 5px; border: none; box-shadow: 0px 0px 1px hsla(0, 0%, 12%, 0.5); } } `;
In the code block above, we added a few Flexbox containers to center the form on the page, and we included some visual styling elements. Now, let’s add in our custom inputs.
We’ll use two custom inputs from the react-phone-number-input package: Input
for the phone number and CountrySelect
, a custom select element that we’ll define using two functions from the package, getCountries
and getCountryCallingCode
.
Although the CountrySelect
component is documented in the package’s documentation, it’s not exported, meaning we need to define it ourselves. First, let’s import the elements and functions mentioned above:
import Input, { getCountries, getCountryCallingCode } from 'react-phone-number-input/input'; import en from 'react-phone-number-input/locale/en.json'; import 'react-phone-number-input/style.css';
In the code block above, we also imported a locale file that converts callingCodes
to country names in English. Additionally, we imported some CSS styling as required by the package. Now, we can define our CountrySelect
component as follows:
const CountrySelect = ({ value, onChange, labels, ...rest }) => ( <select {...rest} value={value} onChange={(event) => onChange(event.target.value || undefined)}> <option value="">{labels.ZZ}</option> {getCountries().map((country) => ( <option key={country} value={country}> {labels[country]} +{getCountryCallingCode(country)} </option> ))} </select> );
Next, we can switch out the form elements that we created earlier as placeholders with the new custom inputs from the package:
<StyledPage> <StyledForm> <div> <label htmlFor="countrySelect">Country Select</label> <CountrySelect labels={en} name="countrySelect" /> </div> <div> <label htmlFor="phoneNumber">Phone Number</label> <Input placeholder="Enter phone number" name="phoneNumber" /> </div> </StyledForm> </StyledPage>
You may notice that the labels
prop, which controls the labels displayed within it, is being passed to the CountrySelect
component. To generate the values for the dropdown, we pass the country calling codes we imported from the locale file earlier to the labels
prop.
Now, we nearly have our form set up for manual usage. Lastly, we need to add state to the component so that we can manipulate the values displayed in the select
and input
elements.
Let’s start by importing the useState
Hook into our component:
import React, { useState } from 'react';
Next, we’ll create two pieces of state within our component, one for the input
and one for the select
element:
const [phoneNumber, setPhoneNumber] = useState(); const [country, setCountry] = useState();
Finally, we’ll connect the state to the elements by passing the values to them as props:
<StyledPage> <StyledForm> <div> <label htmlFor="countrySelect">Country Select</label> <CountrySelect labels={en} value={country} onChange={setCountry} name="countrySelect" /> </div> <div> <label htmlFor="phoneNumber">Phone Number</label> <Input country={country} value={phoneNumber} onChange={setPhoneNumber} placeholder="Enter phone number" name="phoneNumber" /> </div> </StyledForm> </StyledPage>
The country
prop, which we pass to the Input
component, controls the formatting and masking of the phone number provided via a country calling code. We’ll use this later when we apply the user’s location.
Now, we have a fully-functional manual form. Let’s see how we can automatically detect the user’s latitude and longitude, then convert the coordinates to a country using the Google Maps Geocoding API.
To get a user’s longitude and latitude, we’ll use the browser’s Navigator API. More specifically, we’ll use the navigator.geolocation.getCurrentPosition
method, which will return data about the user’s current position from which we can extract the longitude and latitude data.
I’m going to make the detection run on the application’s initial load. Alternately, you could hook the detection run to a button press so the user isn’t prompted straight away on page load.
To ensure that we only run the request once per page load, we’ll use the useEffect
Hook. Let’s import that into our project:
import React, { useEffect, useState } from 'react';
Inside of our functional component, we can add in our useEffect
Hook and pass an empty array as the dependencies array. The useEffect
Hook will run only once on the application’s initial render:
useEffect(() => { // Insert our code here }, []);
Now, let’s add in the Navigator API to detect the user’s location:
useEffect(() => { navigator.geolocation.getCurrentPosition(success, rejected); }, []);
When the Navigator runs on the page’s initial render, the user will be prompted for permission to access their location. Based on their decision, one of two callback functions will be called, either for approval or rejection.
In this tutorial, we won’t focus on error handling. Instead, if permission is rejected, we’ll log it to the console. For the success callback, let’s define a separate function to call:
async function handleNavigator(pos) { const { latitude, longitude } = pos.coords; } useEffect(() => { navigator.geolocation.getCurrentPosition(handleNavigator, () => console.warn('permission was rejected')); }, []);
When the location request is approved, we’ll call the handleNavigator
function, which provides access to the user’s position data, allowing us to restructure the longitude and latitude data of their current position. Then, we’ll use the Geocoding API request and response to find the country.
To fetch the data from the API, let’s create a new function called lookupCountry.js
inside of a utils
directory within the src
directory. lookupCountry.js
will handle all of the data fetching and processing, returning a country code to handleNavigator
, our original callback function.
Inside of our new lookupCountry.js
file, let’s define a new async function with the same name, then export it:
async function lookupCountry({ latitude, longitude }) { } export default lookupCountry;
We’ll also define our arguments, which will include an object containing the latitude and longitude that we’ll pass in when we call the function.
Next, we need to define the URL that will be performing the fetch request. When defining the URL, we also need to add in our longitude and latitude data along with the API key we got earlier.
The easiest way to do so is using JavaScript’s template literals to interpolate the variables into the string:
async function lookupCountry({ latitude, longitude }) { const URL = `https://maps.googleapis.com/maps/api/geocode/json?latlng=${latitude},${longitude}&key=${process.env.REACT_APP_GOOGLE_MAPS_API_KEY}`; } export default lookupCountry;
Note: I have added my API key via an environmental variable, which is recommended. However, if you are using the project locally and don’t plan to publish it, you can hard code your API key in.
Now, we’ll fetch the data from the API using the browser’s built-in fetch API:
async function lookupCountry({ latitude, longitude }) { const URL = `https://maps.googleapis.com/maps/api/geocode/json?latlng=${latitude},${longitude}&key=${process.env.REACT_APP_GOOGLE_MAPS_API_KEY}`; const locationData = await fetch(URL).then((res) => res.json()); } export default lookupCountry;
We wait for the data to be returned from the API, then convert the response into JSON before storing it in a variable.
The API will return various levels of detail from the longitude and latitude data we provided, including everything from a street address to the country of the user, which is what we’re interested in.
Luckily for us, Google has formatted and categorized the returned array for us. The first item in the array is the most precise (street address) and the last is the most general (country).
Each item also includes the type of data, like street address, town, country, etc., making it easy for us to find the item we’re interested in by filtering it down to the type country
:
async function lookupCountry({ latitude, longitude }) { const URL = `https://maps.googleapis.com/maps/api/geocode/json?latlng=${latitude},${longitude}&key=${process.env.REACT_APP_GOOGLE_MAPS_API_KEY}`; const locationData = await fetch(URL).then((res) => res.json()); const [{ address_components }] = locationData.results.filter(({ types }) => types.includes('country')); } export default lookupCountry;
After filtering the array down, we’ll perform an array destructure because the .filter
method returns an array to us. Next, we’ll do an object destructure so we can access the property we are interested in, address_components
. Below is a sample of the data inside address_components
:
[ { "long_name": "United Kingdom", "short_name": "GB", "types": [ "country", "political" ] } ]
We are interested in obtaining the country code for the user, so we need to destructure out the short_name
variable from the object:
async function lookupCountry({ latitude, longitude }) { const URL = `https://maps.googleapis.com/maps/api/geocode/json?latlng=${latitude},${longitude}&key=${process.env.REACT_APP_GOOGLE_MAPS_API_KEY}`; const locationData = await fetch(URL).then((res) => res.json()); const [{ address_components }] = locationData.results.filter(({ types }) => types.includes('country')); const [{ short_name}] = address_components; } export default lookupCountry;
Once we have destructured out the country code from within the object, we can return it from our function to the original callback function handleNavigator
from earlier:
async function lookupCountry({ latitude, longitude }) { const URL = `https://maps.googleapis.com/maps/api/geocode/json?latlng=${latitude},${longitude}&key=${process.env.REACT_APP_GOOGLE_MAPS_API_KEY}`; const locationData = await fetch(URL).then((res) => res.json()); const [{ address_components }] = locationData.results.filter(({ types }) => types.includes('country')); const [{ short_name}] = address_components; return short_name; } export default lookupCountry;
Let’s add our lookupCountry
function into our handleNavigator
callback function from earlier and connect it to the rest of our form:
async function handleNavigator(pos) { const { latitude, longitude } = pos.coords; const userCountryCode = await lookupCountry({ latitude, longitude }); }
Now, if the user grants permission on the application’s initial page load, we get our latitude and longitude data from the Navigator API. The data is then passed into our lookupCountry
function, which in turn uses Google’s Geocoding API to convert the latitude and longitude into a country code, which is then returned back to us.
Finally, we just need to override the state for country
, which controls the value displayed within the select
element and the formatting of the input component, where the user will type their phone number:
async function handleNavigator(pos) { const { latitude, longitude } = pos.coords; const userCountryCode = await lookupCountry({ latitude, longitude }); setCountry(userCountryCode); }
Now, everything is connected. When the user loads the page, they will be prompted to give permission for the app to access their location. If they grant permission, the app will detect their location, look up the correct country code, then apply it to the form inputs on the display.
I had the idea for this topic when I tried to make a phone number input on a form that could be used globally. I quickly realized how difficult it is to handle all of the requirements for phone numbers across the globe, especially when still trying to provide a good UX.
react-phone-number-input greatly simplifies the process of collecting global phone numbers in your form. I hope you found this tutorial helpful; if you did, please check out my Twitter.
Install LogRocket via npm or script tag. LogRocket.init()
must be called client-side, not
server-side
$ npm i --save logrocket // Code: import LogRocket from 'logrocket'; LogRocket.init('app/id');
// Add to your HTML: <script src="https://cdn.lr-ingest.com/LogRocket.min.js"></script> <script>window.LogRocket && window.LogRocket.init('app/id');</script>
Would you be interested in joining LogRocket's developer community?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowBuild scalable admin dashboards with Filament and Laravel using Form Builder, Notifications, and Actions for clean, interactive panels.
Break down the parts of a URL and explore APIs for working with them in JavaScript, parsing them, building query strings, checking their validity, etc.
In this guide, explore lazy loading and error loading as two techniques for fetching data in React apps.
Deno is a popular JavaScript runtime, and it recently launched version 2.0 with several new features, bug fixes, and improvements […]
3 Replies to "react-phone-number-input: Detecting international location"
Isnt google map api not-free?
how do you want to show wrong number error to the user ?
Man you save my ass… Thank you so much… I really appreciate your work.