Dependencias reactivas de useEffect en React | Nicolás Garzón
Todo valor declarado dentro del componente y leído por un effect puede cambiar entre renders: props, estado y funciones locales.
TypeScript
Copiar useEffect ( ( ) => {
connect ( roomId) ;
} , [ roomId] ) ; El array no es una lista de preferencias. Describe los valores reactivos usados por el effect.
React compara dependencias con Object.is. Objetos y funciones nuevos cambian por referencia.
TypeScript
Copiar const options = { roomId } ; Si options entra como dependencia, cambia en cada render. Mueve su creación dentro del effect o estabiliza el diseño.
Desactivar el linter puede congelar valores antiguos dentro del closure.
TypeScript
Copiar
useEffect ( ( ) => connect ( roomId) , [ ] ) ;
Mueve cálculos puros al render.
Mueve objetos al interior del effect.
Usa actualizadores funcionales.
Extrae lógica no reactiva con useEffectEvent cuando corresponda.
Elegir [] para imitar componentDidMount sin revisar valores usados.
Memoizar todo para satisfacer el linter.
Añadir dependencias que crean bucles porque el effect actualiza estado derivado.
Las dependencias se deducen del código. Si no te gusta la lista, cambia la estructura del effect.
El array responde a una pregunta: ¿qué valores hacen que esta sincronización anterior deje de ser válida?
TypeScript
Copiar useEffect ( ( ) => {
const connection = connect ( serverUrl, roomId) ;
return ( ) => connection. disconnect ( ) ;
} , [ serverUrl, roomId] ) ; Si cambia cualquiera, la conexión anterior corresponde a otra configuración y debe reemplazarse.
Props.
State.
Variables calculadas dentro del componente.
Funciones declaradas dentro del componente.
Valores de Context consumidos.
Un valor definido fuera del componente no cambia como consecuencia del render y normalmente no es dependencia:
TypeScript
Copiar const serverUrl = "https://example.com" ; Mover algo fuera es válido solo si realmente es constante para todas las instancias.
React compara cada entrada con Object.is:
TypeScript
Copiar Object. is ( previousDependency, nextDependency) Strings y números se comparan por valor. Objetos, arrays y funciones por referencia.
TypeScript
Copiar const options = { roomId } ;
useEffect ( ( ) => connect ( options) , [ options] ) ; options es nuevo en cada render. Una solución simple:
TypeScript
Copiar useEffect ( ( ) => {
const options = { roomId } ;
return connect ( options) ;
} , [ roomId] ) ; Ahora la dependencia expresa el dato real.
TypeScript
Copiar function createOptions ( ) {
return { roomId } ;
}
useEffect ( ( ) => {
connect ( createOptions ( ) ) ;
} , [ createOptions] ) ; La función cambia por referencia. Antes de useCallback, pregunta si necesita existir fuera del effect. Moverla dentro suele simplificar:
TypeScript
Copiar useEffect ( ( ) => {
function createOptions ( ) {
return { roomId } ;
}
return connect ( createOptions ( ) ) ;
} , [ roomId] ) ; Usa useCallback cuando la identidad estable también es parte de otro contrato, no solo para silenciar el linter.
Un effect que actualiza desde state puede evitar leerlo:
TypeScript
Copiar useEffect ( ( ) => {
const connection = subscribe ( ( message) => {
setMessages ( ( current) => [ ... current, message] ) ;
} ) ;
return connection. unsubscribe;
} , [ ] ) ; El updater recibe el valor pendiente y messages no es una dependencia. Esto es correcto porque la sincronización no necesita reiniciarse cuando cambia la lista.
TypeScript
Copiar useEffect ( ( ) => {
const connection = connect ( roomId) ;
connection. on ( "connected" , ( ) => {
showNotification ( theme) ;
} ) ;
return ( ) => connection. disconnect ( ) ;
} , [ roomId, theme] ) ; Si cambiar tema no debería reconectar, separa esa lectura mediante useEffectEvent:
TypeScript
Copiar const onConnected = useEffectEvent ( ( ) => {
showNotification ( theme) ;
} ) ;
useEffect ( ( ) => {
const connection = connect ( roomId) ;
connection. on ( "connected" , onConnected) ;
return ( ) => connection. disconnect ( ) ;
} , [ roomId] ) ; No muevas roomId al Effect Event: sí define la conexión y es reactivo.
TypeScript
Copiar useEffect ( ( ) => {
synchronize ( user. id) ;
} , [ user] ) ; Si solo importa user.id, declarar esa propiedad expresa mejor la sincronización:
TypeScript
Copiar useEffect ( ( ) => {
synchronize ( user. id) ;
} , [ user. id] ) ; No reduzcas dependencias si otras propiedades sí afectan el setup.
TypeScript
Copiar useEffect ( ( ) => connectChat ( roomId) , [ roomId] ) ;
useEffect ( ( ) => updateDocumentTitle ( roomId) , [ roomId] ) ; Aunque comparten dependencia, representan sistemas distintos. Separarlos permite que cada setup tenga su cleanup y evolución independiente.
TypeScript
Copiar useEffect ( ( ) => {
setOptions ( { theme } ) ;
} , [ options, theme] ) ; El effect crea un objeto nuevo, actualiza state, renderiza y cambia options otra vez. Pregunta si options puede derivarse durante render. La mayoría de loops revelan estado redundante o un effect innecesario.
[] significa que el effect no lee valores reactivos cambiantes. No significa literalmente “ejecutar una vez para siempre”:
Strict Mode prueba setup/cleanup en desarrollo.
Un remount crea otra ejecución.
Fast Refresh puede reiniciar efectos.
Cambiar key crea una instancia nueva.
Solo debería aparecer con una justificación verificable y una API cuyo contrato el linter no puede inferir. En código de producto habitual, ocultar el warning crea stale closures.
Escribe qué sistema sincroniza el effect.
Enumera valores leídos.
Distingue cuáles definen la sincronización.
Mueve cálculos y helpers al lugar más estrecho.
Usa updater para cambios basados en state anterior.
Separa lógica no reactiva cuando corresponda.
Divide effects por sistema.
Conserva el array que el código demuestra.
Las dependencias describen el código, no una frecuencia deseada.
Objetos y funciones cambian por referencia.
Cambia el diseño antes de memoizar o suprimir.
Updaters y Effect Events eliminan lecturas solo cuando la semántica lo permite.
Un array vacío es una afirmación técnica que debe ser verdadera.
¿Por qué un objeto local vuelve a ejecutar el effect?
¿Cuándo un updater elimina una dependencia correctamente?
¿Qué valor no deberías mover a useEffectEvent?
¿Por qué [] no garantiza una única ejecución durante toda la aplicación?
Ver respuestas
Porque cada render crea otra referencia y Object.is devuelve false.
Cuando solo necesitas transformar el estado pendiente y no reiniciar la sincronización por él.
El que define el recurso sincronizado, como roomId.
Porque la instancia puede probarse, desmontarse y montarse nuevamente.
Cleanup, cancelación y race conditions muestra cómo invalidar correctamente cada ejecución cuando sus dependencias cambian.