← Blog

Cómo construir en React un formulario de dirección que funcione en todos los países

Un formulario de EE. UU. con un desplegable de país no es internacional. Campos por país, códigos postales, autocompletado, búsqueda y errores en React.

Preet Suthar · · 6 min de lectura

  • React
  • Forms
  • E-commerce
  • Accessibility

La mayoría de los formularios de dirección son un formulario de dirección de EE. UU. con un desplegable de país encima. Ciudad, Estado, Código ZIP, en ese orden, para todo el mundo. Entonces un cliente en Londres no tiene estado que elegir, uno en Berlín escribe el código postal antes que la ciudad por costumbre y uno en Tokio tiene que escribir su dirección al revés.

Un formulario de dirección es el formulario más largo que tienen la mayoría de las tiendas, y es donde se juntan el autocompletado, la validación y los usuarios internacionales. Así se construye uno en React que funcione en todos los países a los que envías.

Primero el país, después el diseño

El país decide qué campos existen, cómo se llaman, cuáles son obligatorios y en qué orden van. Así que pide primero el país, preseleccionado a partir del locale o la IP del cliente, y construye el resto a partir de datos.

Algunos ejemplos de cuánto cambia:

PaísCampos después de la calleRegiónCódigo postal
Estados UnidosCiudad, Estado, Código ZIPObligatorio12345 o 12345-6789
Reino UnidoLocalidad o ciudad, PostcodeNingunaComo SW1A 1AA
AlemaniaCódigo postal, CiudadNinguna5 dígitos
JapónCódigo postal, Prefectura, CiudadObligatorio123-4567
CanadáCiudad, Provincia, Código postalObligatorioComo K1A 0B1

No tienes que investigar cada país a mano. Google publica los metadatos de direcciones que usa su librería libaddressinput, con el orden de los campos, las etiquetas, los campos obligatorios y los patrones de código postal de casi todo el mundo. Elige los países a los que más envías y dales un formato exacto, y da a todos los demás una alternativa sencilla: dos líneas de dirección, ciudad, región y código postal, sin validar ninguno de forma estricta.

type Field = "address1" | "address2" | "city" | "region" | "postalCode"

type CountryFormat = {
  rows: Field[][]
  labels: Partial<Record<Field, string>>
  required: Field[]
  postalCode?: { pattern: RegExp; example: string }
  regions?: { code: string; name: string }[]
}

const countryFormats: Record<string, CountryFormat> = {
  US: {
    rows: [["address1"], ["address2"], ["city", "region", "postalCode"]],
    labels: { region: "State", postalCode: "ZIP code" },
    required: ["address1", "city", "region", "postalCode"],
    postalCode: { pattern: /^\d{5}(-\d{4})?$/, example: "94110" },
  },
  GB: {
    rows: [["address1"], ["address2"], ["city"], ["postalCode"]],
    labels: { city: "Town or city", postalCode: "Postcode" },
    required: ["address1", "city", "postalCode"],
    postalCode: {
      pattern: /^[A-Z]{1,2}\d[A-Z\d]? ?\d[A-Z]{2}$/i,
      example: "SW1A 1AA",
    },
  },
}

El componente del formulario renderiza entonces format.rows y nunca menciona un país por su nombre.

Menos campos

Cada campo que muestras es una cosa más que rellenar y una cosa más que hacer mal.

  • Un solo campo de nombre. "Nombre completo", no nombre y apellidos. Muchos nombres no se dividen en dos partes, y el orden cambia de un país a otro.
  • La línea 2, tras un enlace. "Añadir piso, oficina, etc." la abre. La mayoría de la gente no la necesita. Si el autocompletado o la búsqueda de direcciones la rellenan, muéstrala.
  • La empresa, también tras un enlace, para los pocos pedidos B2B.
  • Teléfono opcional, con el motivo. "Solo para dudas sobre la entrega" bajo el campo consigue más números reales que un campo obligatorio.

El autocompletado hace casi todo el trabajo

El autocompletado del navegador puede rellenar todo el formulario con un toque, pero solo si cada input tiene un name y el token de autocomplete correcto. Pon a los tokens el prefijo shipping o billing para que el navegador sepa qué dirección guardada va en cada sitio:

<Input name="name" autoComplete="shipping name" />
<Input name="address1" autoComplete="shipping address-line1" />
<Input name="address2" autoComplete="shipping address-line2" />
<Input name="city" autoComplete="shipping address-level2" />
<Input name="region" autoComplete="shipping address-level1" />
<Input
  name="postalCode"
  autoComplete="shipping postal-code"
  inputMode={country === "US" ? "numeric" : "text"}
/>
<Input name="phone" type="tel" autoComplete="shipping tel" />

address-level1 es el estado, la provincia o la prefectura, y address-level2 es la ciudad. Cuando la facturación es distinta del envío, renderiza un segundo formulario con tokens billing, para que el navegador no rellene el formulario de facturación con la dirección de envío.

No uses type="number" para los códigos postales. Los códigos ZIP empiezan por ceros, los del Reino Unido y Canadá tienen letras, y los inputs numéricos traen flechas y cambios con la rueda del ratón. Usa un input de texto con inputMode="numeric" solo donde los códigos sean únicamente dígitos.

Búsqueda por código postal

En algunos países el código postal ya te dice la ciudad y la región. Japón es el caso clásico: las direcciones empiezan por el código postal, y el código por sí solo identifica la prefectura y la ciudad. Cuando el cliente termine el código postal, búscalo y rellena la ciudad y la región, pero nunca sobrescribas algo que ya haya escrito. Lo que escribe manda sobre tus datos.

La búsqueda de direcciones, escribir el principio de una dirección y elegir una sugerencia, es más rápida en móvil que cualquier formulario. Google Places y Mapbox la ofrecen como dos llamadas: una que sugiere direcciones para lo que se ha escrito y otra que convierte la sugerencia elegida en campos estructurados.

Algunas reglas para conectarlo:

  • Espera a una pausa al escribir y aborta la petición anterior, igual que en cualquier buscador.
  • Termina siempre la lista con "Introducir la dirección manualmente". Puede que la calle sea nueva o que la sugerencia esté mal.
  • Tras elegir una, rellena los campos y muéstralos, para que el cliente pueda revisar el resultado y añadir un número de piso.
  • La Places API de Google usa tokens de sesión para agrupar las llamadas de sugerencias y de detalles de una misma búsqueda. Úsalos, o puede que las llamadas se facturen por separado.

Errores que dicen qué está mal

Valida un campo cuando el cliente sale de él y después en vivo mientras escribe, hasta que esté corregido. Escribe el error para el país: "Introduce un código ZIP de 5 dígitos" en EE. UU., "Introduce un código postal como SW1A 1AA" en el Reino Unido. "Código postal no válido" no ayuda a nadie.

Al enviar, enumera todos los problemas en un resumen arriba del formulario, enlaza cada uno a su campo y mueve el foco al resumen. Conecta cada error con su input mediante aria-describedby y pon aria-invalid, para que los lectores de pantalla lean el mensaje junto con el campo.

Normaliza antes de validar: quita espacios, pasa a mayúsculas los códigos postales del Reino Unido y Canadá y acéptalos con o sin espacio. Rechazar "sw1a1aa" cuando podrías arreglarlo es un bug tuyo, no del cliente.

Cambiar de país

La gente cambia de país tras escribir media dirección más a menudo de lo que crees. Cuando lo hagan, conserva lo que sigue encajando (nombre, líneas de la calle, teléfono), borra la región si el nuevo país tiene otra lista y vuelve a comprobar solo los campos que ya tocaron. Mostrar errores nuevos en campos a los que aún no han llegado se siente como una reprimenda por algo que no hicieron.

Guarda la estructura, no el texto

Guarda la dirección como campos más un código de país ISO, no como líneas formateadas. Necesitarás la estructura para los impuestos, las tarifas de envío y las etiquetas del transportista, y siempre puedes formatearla para mostrarla. Un resumen de una línea como "12 Mission St, San Francisco, CA 94110" es una vista de los datos, no los datos.

Una checklist

  • El país primero, y después los campos, las etiquetas y el orden a partir de un formato por país.
  • Un formato exacto para tus países principales y una alternativa permisiva para el resto.
  • Un solo campo de nombre, la línea 2 y la empresa tras enlaces, y el teléfono opcional con un motivo.
  • Un name y un token de autocomplete shipping o billing en cada input.
  • Inputs de texto para los códigos postales, teclados numéricos solo donde los códigos son dígitos.
  • Búsqueda por código postal que nunca sobrescribe lo escrito.
  • Búsqueda de direcciones con una opción de introducirla a mano al final.
  • Textos de error específicos por país y un resumen enlazado al enviar.

El bloque de HextaUI para esto

El bloque Address Form de HextaUI Pro está construido sobre estas reglas. Dieciséis países tienen su propio formato con etiquetas, campos obligatorios, listas de regiones y reglas postales, y cualquier otro país recibe una alternativa sencilla. Cada input tiene el token de autocomplete correcto con un prefijo shipping o billing. onSearchAddress y onResolveAddress conectan la búsqueda de direcciones, con un ejemplo para Google Places, y esas mismas dos llamadas se corresponden con Mapbox. onLookupPostalCode rellena la ciudad y la región sin sobrescribir nada de lo escrito, y validate() enumera todos los problemas en un resumen enlazado. SavedAddresses y AddressSummary cubren a los clientes con sesión iniciada y la fila final "Enviar a".

El bloque Checkout lo usa para el envío. Si estás construyendo el resto de esa página, también escribí sobre un checkout de una página en React y Next.js.

Preguntas frecuentes

¿El campo de país debe ir primero? Sí. Decide qué campos existen y en qué orden van, así que hay que elegirlo antes de que el resto tenga sentido.

¿Debo usar campos de nombre y apellidos? Usa un único campo de nombre completo, salvo que un transportista o un proveedor de pagos exija separarlo.

¿Qué tipo de input debe usar un código postal? Un input de texto, con inputMode="numeric" solo en los países cuyos códigos son todo dígitos.

¿Necesito validar direcciones con una API? Ayuda, pero no bloquees el pedido por ello. Sugiere una corrección y deja que el cliente conserve lo que escribió.