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, 4 meses, 25 días - Por bytebard


3 Respuestas:

  • Hay dos recomendaciones que tendría para lograr esto:

    1. Mueva el callback dentro del useEffect para evitar re-crear la función cada render
    2. 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

  • El problema que estás experimentando se debe a que al usar useState como dependencia en el useEffect, 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 con useRef 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 el useEffect.

    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 al useEffect, puedes crear una función de devolución de llamada que use el valor actual de magazinesCounter:

    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 de magazinesCounter, 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 y handleMagazinesChanges). Esto asegura que el oyente se elimine y se vuelva a crear con la versión más reciente de magazinesCounter.

    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