- useState y useEffect concentran la mayor parte del estado y los efectos secundarios de los componentes funcionales de React, sustituyendo a muchos métodos de ciclo de vida clásicos.
- Un efecto puede incluir lógica de limpieza devolviendo una función, lo que permite gestionar suscripciones, intervalos y otros recursos externos sin fugas de memoria.
- El array de dependencias controla cuándo se ejecuta cada efecto y debe contener todos los valores reactivos usados en él, equilibrando corrección y rendimiento.
- Separar la lógica en varios useEffect pequeños por responsabilidad mejora la legibilidad, facilita la reutilización y reduce errores habituales como renders infinitos o dependencias incorrectas.

Si trabajas con React a diario, dominar useState y useEffect no es opcional, es prácticamente el pan de cada día. Estos dos Hooks concentran la mayoría de la lógica de estado y de efectos secundarios de cualquier aplicación moderna, desde un simple contador hasta interfaces complejas con peticiones HTTP, suscripciones a eventos y animaciones.
El problema es que es muy fácil usarlos «a ojo» y acabar con fugas de memoria, renders infinitos o efectos que se disparan cuando no toca. En este artículo vamos a desgranar con calma cómo usar correctamente useState y, sobre todo, useEffect: qué hace exactamente, cómo se relaciona con el ciclo de vida de los componentes, cómo gestionar limpiezas, cómo optimizar rendimiento con el array de dependencias y cómo aplicar patrones avanzados sin romper la cabeza.
Qué son los Hooks y por qué useState y useEffect son tan importantes
Los Hooks aparecieron en React 16.8 para permitir usar estado y otras capacidades sin necesidad de clases. En lugar de extender de React.Component y lidiar con métodos de ciclo de vida, trabajamos con funciones que llaman a Hooks como useState y useEffect en su interior.
useState es el Hook que te da estado local dentro de un componente funcional: devuelve un valor y una función para actualizarlo, y cada vez que actualizas ese estado provocas un nuevo renderizado del componente. Es la base para que tu interfaz sea dinámica: contadores, formularios, flags de carga, etc.
useEffect, por su parte, es el Hook que te permite ejecutar efectos secundarios tras el renderizado. Aquí entran cosas como peticiones de datos, suscripciones a websockets, listeners del DOM, temporizadores, integración con librerías externas o actualizaciones manuales del DOM cuando no queda otra.
Si vienes de componentes de clase, puedes ver useEffect como la combinación de componentDidMount, componentDidUpdate y componentWillUnmount, pero empaquetados en una sola API declarativa. La idea es pensar en «efectos que ocurren después del render» en lugar de en fases de «montaje» o «actualización» por separado.
Cómo funciona useEffect a nivel conceptual
La firma básica de useEffect es muy sencilla: useEffect(configuración, dependencias?). Le pasas una función de configuración que se ejecutará después de pintar el componente y, opcionalmente, una lista de dependencias que determina cuándo debe repetirse ese efecto.
La función de configuración concentra la lógica de tu efecto: suscribirte a algo, lanzar una petición, iniciar un intervalo, tocar el DOM, etc. Esa función puede, además, devolver otra función de limpieza que React ejecutará cuando toque deshacer lo que has hecho: cancelar un intervalo, desuscribirte, abortar una petición, etc.
El segundo argumento de useEffect es el famoso array de dependencias. Ahí tienes que listar todos los valores «reactivos» que usas dentro del efecto: props, estado y cualquier variable o función definida directamente dentro del cuerpo del componente. React compara ese array con el del render anterior usando Object.is; si alguna dependencia cambia, limpia el efecto anterior y ejecuta el nuevo.
Si no pasas array de dependencias, useEffect se ejecuta tras cada renderizado. Si pasas un array vacío [], el efecto sólo se ejecuta una vez al montar y se limpia al desmontar, emulando el par componentDidMount / componentWillUnmount. Y si pasas dependencias concretas, el efecto se dispara únicamente cuando alguno de esos valores cambia.
Primeros pasos: combinando useState y useEffect
Un ejemplo clásico para entender la relación entre ambos Hooks es el típico contador que actualiza el título del documento. Con useState manejas el número de clicks, y con useEffect cambias document.title después de cada renderizado:
La clave está en que useEffect se define dentro del componente, por lo que puede leer directamente el estado y las props actuales gracias a los closures de JavaScript. No necesitas APIS especiales de React para acceder al estado dentro del efecto; todo está en el ámbito de la función.
Además, React no está reutilizando la misma función de efecto entre renders: en cada render generas una función nueva. Eso es intencionado: cada efecto queda asociado al render en el que se creó, y cuando React detecta cambios en las dependencias, sustituye un efecto por el otro limpiando antes el anterior. Este modelo hace más fácil razonar sobre qué valores ve cada efecto en cada momento.
Otro detalle importante es que los efectos no bloquean el pintado de la interfaz. React primero actualiza el DOM y deja que el navegador pinte, y después ejecuta useEffect. Para la mayoría de casos (peticiones, logs, etc.) esto mejora la sensación de fluidez. Si alguna vez necesitas hacer algo sincronamente justo antes de que el usuario vea la pantalla (por ejemplo, medir y recolocar un tooltip), entonces sí deberías recurrir a useLayoutEffect, que comparte API pero se ejecuta antes del pintado.
Tipos de efectos: con y sin limpieza
No todos los efectos secundarios son iguales: hay efectos que se ejecutan y listo, y otros que necesitan limpieza explícita. Diferenciar ambos tipos es clave para no dejar cosas colgando en memoria o comportamientos extraños.
Los efectos sin limpieza son aquellos en los que no dejas nada «abierto»: por ejemplo, escribir en el log, hacer una petición puntual y guardar el resultado en el estado, lanzar una animación que se gestiona sola o hacer una modificación puntual del DOM. En términos de ciclo de vida, sólo dependen de componentDidMount / componentDidUpdate y no requieren nada en componentWillUnmount.
Un caso típico sería usar navigator.geolocation.getCurrentPosition una vez que el componente se ha renderizado. Pides la localización, actualizas el estado con la latitud y longitud, React vuelve a renderizar, y listo. No dejas un listener permanente, así que no necesitas limpiar nada al desmontar el componente.
Los efectos con limpieza aparecen cuando tu componente se conecta a un sistema externo de forma persistente: suscripciones a websockets o a un API de chat, event listeners del DOM, intervalos o timeouts repetitivos, etc. En estos casos, si no limpias al desmontar o al cambiar de dependencias, acabarás con fugas de memoria o comportamiento inesperado.
La forma idiomática de React para manejar esa limpieza es devolver una función desde el efecto. Esa función se ejecutará cada vez que el efecto se tenga que «desmontar»: cuando el componente se quite del DOM o cuando las dependencias cambien y haya que reemplazar el efecto antiguo por el nuevo. Un ejemplo típico es un contador con setInterval que se limpia con clearInterval en el return del efecto.
useEffect aplicado a eventos globales y suscripciones
Un uso muy habitual de useEffect es suscribirse a eventos globales del navegador, como resize del window o keydown del document. En estos casos necesitas engancharte al evento al montar el componente y desengancharte al desmontar para no acumular listeners.
El patrón es siempre el mismo: dentro del efecto añades los listeners y en la función de limpieza los eliminas. Además, si quieres que esa suscripción sólo se cree una vez, pasas un array de dependencias vacío [] para que no se repita en cada renderizado.
Este mismo enfoque se aplica a integraciones con APIs externas, SDKs de terceros o cualquier sistema que te obligue a «conectarte» mientras el componente está visible. Te conectas en la función de configuración del efecto y te desconectas en la función de limpieza, confiando en que React llamará a ambas cada vez que sea necesario según cambien las dependencias.
En modo desarrollo y con StrictMode, React hace una pasada extra de configuración + limpieza justo después de montar. Es una especie de prueba de estrés para asegurarse de que tu lógica de limpieza realmente deshace todo lo que hace la configuración. Si notas comportamientos raros sólo en desarrollo, normalmente es porque la limpieza se te ha quedado corta.
Peticiones HTTP y condiciones de carrera con useEffect
Otro escenario muy común para useEffect es hacer peticiones HTTP cuando cambia alguna prop o parte del estado. Por ejemplo, cargar los datos de un usuario cuando cambia userId o recuperar información de un Pokémon cuando se actualiza el nombre en las props.
La idea general es: defines un efecto que lanza una función asíncrona, actualizas el estado con los datos recibidos y especificas en el array de dependencias de qué valores depende esa petición. Si sólo quieres que se dispare al montar, pasas []. Si quieres que se ejecute cada vez que cambie userId, incluyes userId en el array.
Un detalle delicado es qué pasa si el componente se desmonta antes de que la petición responda. Si en el then o tras el await llamas a setState sobre un componente que ya no está montado, puedes encontrarte con warnings o, peor aún, con actualizaciones de estado sobre componentes zombis.
Un patrón habitual para evitar esto es usar una bandera interna (por ejemplo, isMounted o ignore). La defines en el efecto, la pones a true, y en la función de limpieza la pones a false. Dentro de la función asíncrona, justo antes de llamar a setState, compruebas si la bandera sigue en true. Si ya es false, te saltas la actualización. Esto también ayuda a evitar condiciones de carrera cuando varias peticiones pueden resolverse en orden distinto al que se lanzaron.
Otra opción más moderna es usar AbortController si la API que usas lo soporta, de forma que la función de limpieza llame a abort() y la promesa de la petición se rechace sin intentar tocar el estado. En cualquier caso, la lógica de cancelación o de ignorar respuestas debe vivir dentro del propio efecto.
Varias llamadas a useEffect en un mismo componente
Una de las ventajas de los Hooks es que te permiten separar la lógica por lo que hace, no por el método de ciclo de vida en el que toca colocarla. No hay ninguna limitación a la hora de usar varios useEffect en un mismo componente.
Esto significa que puedes tener un efecto para gestionar el título del documento, otro para suscribirte a un chat y otro para escuchar un evento de scroll, cada uno con sus propias dependencias y su propia lógica de limpieza. React ejecutará todos ellos en el orden en el que aparecen en el componente.
Comparado con las clases, donde mucha lógica sin relación acababa mezclada en los mismos métodos de ciclo de vida, los Hooks te animan a dividir por responsabilidades. Un efecto = un propósito concreto. Si un efecto empieza a hacer varias cosas distintas, probablemente es momento de dividirlo en varios.
Este patrón se vuelve aún más potente cuando encapsulas esos efectos en Hooks personalizados (por ejemplo, useWindowSize, useChatStatus, etc.), pero incluso sin llegar a eso ya ganas legibilidad usando múltiples efectos pequeños en lugar de uno gigantesco.
El array de dependencias: reactividad y rendimiento
El array de dependencias es, a la vez, lo que hace potente a useEffect y la fuente de muchos dolores de cabeza. La regla general es sencilla: todo valor reactivo que uses dentro del efecto debe aparecer en ese array. Valores reactivos son props, estado y variables o funciones definidas dentro del componente.
No se trata de «elegir» dependencias para que el efecto se ejecute menos, sino de describir con honestidad de qué valores depende la lógica del efecto. Si el linter de eslint-plugin-react-hooks está bien configurado, te avisará cuando te dejes algo fuera o cuando hayas añadido algo que no hace falta.
Si quieres eliminar una dependencia, no se hace silenciando el warning, se hace cambiando el código para que ese valor deje de ser reactivo. Por ejemplo, si usas una constante serverUrl que nunca cambia, puedes moverla fuera del cuerpo del componente. De ese modo deja de ser un valor reactivo y puedes quitarla del array sin mentirle a React.
Cuando un efecto lee un valor reactivo, lo normal es que quieras que reaccione a sus cambios. Eso significa añadirlo al array. Pero a veces quieres leer el valor «actual» de algo (como un carrito de la compra) sin que los cambios en ese algo vuelvan a disparar el efecto. Para esos casos, React está introduciendo el concepto de «eventos de efecto» (como useEffectEvent), que encapsulan código no reactivo que puede leer el último valor sin convertirse en dependencia.
Para mejorar rendimiento, el patrón típico es restringir el array de dependencias a lo imprescindible. En vez de dejar el efecto sin segundo argumento (lo que lo ejecuta tras cada render), pasas un array con sólo los valores que realmente deben dispararlo. Si ninguna dependencia cambia entre un render y el siguiente, React se salta la limpieza + configuración y el efecto no se reejecuta.
Solución de problemas frecuentes con useEffect
Uno de los errores más comunes es terminar con un efecto que se ejecuta en bucle infinito. Suele ocurrir cuando dentro del propio efecto actualizas un estado que forma parte del array de dependencias. El efecto cambia el estado, el estado dispara un render, el render reejecuta el efecto, y vuelta a empezar.
Para romper ese bucle, revisa por qué estás actualizando ese estado desde el efecto. Si no estás sincronizando con un sistema externo, quizá ni siquiera necesitas useEffect: muchas cosas se pueden resolver directamente en el render. Si sí estás sincronizando con algo externo, comprueba que la actualización de estado sólo dependa de cambios reales y no se dispare siempre.
Otra fuente habitual de problemas son las dependencias que cambian en cada render sin que te des cuenta. Crear objetos o funciones inline en el JSX y usarlos en el efecto hará que el array de dependencias sea siempre distinto, porque cada render genera una referencia nueva. Para evitarlo, puedes mover la creación de esos objetos al propio efecto o, como último recurso, estabilizarlos con useMemo / useCallback.
También es frecuente sorprenderse al ver que la función de limpieza se ejecuta aun cuando el componente sigue en pantalla. Esto es normal: React limpia el efecto anterior antes de aplicar el nuevo cada vez que cambian las dependencias. Y en desarrollo, con StrictMode, hace además un ciclo extra de configuración + limpieza justo tras el montaje.
Por último, si tu efecto hace algo visual y notas un parpadeo antes de que se aplique, puede que necesites cambiar a useLayoutEffect para forzar que se ejecute antes de que el navegador pinte. Es una herramienta a usar con moderación, sólo cuando realmente necesites bloquear el pintado hasta que la medición o el posicionamiento estén listos.
useEffect y renderizado en servidor (SSR)
Un detalle importante en aplicaciones con renderizado en servidor es que los efectos sólo se ejecutan en el cliente. Durante el SSR React genera el HTML inicial, pero no corre los efectos. Estos se disparan después, cuando la app se hidrata en el navegador.
Eso significa que cualquier lógica dentro de useEffect que dependa de APIs del navegador (como localStorage o window) es segura frente al servidor, porque directamente no se ejecutará allí. Lo que sí tienes que cuidar es que el primer render (el que se hace en servidor y luego se repite en cliente) sea determinista y no lea nada que sólo exista en el navegador.
Si quieres mostrar contenido distinto en servidor y en cliente, un patrón común es usar un estado como didMount. Inicialmente está en false, y en un useEffect con [] lo pones a true. Mientras es falso, renderizas una versión «segura» del componente; cuando pasa a verdadero, ya puedes leer de localStorage, tocar el DOM, etc. Eso sí, intenta que el cambio visual no sea demasiado brusco para el usuario con conexiones lentas.
En general, cuanto más consigas que tu árbol de componentes sea independiente del entorno, menos sorpresas tendrás al combinar SSR con Hooks. Reserva useEffect para la integración real con sistemas externos, que es para lo que está pensado.
Dominar bien useState y useEffect pasa por interiorizar que los componentes se «recrean» en cada render, que los efectos pertenecen a un ciclo de vida específico y que el array de dependencias describe la relación entre tu código y los datos reactivos. Cuando interiorizas ese modelo mental, desaparece la sensación de magia oscura y empiezas a usar estos Hooks con soltura, escribiendo componentes más declarativos, limpios y fáciles de mantener incluso en aplicaciones React grandes.
Redactor apasionado del mundo de los bytes y la tecnología en general. Me encanta compartir mis conocimientos a través de la escritura, y eso es lo que haré en este blog, mostrarte todo lo más interesante sobre gadgets, software, hardware, tendencias tecnológicas, y más. Mi objetivo es ayudarte a navegar por el mundo digital de forma sencilla y entretenida.