React component example
React Hook Form
Overview
If you're using React Hook Form, the cleanest way to integrate the React component is to wrap it in Controller. Bind field.value to the component's value prop, field.onChange to onChangeNumber, and pass field.onBlur through inputProps so RHF's blur tracking works as normal.
For validation, rather than mirroring validity into form state via callbacks, you can delegate to the underlying library directly inside RHF's validate rule by reading isValidNumber and getValidationError off the ref's getInstance(). This keeps the integration almost entirely declarative — there's no useState, no manual blur/submit gating, and the underlying library remains the single source of truth for validity.
For a hand-rolled equivalent without a form library, see the basic validation example.
Note: the red/green styling and warning/success icons are not part of the component — in this example they come from Bootstrap form validation.
Note: the toast shown when input is rejected isn't part of the library — see the onStrictReject callback docs for an example of how to wire one up yourself.
Demo
allowedNumberTypes option for more information.
JavaScript
initialCountryLookup here uses ipapi's free tier. For production, swap for a paid plan or alternative - see docs.getErrorMessage is up to you - see a worked example.import React, { useRef, useState } from "react";
import { useForm, Controller, useWatch } from "react-hook-form";
import IntlTelInput, { intlTelInput } from "@intl-tel-input/react";
import "intl-tel-input/styles";
const initialCountryLookup = async () => {
const res = await fetch("https://ipapi.co/json");
const data = await res.json();
return data.country_code;
};
const App = () => {
const itiRef = useRef(null);
const [validNumber, setValidNumber] = useState(null);
const { control, handleSubmit, formState: { errors } } = useForm({
mode: "onTouched",
defaultValues: { phone: "" },
});
const phoneValue = useWatch({ control, name: "phone" });
// RHF calls this with the current value; we delegate to the underlying library's
// validation by reading isValidNumber() / getValidationError() off the iti instance.
const validatePhone = (value) => {
if (!intlTelInput.utils) {
return true; // utils still loading; RHF will re-run validate on the next change
}
const iti = itiRef.current.getInstance();
if (iti.isValidNumber()) {
return true;
}
const errorCode = iti.getValidationError();
// your code here to map the errorCode to a user-facing message
return getErrorMessage(value, errorCode);
};
const onSubmit = (data) => {
// submit data.phone to your backend
setValidNumber(data.phone);
};
const showValidMsg = validNumber !== null && validNumber === phoneValue;
return (
<form onSubmit={handleSubmit(onSubmit)}>
<label htmlFor="phone">Phone number</label>
<Controller
name="phone"
control={control}
rules={{ validate: validatePhone }}
render={({ field, fieldState }) => (
<IntlTelInput
ref={itiRef}
value={field.value}
onChangeNumber={field.onChange}
initialCountryLookup={initialCountryLookup}
loadUtils={() => import("intl-tel-input/utils")}
inputProps={{
id: "phone",
name: field.name,
onBlur: field.onBlur,
className: fieldState.invalid ? "is-invalid" : "",
}}
/>
)}
/>
<button type="submit">Submit</button>
{errors.phone && <div className="invalid">{errors.phone.message}</div>}
{showValidMsg && <div className="valid">Full number: {validNumber}</div>}
</form>
);
};
export default App;