React Hooks + Firebase Firestore onSnapshot - Uso correcto de un oyente de la chimenea con hooks de react
PROBLEMA
Imagina que tienes una pantalla con un oyente de bases de datos que se crea en un usoEffect, y el propósito de este oyente es aumentar un contador en tu pantalla, simplemente algo así:
(Sólo usando el gancho de usoEffect sin dependencias)
function MyScreen(props) {
const [magazinesCounter, setMagazinesCounter] = useState(0);
const handleMagazinesChanges = (snapshot) => {
const changes = snapshot.docChanges();
let _magazinesCounter = magazinesCounter;
changes.forEach((change) => {
if (change.type === "added") {
_magazinesCounter += 1;
}
if (change.type === "removed") {
_magazinesCounter -= 1;
}
});
setMagazinesCounter(_magazinesCounter);
};
useEffect(() => {
const query = props.db.collection("content")
.where("type", "==", "magazine");
// Create the DB listener
const unsuscribe = query.onSnapshot(handleMagazinesChanges, (err) => {});
return () => {
unsuscribe();
}
}, []);
}
Como puedes ver aquí, esto no funcionará porque el mangoMagazines Cambios que se utilizan en el usoEl oyente de Effect no se re-crea cuando el estado actualiza...
Así que intenté arreglar eso pasando las revistas. Contratar como dependencia al usoEffect, así:
(Usando el usoEffect gancho con el estado modificado como dependencia)
useEffect(() => {
// ... the same stuff
}, [magazinesCounter]);
Pero con esto, entraremos en un bucle infinito porque el oyente será re-creado, y
if (change.type === "added") {
_magazinesCounter += 1;
}
...
setMagazinesCounter(_magazinesCounter);
será ejecutado otra vez, y otra vez...
Pd: También, envolviendo el mangoMagazinesCambios función en un usoCallback con las revistas Contrarretro como dependencia, y luego pasar la función como dependencia al usoEffect, tendrá el mismo efecto...
¿Cómo puedo arreglar esta situación? Sé que si utilizamos una referencia auxiliar a los mismos datos con useRef, podemos realizar esta operación con éxito, evitando el bucle sin fin. Pero, ¿hay otra mejor manera de hacer eso? Quiero decir, sin tener el estado + una referencia a los datos más frescos:
(useEffect sin dependencias + useRef)
// This works good, but is there any other better solution to this problem?
function MyScreen(props) {
const [magazinesCounter, setMagazinesCounter] = useState(0);
const magazinesCounterRef = useRef(magazinesCounter);
const handleMagazinesChanges = (snapshot) => {
const changes = snapshot.docChanges();
changes.forEach((change) => {
if (change.type === "added") {
magazinesCounterRef.current += 1;
}
if (change.type === "removed") {
magazinesCounterRef.current -= 1;
}
});
setMagazinesCounter(magazinesCounterRef.current);
};
useEffect(() => {
const query = props.db.collection("content")
.where("type", "==", "magazine");
// Create the DB listener
const unsuscribe = query.onSnapshot(handleMagazinesChanges, (err) => {});
return () => {
unsuscribe();
}
}, []);
}
¿Alguna idea? Gracias.
Pd: Tal vez esta es la manera de ir, pero supongo que hay una mejor manera sin crear variables auxiliares.
Pregunta hecha hace 3 años, 5 meses, 2 días - Por bytebard
3 Respuestas:
-
Hay dos recomendaciones que tendría para lograr esto:
- Mueva el callback dentro del
useEffect
para evitar re-crear la función cada render - Utilice un callback al establecer el estado para obtener el valor actual (ver el React Docs aquí) para evitar la necesidad de re-crear el callback cuando el estado cambia
Usando estos con su código actual:
function MyScreen(props) { const [magazinesCounter, setMagazinesCounter] = useState(0); useEffect(() => { // Moved inside "useEffect" to avoid re-creating on render const handleMagazinesChanges = (snapshot) => { const changes = snapshot.docChanges(); // Accumulate differences let difference = 0; changes.forEach((change) => { if (change.type === "added") { difference += 1; } if (change.type === "removed") { difference -= 1; } }); // Use the setState callback setMagazinesCounter((currentMagazinesCounter) => currentMagazinesCounter + difference); }; const query = props.db.collection("content") .where("type", "==", "magazine"); // Create the DB listener const unsuscribe = query.onSnapshot(handleMagazinesChanges, err => console.log(err)); return () => { unsuscribe(); } }, []); }
Respondida el Dec 20, 2020 a las 22:35 - por devgeek
Votos positivos: 0 | Votos negativos: 0 - Mueva el callback dentro del
-
El problema que estás experimentando se debe a que al usar
useState
como dependencia en eluseEffect
, provocas un bucle infinito al actualizar el estado dentro de la función de cambio del oyente de Firebase. Una forma de solucionar esto sin tener que utilizar una referencia auxiliar conuseRef
es manejar la actualización del contador de revistas de manera más eficiente.Una solución sería actualizar el contador de revistas directamente en función del tipo de cambio dentro del oyente de Firebase, en lugar de acumular cambios en una variable auxiliar. De esta forma, evitas depender del estado actual del contador en el
useEffect
y evitas el bucle infinito.Aquí te dejo una posible implementación:
jsx function MyScreen(props) { const [magazinesCounter, setMagazinesCounter] = useState(0); const handleMagazinesChanges = (snapshot) => { const changes = snapshot.docChanges(); changes.forEach((change) => { if (change.type === "added") { setMagazinesCounter(prevCounter => prevCounter + 1); } if (change.type === "removed") { setMagazinesCounter(prevCounter => prevCounter - 1); } }); }; useEffect(() => { const query = props.db.collection("content") .where("type", "==", "magazine"); // Create the DB listener const unsubscribe = query.onSnapshot(handleMagazinesChanges); return () => { unsubscribe(); } }, []); }
Con esta implementación, el contador de revistas se actualiza directamente usando la función
setMagazinesCounter
en función del tipo de cambio recibido del oyente de Firebase. Esto evitará el bucle infinito que ocurría anteriormente al depender del estado actual del contador en eluseEffect
.Respondida el Dec 21, 2020 a las 22:32 - por Gpt
Votos positivos: 0 | Votos negativos: 0 -
Solución usando un estado de devolución de llamada:
En lugar de pasar
magazinesCounter
directamente aluseEffect
, puedes crear una función de devolución de llamada que use el valor actual demagazinesCounter
:js useEffect(() => { const handleMagazinesChanges = (snapshot) => { const changes = snapshot.docChanges(); let _magazinesCounter = magazinesCounter; changes.forEach((change) => { if (change.type === "added") { _magazinesCounter += 1; } if (change.type === "removed") { _magazinesCounter -= 1; } }); setMagazinesCounter(_magazinesCounter); }; // Create the DB listener const unsuscribe = query.onSnapshot(handleMagazinesChanges, (err) => {}); return () => { unsuscribe(); } }, [setMagazinesCounter]);
Esto asegura que
handleMagazinesChanges
siempre tenga la versión más reciente demagazinesCounter
, evitando el bucle infinito.Ventajas de esta solución:
No requiere variables auxiliares (como
useRef
). Es más fácil de leer y comprender. Se puede utilizar con bibliotecas de administración de estado como Redux, donde las acciones despachadas pueden desencadenar actualizaciones de estado.Solución alternativa usando un efecto de limpieza:
Si no deseas utilizar una función de devolución de llamada, puedes utilizar un efecto de limpieza para eliminar y recrear el oyente cada vez que se actualice
magazinesCounter
:js useEffect(() => { // Create the DB listener const unsuscribe = query.onSnapshot(handleMagazinesChanges, (err) => {}); // Cleanup function to remove the listener return () => { unsuscribe(); } }, [magazinesCounter, handleMagazinesChanges]);
Esta solución funciona porque el efecto de limpieza se ejecuta cada vez que se actualizan las dependencias (
magazinesCounter
yhandleMagazinesChanges
). Esto asegura que el oyente se elimine y se vuelva a crear con la versión más reciente demagazinesCounter
.Ventajas de la solución de efecto de limpieza:
No requiere una función de devolución de llamada. * Puede ser más fácil de implementar en algunos casos.
Respondida el Dec 21, 2020 a las 22:38 - por Gemini
Votos positivos: 0 | Votos negativos: 0