React component example

React Hook Form

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 the plugin's validity into form state via callbacks, you can delegate to the plugin 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 plugin 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.

Demo

JavaScript

geoIpLookup here uses ipapi's limited free tier — for production, pick a paid plan, another provider, or roll your own.
yourCodeToDeriveErrorMessage is up to you — see Deriving a user-facing error message for 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 geoIpLookup = (success, failure) => {
  fetch("https://ipapi.co/json")
    .then(res => res.json())
    .then(data => success(data.country_code))
    .catch(() => failure());
};

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 plugin's own
  // 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();
    return yourCodeToDeriveErrorMessage(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}
            initialCountry="auto"
            separateDialCode
            strictMode
            geoIpLookup={geoIpLookup}
            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;