Coner Murphy Frontend web developer building a freelance business. YouTuber and livestreamer. I share actionable tips, thoughts, and lessons on my Twitter and blog.

react-phone-number-input: Detecting international location

8 min read 2348

React Phone Number

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!

React Phone Number Input Field Example
react-phone-number-input field example

 

Getting 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.

Creating and styling our form

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:

&lt;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:

We made a custom demo for .
No really. Click here to check it out.

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.

Adding custom form 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.

Getting the user’s longitude and latitude

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.

Geocoding latitude and longitude to a 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;

Putting everything together

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.

Conclusion

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.

 

Full visibility into production React apps

Debugging React applications can be difficult, especially when users experience issues that are hard to reproduce. If you’re interested in monitoring and tracking Redux state, automatically surfacing JavaScript errors, and tracking slow network requests and component load time, try LogRocket.

LogRocket is like a DVR for web apps, recording literally everything that happens on your React app. Instead of guessing why problems happen, you can aggregate and report on what state your application was in when an issue occurred. LogRocket also monitors your app's performance, reporting with metrics like client CPU load, client memory usage, and more.

The LogRocket Redux middleware package adds an extra layer of visibility into your user sessions. LogRocket logs all actions and state from your Redux stores.

Modernize how you debug your React apps — .

Coner Murphy Frontend web developer building a freelance business. YouTuber and livestreamer. I share actionable tips, thoughts, and lessons on my Twitter and blog.

Leave a Reply