Uso useEffect() para obtener una instantánea de Firestore y, en paralelo, quiero contar un valor:
const [counter, setCounter] = useState({ mona: 0, phil: 0 }); useEffect(() => { onSnapshot(q, (snapshop) => { setTasks( snapshop.docs.map((doc) => { if (doc.data().wer === "Mona") { console.log("Mona + 1"); // This get's executed as expected (eg 3 times) setCounter({ ...counter, mona: counter.mona + 1 }); } if (doc.data().wer === "Phil") { console.log("Phil + 1"); // This get's executed as expected (eg 6 times) setCounter({ ...counter, phil: counter.phil + 1 }); } return { ...doc.data(), id: doc.id, timestamp: doc.data().timestamp?.toDate().getTime(), }; }) ); setLoading(false); }); }, []); useEffect(() => { console.log({ counter }); //this get's executed only 2 times. }, [counter]); Cuando console.log() dentro de map() se ejecuta correctamente, ¿por qué setCounter no ejecuta o actualiza el contador correctamente?
El console.log({ counter }); por cierto no da nada más que:
{counter: {mona: 0, phil: 0}} {counter: {mona: 0, phil: 1}}La función que pasa a useEffect se cierra sobre la variable counter .
Cuando llamas a setCounter , actualiza el counter en la tienda y el enlace se vuelve a representar. El gancho de efectos no vuelve a ejecutarse porque ninguna de las dependencias ( [] - no hay ninguna) ha cambiado.
La próxima vez que se active el controlador de eventos configurado por onSnapshot , utilizará el mismo valor de counter que la vez anterior. Esto significa que counter.phil sigue siendo 0 dentro del gancho de efectos. Agrega 1 a 0 nuevamente y llama a setCounter pero este valor es el mismo que el valor anterior.
Dado que el counter no ha cambiado esta vez, el segundo gancho de efecto que depende del valor del counter no se activa.
Pase una función a setCounter para obtener el valor más reciente en lugar del valor cerrado original:
setCounter((latestCounter) => { ...latestCounter, phil: latestCounter.phil + 1 });React a veces actualiza por lotes el estado. Lo que significa que todas sus llamadas a setCounter solo activan un efecto.
Además, el valor del counter dentro de su función también se actualiza al final de la función, por lo tanto, está perdiendo actualizaciones.
Lo que debes hacer:
En primer lugar, pase una devolución de llamada a setCounter en lugar de usar el valor de counter . Así que cambia:
setCounter({ mona: counter.mona, phil: counter.phil + 1 });a:
setCounter(counter => ({ mona: counter.mona, phil: counter.phil + 1 })); Para forzar la llamada a useEffect varias veces, debe optar por no recibir actualizaciones por lotes usando ReactDOM.flushSync :
import { flushSync } from 'react-dom'; // ... flushSync(() => setCounter(counter => ({ mona: counter.mona, phil: counter.phil + 1 }))); De esta manera, su useEffect debe llamarse para cada cambio del contador. Obviamente, esto es menos eficiente que tener las actualizaciones por lotes.
Dado que está recargando todo el conjunto de datos cada vez que desea volver a contar todo en cada llamada a onSnapshot en lugar de simplemente modificar el valor actual.
En ese caso puedes hacer esto:
const newCounter = { mona: 0, phil: 0}; snapshop.docs.map((doc) => { if (doc.data().wer === "Mona") { console.log("Mona + 1"); // This get's executed as expected (eg 3 times) newCounter.mona += 1; } if (doc.data().wer === "Phil") { console.log("Phil + 1"); // This get's executed as expected (eg 6 times) newCounter.phil += 1; } // ... }); setCounter(newCounter); Así que simplemente calcula el resultado y llama a setCounter una vez fuera del ciclo con el conteo final. En este caso, no necesita leer el estado anterior ya que lo vuelve a calcular desde cero.
Podría mantener el código anterior y agregar un setCounter({mona: 0, phil: 0}) fuera del ciclo, pero creo que sería menos eficiente que calcular los valores fuera de los ganchos de reacción y solo llamar al setCounter una vez.