Eliminar dependencias de los Efectos
Cuando escribes un Efecto, el linter verificará que has incluido todos los valores reactivos (como las props y el estado) que tu Efecto lee en la lista de dependencias de tu Efecto. Así se asegura que el Efecto se mantenga sincronizado con las últimas props y el último estado de tu componente. Dependencias innecesarias pueden ocasionar que tu Efecto se ejecute demasiadas veces, o incluso crear un ciclo infinito. Sigue esta guía para revisar y eliminar dependencias innecesarias de tus Efectos.
Aprenderás
- Cómo arreglar ciclos infinitos de dependencias de un Efecto
- Qué hacer cuando quieres eliminar una dependencia
- Cómo leer un valor en un Efecto sin «reaccionar» a él
- Cómo y por qué evitar objectos y funciones como dependencias
- Por qué suprimir la advertencia de la dependencia es peligroso, y qué hacer en su lugar
Las dependencias deben corresponderse con el código
Cuando escribes un Efecto, primero debes especificar como iniciar y parar lo que sea que tu Efecto está haciendo.
const serverUrl = 'https://localhost:1234';
function ChatRoom({ roomId }) {
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => connection.disconnect();
// ...
}
Entonces, si dejas la lista de dependencias del Efecto vacía ([]
), el linter sugerirá las dependencias correctas:
import { useState, useEffect } from 'react'; import { createConnection } from './chat.js'; const serverUrl = 'https://localhost:1234'; function ChatRoom({ roomId }) { useEffect(() => { const connection = createConnection(serverUrl, roomId); connection.connect(); return () => connection.disconnect(); }, []); // <-- Fix the mistake here! return <h1>Welcome to the {roomId} room!</h1>; } export default function App() { const [roomId, setRoomId] = useState('general'); return ( <> <label> Choose the chat room:{' '} <select value={roomId} onChange={e => setRoomId(e.target.value)} > <option value="general">general</option> <option value="travel">travel</option> <option value="music">music</option> </select> </label> <hr /> <ChatRoom roomId={roomId} /> </> ); }
Llénalas de acuerdo a lo que dice el linter:
function ChatRoom({ roomId }) {
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => connection.disconnect();
}, [roomId]); // ✅ All dependencies declared
// ...
}
Los Efectos «reaccionar» a valores reactivos. Dado que roomId
es un valor reactivo (puede cambiar durante un rerenderizado), el linter verifica que lo has especificado como una dependencia. Si roomId
recibe un valor diferente, React resincronizará tu Efecto. Esto asegura que el chat se mantiene conectado a la sala seleccionada y «reacciona» al dropdown:
import { useState, useEffect } from 'react'; import { createConnection } from './chat.js'; const serverUrl = 'https://localhost:1234'; function ChatRoom({ roomId }) { useEffect(() => { const connection = createConnection(serverUrl, roomId); connection.connect(); return () => connection.disconnect(); }, [roomId]); return <h1>Welcome to the {roomId} room!</h1>; } export default function App() { const [roomId, setRoomId] = useState('general'); return ( <> <label> Choose the chat room:{' '} <select value={roomId} onChange={e => setRoomId(e.target.value)} > <option value="general">general</option> <option value="travel">travel</option> <option value="music">music</option> </select> </label> <hr /> <ChatRoom roomId={roomId} /> </> ); }
Para eliminar una dependencia, prueba que no es una dependencia
Debes notar que no puedes «escoger» tus dependencias de tu Efecto. Cada valor reactivo que se usa en el código de tu Efecto debe declararse en tu lista de dependencias. La lista de dependencias de tu Efecto está determinada por el código a su alrededor:
const serverUrl = 'https://localhost:1234';
function ChatRoom({ roomId }) { // Este es un valor reactivo
useEffect(() => {
const connection = createConnection(serverUrl, roomId); // Este Efecto lee el valor reactivo
connection.connect();
return () => connection.disconnect();
}, [roomId]); // ✅ Por tanto debes especificar el valor reactivo como una dependencia de tu Efecto
// ...
}
Los valores reactivos incluyen las props y todas las variables y funciones declaradas directamente dentro de componente. Dado que roomId
es un valor reactivo, no puedes eliminarlo de la lista de dependencias. El linter no lo permitiría:
const serverUrl = 'https://localhost:1234';
function ChatRoom({ roomId }) {
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => connection.disconnect();
}, []); // 🔴 React Hook useEffect has a missing dependency: 'roomId'
// ...
}
¡Y el linter estaría en lo correcto! Dado que roomId
puede cambiar con el tiempo, esto introduciría un bug en tu código.
Para eliminar una dependencias, necesitas «probarle» al linter que no necesita ser una dependencia. Por ejemplo, puedes mover roomId
fuera de componente para probar que no es reactivo y no cambiará entre rerenderizados:
const serverUrl = 'https://localhost:1234';
const roomId = 'music'; // Ya no es un valor reactivo
function ChatRoom() {
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => connection.disconnect();
}, []); // ✅ Se declararon todas las dependencias
// ...
}
Ahora que roomId
no es un valor reactivo (y no puede cambiar en un rerenderizado) no necesita estar como dependencia:
import { useState, useEffect } from 'react'; import { createConnection } from './chat.js'; const serverUrl = 'https://localhost:1234'; const roomId = 'music'; export default function ChatRoom() { useEffect(() => { const connection = createConnection(serverUrl, roomId); connection.connect(); return () => connection.disconnect(); }, []); return <h1>Welcome to the {roomId} room!</h1>; }
Por esto es que ahora podemos especificar una lista de dependencias vacía ([]
). Tu Efecto realmente no depende y de ningún valor reactivo, por lo que realmente no necesita volverse a ejecutar cuando cualquiera de las props o el estado del componente cambie.
Para cambiar las dependencias, cambia el código
Puede que hayas notado un patrón en tu flujo de trabajo:
- Primero, cambias el código de tu Efecto o como se declaran los valores reactivos.
- Luego, sigues al linter y ajustas las dependencias para hacerlas corresponder con el código que cambiaste.
- Si no estás a gusto con la lista de dependencias, puedes ir al primer paso (y cambiar el código nuevamente).
La última parte es importante. Si quieres cambiar las dependencias, cambia primero el código que lo circunda. Puedes pensar en la lista de dependencia como una lista de todos los valores reactivos usado por el código de tu Efecto. No eliges intencionalmente qué poner en esa lista. La lista describe tu código. Para cambiar la lista de dependencia, cambia el código.
Esto puede parecerse a resolver una ecuación. Puedes iniciar con un objetivo (por ejemplo, eliminar una dependencia), y necesitas «encontrar» el código exacto que logre ese objetivo. No todo el mundo encuentra divertido resolver ecuaciones ¡y lo mismo podría decirse sobre escribir Efectos! Por suerte, debajo hay una lista de recetas comunes que puedes probar.
Profundizar
Suprimir la advertencia del linter conduce a errores muy poco intuitivos que son difíciles de encontrar y corregir. Aquí hay un ejemplo:
import { useState, useEffect } from 'react'; export default function Timer() { const [count, setCount] = useState(0); const [increment, setIncrement] = useState(1); function onTick() { setCount(count + increment); } useEffect(() => { const id = setInterval(onTick, 1000); return () => clearInterval(id); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); return ( <> <h1> Counter: {count} <button onClick={() => setCount(0)}>Reset</button> </h1> <hr /> <p> Every second, increment by: <button disabled={increment === 0} onClick={() => { setIncrement(i => i - 1); }}>–</button> <b>{increment}</b> <button onClick={() => { setIncrement(i => i + 1); }}>+</button> </p> </> ); }
Digamos que querías ejecutar el Efecto «solo durante el montaje». Has leído que la lista de dependencias vacía ([]
) hacen eso, así que decides ignorar al linter y especificar a la fuerza []
como dependencias.
Este contador se supone que incremente cada segundo la cantidad configurable con los dos botones. Sin embargo, dado que le «mentiste» a React diciendo que este Efecto no tiene dependencias, React sigue usando la función onTick
del renderizado inicial. Durante ese renderizado count
era 0
e increment
era 1
. Por eso es que onTick
de ese renderizado siempre llama a setCount(0 + 1)
cada segundo, y siempre ves 1
. Errores como este son difíciles de corregir cuando están esparcidos por múltiples componentes.
¡Siempre hay una mejor solución que ignorar el linter! Para corregir este código, necesitas añadir onTick
a la lista de dependencias. (Para asegurarte de que el intervalo solo se configure una vez, [haz onTick
un Evento de Efecto])(/learn/separating-events-from-effects#reading-latest-props-and-state-with-effect-events)).
Recomendamos tratar el error de linter de la lista de dependencias como un error de compilación. Si no lo suprimes, nunca verás bugs como este. El resto de esta página documenta las alternativas para este y otros casos.
Eliminar dependencias innecesarias
Cada vez que ajustas las dependencias del Efecto para reflejar el código, mira a la lista de dependencias. ¿Tiene sentido volver a correr cuando alguna de estas dependencias cambie? A veces, la respuesta es «no»:
- A veces, quieres volver a ejecutar diferentes partes de tu Efecto bajo condiciones diferentes.
- A veces, quieres leer solo el último valor de alguna dependencia en lugar de «reaccionar» a sus cambios.
- A veces, una dependencia puede cambiar muy a menudo de forma no intencional porque es un objeto o una función.
Para encontrar la solución correcta, necesitas responder algunas preguntas sobre tu Efecto. Revisémoslas.
¿Debería moverse este código a un manejador de eventos?
Sobre lo primero que debes pensar es si este código debería ser un Efecto.
Imagina un formulario. Al enviarse, actualizas la variable de estado submitted
a true
. Necesitas enviar una petición POST y mostrar una notificación. Has decidido ubicar esta lódigo dentro de un Efecto que «reacciona» al cambio de submitted
a true
:
function Form() {
const [submitted, setSubmitted] = useState(false);
useEffect(() => {
if (submitted) {
// 🔴 Evita: Lógica específica de Evento dentro de un Efecto
post('/api/register');
showNotification('Successfully registered!');
}
}, [submitted]);
function handleSubmit() {
setSubmitted(true);
}
// ...
}
Después, quieres estilizar el mensaje de notificación de acuerdo al tema actual, así que lees el tema actual. Dado que theme
se declara en el cuerpo del componente, es un valor reactivo, y debes declararlo como una dependencia:
function Form() {
const [submitted, setSubmitted] = useState(false);
const theme = useContext(ThemeContext);
useEffect(() => {
if (submitted) {
// 🔴 Evita: Lógica específica de Evento dentro de un Efecto
post('/api/register');
showNotification('Successfully registered!', theme);
}
}, [submitted, theme]); // ✅ Todas las dependencias declaradas
function handleSubmit() {
setSubmitted(true);
}
// ...
}
Pero al hacer esto, has introducido un bug. Imagina que envías un formulario primero y luego cambias entre temas oscuros y claros. La variable theme
cambiará, el Efecto se volverá a ejecutar, ¡y por tanto mostrará la misma notificación nuevamente!
El problema aquí es que no debió haber sido nunca un Efecto. Quieres enviar una petición POST y mostrar la notificación en respuesta al envío del formulario, que es una interacción particular. Cuando quieres ejecutar algún código en respuesta a una interacción particular, pon esa lógica directamente en el manejador de eventos correspondiente:
function Form() {
const theme = useContext(ThemeContext);
function handleSubmit() {
// ✅ Bien: Lógica específica de Evento se llama desde manejadores de evento
post('/api/register');
showNotification('Successfully registered!', theme);
}
// ...
}
Ahora que el código está en un manejador de evento, no es reactivo —por lo que solo se ejecutará cuando el usuario envía el formulario—. Lee más acerca de escoger entre manejadores de eventos y Efectos y cómo eliminar Efectos innecesarios .
¿Tú Efecto hace varias cosas no relacionadas?
La próxima preguntas que te debes hacer es si tu Efecto está haciendo varias cosas no relacionadas.
Imagina que estás creando un formulario de envíos en el que el usuario necesita elegir su ciudad y área. Obtienes la lista de ciudades cities
del servidor de acuerdo al país seleccionado country
de forma tal que los puedas mostrar como opciones en un dropdown:
function ShippingForm({ country }) {
const [cities, setCities] = useState(null);
const [city, setCity] = useState(null);
useEffect(() => {
let ignore = false;
fetch(`/api/cities?country=${country}`)
.then(response => response.json())
.then(json => {
if (!ignore) {
setCities(json);
}
});
return () => {
ignore = true;
};
}, [country]); // ✅ All dependencies declared
// ...
Este es un buen ejemplo de obtener datos en un Efecto. Estás sincronizando el estado cities
con la red de acuerdo a la prop country
. No puedes hacer esto en un manejador de eventos porque necesitas obtener los datos tan pronto como se muestre ShippingForm
y cada vez que cambie country
(sin importar qué interacciones causa el cambio).
Digamos ahora que estás añadiendo una segunda caja de selección para las areas de la ciudad, que debería obtener las areas
para la ciudad city
actualmente seleccionada. Podrías comenzar añadiendo una segunda llamada fetch
para la lista de areas dentro del mismo Efecto:
function ShippingForm({ country }) {
const [cities, setCities] = useState(null);
const [city, setCity] = useState(null);
const [areas, setAreas] = useState(null);
useEffect(() => {
let ignore = false;
fetch(`/api/cities?country=${country}`)
.then(response => response.json())
.then(json => {
if (!ignore) {
setCities(json);
}
});
// 🔴 Avoid: A single Effect synchronizes two independent processes
if (city) {
fetch(`/api/areas?city=${city}`)
.then(response => response.json())
.then(json => {
if (!ignore) {
setAreas(json);
}
});
}
return () => {
ignore = true;
};
}, [country, city]); // ✅ All dependencies declared
// ...
Sin embargo, como ahora el Efecto usa la variable de estado city
, tienes que añadir city
a la lista de dependencias. Resulta que esto introduce un problema. Ahora, cada vez que el usuario seleccionar una ciudad diferente, el Efecto volverá a ejecutarse y llamar a fetchCities(country)
. Como resultado, obtendrás innecesariamente la lista de ciudades muchas veces.
El problema con este código es que estás sincronizando dos cosas que no guardan relación:
- Quieres sincronizar el estado
cities
con la red con base en la propcountry
. - Quieres sincronizar el estado
areas
con la red con base en el estadocity
.
Divide la lógica en dos Efectos y cada uno reaccionará a la variable que necesita para sincronizarse:
function ShippingForm({ country }) {
const [cities, setCities] = useState(null);
useEffect(() => {
let ignore = false;
fetch(`/api/cities?country=${country}`)
.then(response => response.json())
.then(json => {
if (!ignore) {
setCities(json);
}
});
return () => {
ignore = true;
};
}, [country]); // ✅ All dependencies declared
const [city, setCity] = useState(null);
const [areas, setAreas] = useState(null);
useEffect(() => {
if (city) {
let ignore = false;
fetch(`/api/areas?city=${city}`)
.then(response => response.json())
.then(json => {
if (!ignore) {
setAreas(json);
}
});
return () => {
ignore = true;
};
}
}, [city]); // ✅ All dependencies declared
// ...
Ahora el primer Efecto solo se vuelve a ejecutar si country
cambia, mientras el segundo Efecto se vuelve a ejecutar cuando city
cambia. Los has separado a propósito: dos cosas diferentes se sincronizan con dos Efectos separados. Dos Efectos separados tienen dos listas de dependencias separadas, por lo que ya no se activarán mutuamente sin quererlo.
El código final no es más largo que el original, pero separar estos Efectos aún es correcto. Cada Efecto debe representar un proceso de sincronización independiente. En este ejemplo, eliminar un Efecto no rompe la lógica del otro Efecto. Este es un buen indicador de que sincronizan cosas diferentes, y tenía sentido separarlos. Si la duplicación te preocupa, puedes mejorar este código aún más extrayendo lógica repetitiva en un Hook personalizado.
¿Estás leyendo algún estado para calcular el próximo estado?
Este Efecto actualiza la variable de estado messages
con un nuevo array creado cada vez que llega un nuevo mensaje:
function ChatRoom({ roomId }) {
const [messages, setMessages] = useState([]);
useEffect(() => {
const connection = createConnection();
connection.connect();
connection.on('message', (receivedMessage) => {
setMessages([...messages, receivedMessage]);
});
// ...
Usa la variable messages
para crear un nuevo array que se inicia con todos los mensajes existentes y añade el nuevo mensaje al final. Sin embargo, dado que messages
es un valor reactivo que un Efecto lee, debe ser una dependencia:
function ChatRoom({ roomId }) {
const [messages, setMessages] = useState([]);
useEffect(() => {
const connection = createConnection();
connection.connect();
connection.on('message', (receivedMessage) => {
setMessages([...messages, receivedMessage]);
});
return () => connection.disconnect();
}, [roomId, messages]); // ✅ All dependencies declared
// ...
Y cuando se incluye messages
como dependencia se introduce un problema.
Cada vez que recibes un mensaje, setMessages()
causa que el componente se vuelva a renderizar con un nuevo array messages
que incluye el mensaje recibido. Sin embargo, dado que este Efecto ahora depende de messages
, esto también resincronizará el Efecto. Por tanto cada nuevo mensaje hará que el chat se reconecte. ¡El usuario no querría eso!
Para resolver el problema, no leas messages
dentro del Efecto. En cambio, pasa una función actualizadora a setMessages
:
function ChatRoom({ roomId }) {
const [messages, setMessages] = useState([]);
useEffect(() => {
const connection = createConnection();
connection.connect();
connection.on('message', (receivedMessage) => {
setMessages(msgs => [...msgs, receivedMessage]);
});
return () => connection.disconnect();
}, [roomId]); // ✅ All dependencies declared
// ...
Ten en cuenta que ahora el Efecto no lee para nada la variable messages
. Solo necesitas pasar una función actualizadora como msgs => [...msgs, receivedMessage]
. React pone tu función actualizadora en una cola y le proporcionará el parámetro msgs
en el próximo renderizado. Es por esto que el Efecto en sí ya no necesita la dependencia de messages
. Como resultado de esta solución, al recibir un mensaje de chat ya no se provocará que el chat se reconecte.
¿Quieres leer un valor sin «reaccionar» as sus cambios?
Supón que quieres poner un sonido cuando el usuario recibe un nuevo mensaje a menos que isMuted
sea true
:
function ChatRoom({ roomId }) {
const [messages, setMessages] = useState([]);
const [isMuted, setIsMuted] = useState(false);
useEffect(() => {
const connection = createConnection();
connection.connect();
connection.on('message', (receivedMessage) => {
setMessages(msgs => [...msgs, receivedMessage]);
if (!isMuted) {
playSound();
}
});
// ...
Dado que tu Efecto ahora usa isMuted
en su código, tienes que añadirlo a las dependencias:
function ChatRoom({ roomId }) {
const [messages, setMessages] = useState([]);
const [isMuted, setIsMuted] = useState(false);
useEffect(() => {
const connection = createConnection();
connection.connect();
connection.on('message', (receivedMessage) => {
setMessages(msgs => [...msgs, receivedMessage]);
if (!isMuted) {
playSound();
}
});
return () => connection.disconnect();
}, [roomId, isMuted]); // ✅ All dependencies declared
// ...
El problema es que cada vez que isMuted
cambie (por ejemplo, cuando el usuario presiona el botón «Muted»), el Efecto se volverá a sincronizar y se reconectará al servidor de chat. ¡Esta no es la experiencia de usuario deseada! (En este ejemplo, aún deshabilitando el linter no funcionaría —si haces eso, isMuted
se quedaría «atrapado» en su valor antiguo—).
Para resolver este problema, necesitas extraer la lógica que no debe ser reactiva fuera de tu Efecto. No quieres que este Efecto «reaccione» a los cambios de isMuted
. Mueve este pedazo de lógica a un Evento de Efecto::
import { useState, useEffect, useEffectEvent } from 'react';
function ChatRoom({ roomId }) {
const [messages, setMessages] = useState([]);
const [isMuted, setIsMuted] = useState(false);
const onMessage = useEffectEvent(receivedMessage => {
setMessages(msgs => [...msgs, receivedMessage]);
if (!isMuted) {
playSound();
}
});
useEffect(() => {
const connection = createConnection();
connection.connect();
connection.on('message', (receivedMessage) => {
onMessage(receivedMessage);
});
return () => connection.disconnect();
}, [roomId]); // ✅ All dependencies declared
// ...
Los Eventos de Efecto te permiten separar un Efecto en partes reactivas (que deben «reaccionar» a valores reactivos como roomId
y sus cambios) y partes no reactivas (que solo leen sus últimos valores, como onMessage
lee isMuted
). Ahora que has leído isMuted
dentro de un Evento de Efecto, no necesita ser una dependencia de tu Efecto. Como resultado, el chat no se reconectará cuando cambies la configuración «Muted» de on a off, ¡solucionando el problema original!
Envolver un manejador de evento de las props
Puede que te hayas encontrado con un problema similar en el que tu componente recibe un manejador de eventos como una prop:
function ChatRoom({ roomId, onReceiveMessage }) {
const [messages, setMessages] = useState([]);
useEffect(() => {
const connection = createConnection();
connection.connect();
connection.on('message', (receivedMessage) => {
onReceiveMessage(receivedMessage);
});
return () => connection.disconnect();
}, [roomId, onReceiveMessage]); // ✅ All dependencies declared
// ...
Supón que el componente padre pasa un función onReceiveMessage
diferente en cada renderizado:
<ChatRoom
roomId={roomId}
onReceiveMessage={receivedMessage => {
// ...
}}
/>
Dado que onReceiveMessage
es una dependencia de tu Efecto, causaría que el Efecto se vuelva a sincronizar después de cada rerenderizado del padre. Esto haría que se reconecte al chat. Para resolver esto, envuelve la llamada en un Evento de Efecto:
function ChatRoom({ roomId, onReceiveMessage }) {
const [messages, setMessages] = useState([]);
const onMessage = useEffectEvent(receivedMessage => {
onReceiveMessage(receivedMessage);
});
useEffect(() => {
const connection = createConnection();
connection.connect();
connection.on('message', (receivedMessage) => {
onMessage(receivedMessage);
});
return () => connection.disconnect();
}, [roomId]); // ✅ All dependencies declared
// ...
Los Eventos de Efecto no son reactivas, por lo que no necesitas especificarlas como dependencias. Como resultado, el chat no se reconectará más aún si el componente padre pasa una función que es diferente en cada rerenderizado.
Separar código reactivo y código no reactivo
En este ejemplo, quieres registrar una visita cada vez que cambia roomId
. Quieres incluir el valor actual de notificationCount
con cada registro, pero no quieres que un cambio a notificationCount
dispare un nuevo evento de registro.
La solución nuevamente consiste en separar el código no reactivo en un Evento de Efecto:
function Chat({ roomId, notificationCount }) {
const onVisit = useEffectEvent(visitedRoomId => {
logVisit(visitedRoomId, notificationCount);
});
useEffect(() => {
onVisit(roomId);
}, [roomId]); // ✅ All dependencies declared
// ...
}
Quieres que tu lógica sea reactiva con respecto a roomId
, por lo que quieres leer roomId
dentro de tu Efecto. Sin embargo, no quieres que un cambio a notificationCount
registre una nueva visita, por lo que lees notificationCount
dentro del Evento de Efecto. Aprende más sobre leer las últimas props y estado desde Efectos con el uso de Eventos de Efecto.
¿Algún valor reactivo cambia inintencionadamente?
A veces, sí quieres que tu Efecto reaccione a cierto valor, pero los cambios a ese valor son más frecuentes de lo que quisieras —y puede que no refleje un cambio real desde la perspectiva del usuario—. Por ejemplo, digamos que creas un objeto options
en el cuerpo de tu componente, y luego lees ese objeto dentro de tu Efecto:
function ChatRoom({ roomId }) {
// ...
const options = {
serverUrl: serverUrl,
roomId: roomId
};
useEffect(() => {
const connection = createConnection(options);
connection.connect();
// ...
Este objeto se declara en el cuerpo del componente, por lo que es un valor reactivo. Cuando lees un valor reactivo como este dentro de un Efecto, lo declaras como una dependencia. Esto garantiza que tu Efecto «reacciona» a sus cambios:
// ...
useEffect(() => {
const connection = createConnection(options);
connection.connect();
return () => connection.disconnect();
}, [options]); // ✅ All dependencies declared
// ...
¡Es importante declararlo como una dependencia! Esto garantiza, por ejemplo, que si cambia roomId
, luego tu Efecto se volverá a conectar al chat con las nuevas opciones. Sin embargo, también hay un problema con el código de arriba. Para ver el problema, intenta escribir en la caja de texto del sandbox de abajo y mira que pasa en la consola:
import { useState, useEffect } from 'react'; import { createConnection } from './chat.js'; const serverUrl = 'https://localhost:1234'; function ChatRoom({ roomId }) { const [message, setMessage] = useState(''); // Temporarily disable the linter to demonstrate the problem // eslint-disable-next-line react-hooks/exhaustive-deps const options = { serverUrl: serverUrl, roomId: roomId }; useEffect(() => { const connection = createConnection(options); connection.connect(); return () => connection.disconnect(); }, [options]); return ( <> <h1>Welcome to the {roomId} room!</h1> <input value={message} onChange={e => setMessage(e.target.value)} /> </> ); } export default function App() { const [roomId, setRoomId] = useState('general'); return ( <> <label> Choose the chat room:{' '} <select value={roomId} onChange={e => setRoomId(e.target.value)} > <option value="general">general</option> <option value="travel">travel</option> <option value="music">music</option> </select> </label> <hr /> <ChatRoom roomId={roomId} /> </> ); }
En el sandbox de arriba, la caja de texto solo actualiza la variable de estado message
. Desde la perspectiva del usuario, esto no debería afectar a la conexión del chat. Sin embargo, cada vez que actualizas la variable message
, tu componente se vuelve a renderizar. Cuando tu componente rerenderiza, el código dentro de él se ejecuta nuevamente.
Esto significa que se crea un nuevo objeto options
en cada rerenderizado del componente ChatRoom
. React ve que ese objeto options
es un objeto diferente al objeto options
que se creó en el renderizado anterior. Es por eso que resincroniza tu Efecto (que depende de options
) y el chat se reconecta mientras escribes.
Este problema afecta a objetos y funciones en particular. En JavaScript, cada objeto y función creado nuevamente se considera distinto a todos los demás objetos. ¡No importa si el contenido dentro de ellos puede ser el mismo!
// During the first render
const options1 = { serverUrl: 'https://localhost:1234', roomId: 'music' };
// During the next render
const options2 = { serverUrl: 'https://localhost:1234', roomId: 'music' };
// These are two different objects!
console.log(Object.is(options1, options2)); // false
Objetos y funciones como dependencias crean un riesgo de que tu Efecto se resincronice más a menudo de lo que necesitas.
Es por esto que, siempre que sea posible, debes intentar evitar objetos y funciones como dependencias de los Efectos. En su lugar, intenta moverlos fuera del componente, o dentro del Efecto, o extraer valores primitivos fuera de ellos.
Mueve objetos estáticos y funciones fuera de tu componente
Si el objeto no depende de ninguna prop o estado, puedes mover ese objeto fuera de tu componente:
const options = {
serverUrl: 'https://localhost:1234',
roomId: 'music'
};
function ChatRoom() {
const [message, setMessage] = useState('');
useEffect(() => {
const connection = createConnection(options);
connection.connect();
return () => connection.disconnect();
}, []); // ✅ All dependencies declared
// ...
De esta forma, le pruebas al linter que no es reactivo. No puede cambiar como resultado de un rerenderizado, por lo que no necesita ser una dependencia de tu Efecto. Ahora si se rerenderiza ChatRoom
no causará que se resincronice tu Efecto.
Esto también sirve para funciones:
function createOptions() {
return {
serverUrl: 'https://localhost:1234',
roomId: 'music'
};
}
function ChatRoom() {
const [message, setMessage] = useState('');
useEffect(() => {
const options = createOptions();
const connection = createConnection();
connection.connect();
return () => connection.disconnect();
}, []); // ✅ All dependencies declared
// ...
Dado que createOptions
se declara fuera del componente, no es un valor reactivo. Es por eso que no necesita especificarse en las dependencias de tu Efecto y por qué nunca causará que tu Efecto se resincronice.
Mueve objetos y funciones dinámicas dentro de tu Efecto
Si tu objeto depende de algún valor reactivo que puede cambiar como resultado de un rerenderizado, como la prop roomId
, no puedes sacarlo fuera de tu component. Sin embargo, sí puedes mover su creación dentro del código de tu Efecto:
const serverUrl = 'https://localhost:1234';
function ChatRoom({ roomId }) {
const [message, setMessage] = useState('');
useEffect(() => {
const options = {
serverUrl: serverUrl,
roomId: roomId
};
const connection = createConnection(options);
connection.connect();
return () => connection.disconnect();
}, [roomId]); // ✅ All dependencies declared
// ...
Ahora que options
se declara dentro de tu Efecto, ya no es una dependencia de tu Efecto. En cambio, el único valor reactivo que usa tu Efecto es roomId
. Dado que roomId
no es un objeto o una función, puedes tener la seguridad de que no será inintencionadamente diferente. En JavaScript, números y cadenas se comparan por su contenido:
// During the first render
const roomId1 = 'music';
// During the next render
const roomId2 = 'music';
// These two strings are the same!
console.log(Object.is(roomId1, roomId2)); // true
Gracias a esta solución, el chat no se reconectará más si editas la caja de texto:
import { useState, useEffect } from 'react'; import { createConnection } from './chat.js'; const serverUrl = 'https://localhost:1234'; function ChatRoom({ roomId }) { const [message, setMessage] = useState(''); useEffect(() => { const options = { serverUrl: serverUrl, roomId: roomId }; const connection = createConnection(options); connection.connect(); return () => connection.disconnect(); }, [roomId]); return ( <> <h1>Welcome to the {roomId} room!</h1> <input value={message} onChange={e => setMessage(e.target.value)} /> </> ); } export default function App() { const [roomId, setRoomId] = useState('general'); return ( <> <label> Choose the chat room:{' '} <select value={roomId} onChange={e => setRoomId(e.target.value)} > <option value="general">general</option> <option value="travel">travel</option> <option value="music">music</option> </select> </label> <hr /> <ChatRoom roomId={roomId} /> </> ); }
Sin embargo, sí se reconecta cuando cambias el botón desplegable para elegir roomId
, como se esperaría.
Esto funciona también para funciones:
const serverUrl = 'https://localhost:1234';
function ChatRoom({ roomId }) {
const [message, setMessage] = useState('');
useEffect(() => {
function createOptions() {
return {
serverUrl: serverUrl,
roomId: roomId
};
}
const options = createOptions();
const connection = createConnection(options);
connection.connect();
return () => connection.disconnect();
}, [roomId]); // ✅ All dependencies declared
// ...
Puedes escribir tus propias funciones para agrupar porciones de lógica dentro de tu Efecto. Siempre que las declares dentro de tu Efecto, no serán valores reactivos, y por tanto no necesitan ser dependencias de tu Efecto.
Leer valores primitivos de objetos
En ocasiones, puede que recibas un objeto como prop:
function ChatRoom({ options }) {
const [message, setMessage] = useState('');
useEffect(() => {
const connection = createConnection(options);
connection.connect();
return () => connection.disconnect();
}, [options]); // ✅ All dependencies declared
// ...
El riesgo aquí es que el componente padre cree el objeto durante el renderizado:
<ChatRoom
roomId={roomId}
options={{
serverUrl: serverUrl,
roomId: roomId
}}
/>
Esto causaría que tu Efecto se reconectara cada vez que el componente padre se rerenderiza. Para solucionarlo, lee toda la información necesaria del objeto fuera del Efecto y evita tener objetos y funciones como dependencias:
function ChatRoom({ options }) {
const [message, setMessage] = useState('');
const { roomId, serverUrl } = options;
useEffect(() => {
const connection = createConnection({
roomId: roomId,
serverUrl: serverUrl
});
connection.connect();
return () => connection.disconnect();
}, [roomId, serverUrl]); // ✅ All dependencies declared
// ...
La lógica se vuelve un poco repetitiva (lees algunos valores de un objeto fuera de un Efecto, y luego creas un objeto con los mismos valores dentro de un Efecto). Pero deja muy explícitamente de qué información depende realmente tu Efecto. Si un objeto se vuelve a crear sin intención por el componente padre, el chat no se reconectaría. Sin embargo, si options.roomId
o options.serverUrl
sí cambian, el chat se volvería a conectar como esperarías.
Calcular valores primitivos de funciones
El mismo enfoque puede servir para las funciones. Por ejemplo, supón que el componente padre pasa una función:
<ChatRoom
roomId={roomId}
getOptions={() => {
return {
serverUrl: serverUrl,
roomId: roomId
};
}}
/>
Para evitar hacerla una dependencias (y causar que se reconecte on cada rerenderizado), llámala fuera del Efecto. Esto te da los valores roomId
y serverUrl
que no son objetos y que puedes leerlos desde dentro de tu Efecto:
function ChatRoom({ getOptions }) {
const [message, setMessage] = useState('');
const { roomId, serverUrl } = getOptions();
useEffect(() => {
const connection = createConnection({
roomId: roomId,
serverUrl: serverUrl
});
connection.connect();
return () => connection.disconnect();
}, [roomId, serverUrl]); // ✅ All dependencies declared
// ...
Esto solo funciona para funciones puras porque es seguro llamarlas durante el renderizado. Si tu función es un manejador de eventos, pero no quieres que sus cambios resincronicen tu Efecto, envuélvela en un Evento de Efecto
Recapitulación
- Las dependencias siempre deben corresponderse con el código.
- Cuando no estás a gusto con tus dependencias, lo que necesitas editar es el código.
- Suprimir el linter lleva a errores confusos, y siempre deberías evitarlo.
- Para eliminar una dependencia, debes «probarle» al linter que no es necesaria.
- Si el código en tu Efecto debe ejecutarse como respuesta a una interacción específica, mueve el código a un manejador de eventos.
- Si partes diferentes de tu Efecto deberían volverse a ejecutar por diferentes razones, divídelo en diferentes Efectos.
- Si quieres actualizar un estado basado en el estado anterior, pasa una función actualizadora.
- Si quieres leer el último valor sin «reaccionar» a él, extrae un Evento de Efecto de tu Efecto.
- En JavaScript, los objetos y funciones se consideran diferentes si se crean en momentos diferentes.
- Intenta evitar objetos y funciones como dependencias. Muévelos fuera del componente o dentro del Efecto.
Desafío 1 de 4: Arregla un intervalo que se reinicia
Este Efecto configura un intervalo que hace tictac cada segundo. Has notado que algo extraño pasa: parece que el intervalo es destruido y recreado en cada tic. Arregla el código para que el interval no sea recreado constantemente.
import { useState, useEffect } from 'react'; export default function Timer() { const [count, setCount] = useState(0); useEffect(() => { console.log('✅ Creating an interval'); const id = setInterval(() => { console.log('⏰ Interval tick'); setCount(count + 1); }, 1000); return () => { console.log('❌ Clearing an interval'); clearInterval(id); }; }, [count]); return <h1>Counter: {count}</h1> }