Un componente controlado es un elemento de formulario cuyo valor vive en el estado de React: pasas value y actualizas el estado en onChange, así que React decide qué muestra el input. Un componente no controlado guarda su valor en el DOM, como en HTML puro: pasas un defaultValue inicial y lees el valor actual después mediante una ref o FormData.
Escribe en ambos inputs. El párrafo sigue al primero en cada pulsación, porque cada cambio actualiza el estado y vuelve a renderizar. El segundo input cambia solo en el DOM; React no renderiza, y solo ves su valor cuando "Read both" lo pide.
Inputs controlados
Con un input controlado, el estado de React es la única fuente de verdad. El flujo en cada pulsación es:
- El usuario escribe y el navegador dispara un evento de input.
- Tu manejador
onChangeleee.target.valuey llama al setter. - React vuelve a renderizar y escribe el nuevo estado en el
valuedel input.
Como tu código está en medio de ese ciclo, puedes hacer cosas que un input no controlado no puede: rechazar o transformar caracteres, mostrar el valor en otro lugar, deshabilitar un botón hasta que el valor sea válido, o definir el valor desde el código (un botón "Clear", una sugerencia en la que el usuario hace clic).
Prueba a escribir letras: nunca aparecen, porque el manejador quita todo lo que no sea un dígito antes de actualizar el estado. El botón Verify sigue deshabilitado hasta que haya exactamente seis dígitos.
Controlado es lo que usan los ejemplos de la página de formularios, y es la opción por defecto en la mayor parte del código de React.
Inputs no controlados
Un input no controlado funciona como en HTML puro. React lo renderiza una vez con defaultValue (o defaultChecked para casillas y radios) y luego lo deja en paz. Para leer el valor, guarda una ref al elemento o lee todos los campos con nombre del formulario al enviar con FormData.
No hay estado para los campos ni onChange. Los valores se recogen solo cuando el formulario se envía. Desmarca la casilla y envía otra vez: la clave subscribe desaparece, porque una casilla sin marcar no forma parte de FormData en absoluto.
Cambiar defaultValue después del primer renderizado no le hace nada al input, porque React solo lo aplica cuando se crea el elemento. Para reiniciar un formulario no controlado, llama a e.target.reset(), o dale al formulario una key nueva para que React cree inputs nuevos.
Un input en el que no puedes escribir
Si pasas value pero no onChange, el input es controlado, pero nada actualiza nunca el estado. React vuelve a escribir el mismo valor después de cada pulsación, así que el input parece congelado.
Prueba a escribir en cada campo. El primero no cambia. El segundo es la solución para un input controlado, y mientras escribes ahí, el primer y el último campo lo siguen, porque los tres leen el mismo estado. El tercero acepta lo que escribes porque es no controlado. El cuarto dice a propósito que no se puede editar.
En desarrollo, React registra un aviso para el primer input: "You provided a value prop to a form field without an onChange handler". La vista previa funciona como un build de producción, así que aquí no verás ese aviso, pero sí en tu propio servidor de desarrollo.
Pasar de no controlado a controlado
React decide si un input es controlado mirando value. Si value es undefined o null, el input es no controlado. Así que un input cuyo value empieza como undefined y luego pasa a ser una cadena cambia de no controlado a controlado durante su vida, y React avisa en desarrollo: "A component is changing an uncontrolled input to be controlled".
El aviso señala un error real. Volver en la otra dirección, de una cadena a undefined, hace que el input vuelva a ser no controlado, y React deja de escribir en él.
Escribe algo en ambos inputs y pulsa cada botón Clear. El segundo input se vacía. El primero conserva su texto: poner el estado en undefined lo volvió no controlado, así que React ya no controla lo que muestra.
La fuente habitual de undefined son datos que aún no han cargado (value={user.name} antes de que llegue el usuario) o un campo de objeto que todavía no existe. Dos soluciones:
const [name, setName] = useState(''); // start with a string
<input value={user?.name ?? ''} /> // or fall back to one
Para las casillas la misma regla se aplica a checked: empiézalo en false, no en undefined.
Los inputs de archivo siempre son no controlados
Un <input type="file"> no puede ser controlado, porque los navegadores no permiten que una página defina su valor (una página que pudiera elegir archivos de tu disco sería un agujero de seguridad). Lee los archivos elegidos desde e.target.files en onChange, o mediante una ref cuando se envía el formulario.
Elige uno o dos archivos de tu computadora; no se sube nada, el ejemplo solo lista nombres y tamaños. El único valor que puedes asignar a un input de archivo es la cadena vacía, que es como el botón Clear lo reinicia.
Cuándo elegir cuál
| Controlado | No controlado | |
|---|---|---|
| El valor vive en | El estado de React | El DOM |
| Definir el valor inicial | value / checked | defaultValue / defaultChecked |
| Leer el valor | En cualquier momento, desde el estado | Con una ref, o FormData al enviar |
| Renderiza en cada pulsación | Sí | No |
| Cambiar el valor desde el código | Actualizar el estado | Escribir en el DOM mediante una ref |
| Útil para | Validación en vivo, formato, campos dependientes | Formularios simples leídos al enviar, inputs de archivo, acciones de formulario |
Elige controlado cuando la pantalla tiene que reaccionar al valor mientras el usuario escribe. Elige no controlado cuando solo necesitas los valores al final, que es también como funcionan las acciones de formulario de React 19: <form action={fn}> le entrega a tu función un FormData y después reinicia los campos no controlados.
Renderizar en cada pulsación suena costoso, pero en un formulario normal no es un problema. Solo lo es cuando un árbol grande y lento se vuelve a renderizar con el input; en ese caso, mantén el estado cerca del input o mueve la parte lenta a su propio componente.
Las mismas dos palabras describen también tus propios componentes. Un componente que recibe su valor y un onChange de su padre mediante props está controlado por el padre; uno que guarda su propio estado es no controlado. Mover el estado hacia arriba para hacer un componente controlado es de lo que trata elevar el estado.
Preguntas frecuentes
¿Qué es un componente controlado en React?
Un elemento de formulario cuyo valor actual viene del estado de React, definido con value (o checked) y actualizado mediante onChange. El estado de React es la única fuente de verdad, así que siempre conoces el valor y puedes cambiarlo desde el código.
¿Qué es un componente no controlado en React?
Un elemento de formulario que guarda su propio valor en el DOM, como en HTML puro. Le das un valor inicial con defaultValue (o defaultChecked) y lees el valor actual cuando lo necesitas, mediante una ref o FormData al enviar.
¿Por qué no puedo escribir en mi input de React?
Pasaste value sin un onChange. El input es controlado, así que React vuelve a poner el valor del estado después de cada pulsación. Agrega un onChange que actualice el estado, cambia a defaultValue, o agrega readOnly si de verdad no debe cambiar.
¿Cómo arreglo "A component is changing an uncontrolled input to be controlled"?
El value del input empezó como undefined (o null) y luego pasó a ser una cadena. Asegúrate de que siempre sea una cadena: inicia el estado en '', o escribe value={name ?? ''}.
¿Debo usar inputs controlados o no controlados?
Usa inputs controlados cuando la UI reacciona al valor mientras el usuario escribe: validación, formato, un botón deshabilitado, campos que dependen entre sí. Usa inputs no controlados cuando solo necesitas los valores al enviar, y para los inputs de archivo, que siempre son no controlados.