react-formularios-controlados

Formularios controlados en React

  • 5 min

El manejo de formularios es una de las tareas más comunes en el desarrollo web y, a la vez, donde más se diferencia React del HTML tradicional.

En HTML clásico, los elementos de formulario (como <input>, <textarea> o <select>) mantienen su propio estado interno. Cuando escribís en una caja de texto, el navegador guarda esa información en el DOM. Vosotros no tenéis que hacer nada.

En React, sin embargo, nuestra filosofía es que el Estado del componente debe ser la única fuente de la verdad.

No queremos que el DOM guarde datos a espaldas de React. Queremos que React sepa en todo momento qué hay escrito, para poder validarlo, enviarlo o transformarlo.

Para lograr esto, utilizamos el patrón de Componentes controlados.

¿Qué es un componente controlado?

Un componente controlado es un input de formulario cuyo valor es controlado por React a través del estado (useState).

El ciclo funciona así:

Estado ➡ Input: El atributo value del input se fija al valor del estado de React.

Usuario ➡ Evento: El usuario pulsa una tecla, disparando onChange.

Evento ➡ Estado: La función manejadora actualiza el estado con el nuevo valor.

Re-render: React pinta de nuevo el input con el nuevo estado.

Parece un ciclo largo para algo tan simple como escribir una letra, pero es lo que nos da un control absoluto sobre los datos.

Implementando un Input básico

Vamos a verlo mejor viendo en un ejemplo cómo transformar un input normal en uno controlado.

import { useState } from 'react';

export default function FormularioNombre() {
  // 1. Creamos el estado para guardar el valor
  const [nombre, setNombre] = useState('');

  const handleChange = (e) => {
    // 3. Actualizamos el estado con lo que escribe el usuario
    setNombre(e.target.value);
  };

  return (
    <form>
      <label>Tu nombre:</label>
      {/* 2. Vinculamos value al estado y onChange al manejador */}
      <input  
        type="text"   
        value={nombre}
        onChange={handleChange}
      />
      
      <p>Estás escribiendo: {nombre}</p>
    </form>
  );
}
Copied!

Si quitaseis la línea onChange={handleChange}, veríais que no podríais escribir nada.

Como value estaría atado a un estado vacío y nada actualiza ese estado, React forzaría al input a quedarse vacío siempre, ignorando vuestros tecleos.

¿Por qué tomarse tantas molestias?

Es cierto que escribir más código para un simple input parece tedioso. Pero tiene muchas ventajas, sobre todo en cuanto a reacción inmediata a los cambios.

  1. Validación inmediata: Podemos mostrar errores mientras el usuario escribe (ej: “La contraseña debe tener 8 caracteres”).
  2. Input masking: Podemos impedir ciertos caracteres. Por ejemplo, forzar mayúsculas o impedir letras en un campo numérico.
  3. Botones condicionales: Podemos deshabilitar el botón “Enviar” si el estado está vacío (disabled={nombre.length === 0}).

Por ejemplo, veamos como usarlo para hacer un campo que nos oblique a usar mayúsculas.

const handleChange = (e) => {
  // Transformamos el dato ANTES de guardarlo
  const valorMayusculas = e.target.value.toUpperCase();
  setNombre(valorMayusculas);
};
Copied!

Textarea y Select

En HTML, algunos input tienen sus propias formas de hacer las cosas. <textarea> define su valor como texto hijo, y <select> usa el atributo selected en sus opciones.

React estandariza esto para que todos los input funcionen igual, a través del atributo value.

En React, <textarea contiene el texto en el atributo value.

<textarea 
  value={mensaje} 
  onChange={e => setMensaje(e.target.value)} 
/>
Copied!

En lugar de poner selected en la <option>, ponemos value en el <select> padre.

const [fruta, setFruta] = useState('coco');

<select value={fruta} onChange={e => setFruta(e.target.value)}>
  <option value="lima">Lima</option>
  <option value="coco">Coco</option>
  <option value="mango">Mango</option>
</select>
Copied!

Checkboxes y Radio Buttons

Los checkboxes y Radio Buttons son los “bichos raros”.

  1. No usan value, usan checked.
  2. En el evento, leemos e.target.checked, no e.target.value.

Es un detalle menor, pero suele causar bugs si intentas aplicar la misma lógica genérica a todo sin mirar el type.

Gestionando múltiples inputs

Si tenemos un formulario con Nombre, Apellido, Email y Edad… ¿tenemos que crear 4 variables useState y 4 funciones handleChange distintas? No parece muy práctico.

Una forma habitual de manejar formularios complejos es usar un solo objeto de estado y una función genérica de update que use el atributo name del input para saber qué actualizar.

export default function FormularioRegistro() {
  // Un solo estado tipo objeto
  const [datos, setDatos] = useState({
    nombre: '',
    email: '',
    password: '',
    aceptarTerminos: false
  });

  const handleChange = (e) => {
    const { name, value, type, checked } = e.target;
    
    // Tratamiento especial para checkboxes
    const valorFinal = type === 'checkbox' ? checked : value;

    setDatos((prevDatos) => ({
      ...prevDatos, // Copiamos lo que había
      [name]: valorFinal // Computed Property Name: actualizamos solo la clave dinámica
    }));
  };

  return (
    <form>
      <input 
        name="nombre" 
        value={datos.nombre} 
        onChange={handleChange} 
        placeholder="Nombre"
      />
      
      <input 
        name="email" 
        value={datos.email} 
        onChange={handleChange} 
        placeholder="Email"
      />
      
      <label>
        Acepto términos
        <input 
          type="checkbox"
          name="aceptarTerminos"
          checked={datos.aceptarTerminos} // Ojo: Checkbox usa 'checked', no 'value'
          onChange={handleChange}
        />
      </label>
    </form>
  );
}
Copied!

Fijaos en la sintaxis [name]: valorFinal. Es una característica de ES6 llamada Computed Property Names que nos permite usar una variable como clave de un objeto.