Hasta ahora, nuestros componentes han sido simples presentadores. Reciben datos (Props) y los pintan. Si el padre cambia las props, el componente se actualizaba.
Pero, ¿qué pasa si el cambio nace dentro del componente? ¿Qué pasa si quiero escribir en un input, desplegar un menú o incrementar un contador al hacer click?
Aquí es donde entra el concepto de Estado interno del componente
El estado es la memoria interna del componente. Son los datos que pertenecen al componente, que cambian con el tiempo y que, cuando cambian, obligan a la interfaz a redibujarse.
Para gestionar esto en componentes funcionales, utilizamos nuestro primer Hook: useState.
Los Hooks son funciones especiales que permiten a tu componente funcional (efímero) “engancharse” a funcionalidades de React (persistentes, como el estado o el ciclo de vida).
Por convención, todos los Hooks comienzan con el prefijo use (ej: useState, useEffect, useContext).
El problema de las variables locales
Para entender por qué necesitamos useState, primero vamos a “equivocarnos” intentándolo “a la vieja usanza”, con variables normales de JavaScript.
Mirad este código, parece lógico, ¿verdad?
export default function ContadorRoto() {
let cuenta = 0; // Variable local
const incrementar = () => {
cuenta = cuenta + 1;
console.log(cuenta); // En consola veremos 1, 2, 3...
};
return (
<div>
<h1>{cuenta}</h1>
<button onClick={incrementar}>+1</button>
</div>
);
}
Si ejecutáis esto, veréis que
- ✔️El
console.logmuestra que la variable sube - ❌ Pero el número en la pantalla (el
<h1>) se queda clavado en 0
Por dos razones fundamentales de la arquitectura de React:
- Falta de Reactividad: React no tiene forma de saber que
cuentaha cambiado. Modificar una variable local no avisa a React para que dispare un re-renderizado. - Alcance de la variable: Incluso si forzáramos el renderizado, al volverse a ejecutar la función
ContadorRoto(), la línealet cuenta = 0se ejecutaría de nuevo, reiniciando todo.
La solución: useState
Para solucionar ambos problemas, tenemos el Hook useState. Es una función que nos proporciona una variable que:
- Persiste entre renderizados (React se encarga de “recordarla”)
- Dispara una actualización de la interfaz cuando se modifica
Para usarlo, primero lo importamos de react,
import { useState } from 'react';
Y lo utilizamos así:
const [estado, setEstado] = useState(valorInicial);
Lo que ocurre aquí es Destructuring de Arrays. La función useState devuelve siempre un array con exactamente dos elementos:
- El valor actual del estado (inicialmente valorInicial).
- Una función setter para actualizar ese valor.
El contador que sí funciona
Vamos a arreglar el ejemplo anterior:
import { useState } from 'react';
export default function Contador() {
// Declaramos una variable de estado llamada "cuenta"
const [cuenta, setCuenta] = useState(0);
const incrementar = () => {
// Usamos el setter, NO modificamos la variable directamente
setCuenta(cuenta + 1);
};
return (
<div>
<h1>{cuenta}</h1>
<button onClick={incrementar}>+1</button>
</div>
);
}
Ahora, al llamar a setCuenta(1):
React actualiza su memoria interna: “cuenta ahora vale 1”.
React detecta el cambio y vuelve a ejecutar (re-renderiza) la función Contador.
En esta nueva ejecución, useState(0) ya no devuelve 0, sino 1 (el valor recordado).
El JSX usa este valor, y devuelve el HTML <h1>1</h1>.
React actualiza el DOM.
Reglas de uso
- Nunca modifiquéis el estado directamente:
cuenta = 5no hará nada. Debéis usarsetCuenta(5). - Los Hooks solo en el nivel superior: Nunca llaméis a
useStatedentro de bucles, condiciones (if) o funciones anidadas. React depende del orden de llamada de los hooks para saber qué estado corresponde a qué variable.
No todo necesita estar en el estado. Preguntaos:
- ¿Este dato se pasa por props desde el padre? ➡ No es estado.
- ¿Se mantiene igual con el tiempo? ➡ No es estado.
- ¿Se puede calcular a partir de otros estados o props? ➡ No es estado (es una variable derivada).
Solo usad useState para datos que cambian con el tiempo y que necesitáis recordar entre renderizados para pintar la interfaz.
Actualizaciones funcionales
Las actualizaciones de estado en React pueden ser asíncronas. Si intentáis actualizar el estado basándoos en el valor anterior, hacer esto puede ser peligroso en escenarios rápidos:
// Si pulsamos muy rápido o dentro de bucles
setCuenta(cuenta + 1);
setCuenta(cuenta + 1);
setCuenta(cuenta + 1);
Es posible que, debido al cierre (closure) de JavaScript, en las tres líneas cuenta valga lo mismo (ej: 0), y el resultado final sea 1 en vez de 3.
Para evitar esto, cuando el nuevo estado depende del anterior, debemos pasar una función callback al setter:
// Forma correcta y segura (Functional Update)
setCuenta((valorAnterior) => valorAnterior + 1);
Aquí React nos garantiza que valorAnterior es el valor más reciente y real que tiene en memoria, justo antes de aplicar el cambio.
Estado con Objetos
El hook useState no mezcla (merge) automáticamente objetos. Es decir, si tenéis un objeto complejo:
const [usuario, setUsuario] = useState({
nombre: 'Luis',
edad: 30,
email: '[email protected]'
});
Y queréis cambiar solo el email, debéis copiar manualmente el resto de propiedades, o las perderéis.
// ❌ MAL: Esto borra nombre y edad
setUsuario({ email: '[email protected]' });
// ✅ BIEN: Usamos el Spread Operator para copiar lo anterior
setUsuario({
...usuario, // Copia nombre y edad
email: '[email protected]' // Sobrescribe email
});
Si el estado tiene muchas propiedades no relacionadas, suele ser mejor dividirlas en múltiples useState independientes (const [nombre, setNombre], const [edad, setEdad]) en lugar de tener un objeto gigante.
