Mi aplicación React usa setTimeout() y setInterval() . Dentro de ellos, necesito acceder al valor del estado. Como sabemos, los cierres están vinculados a su contexto una vez creados, por lo que usar valores de estado en setTimeout() / setInterval() no usará el valor más nuevo.
Simplifiquemos las cosas y digamos que mi componente se define como tal:
import { useState, useEffect, useRef } from 'react'; const Foo = () => { const [number, setNumber] = useState(0); const numberRef = useRef(number); // Is this common? Any pitfalls? Can it be done better? numberRef.current = number; useEffect( () => setInterval( () => { if (numberRef.current % 2 === 0) { console.log('Yay!'); } }, 1000 ), [] ); return ( <> <button type="button" onClick={() => setNumber(n => n + 1)}> Add one </button> <div>Number: {number}</div> </> ); };En total se me ocurrieron 3 ideas de cómo lograr esto, ¿alguna de ellas es un patrón reconocido?
Asignando valor de estado a ref en cada renderizado, como arriba:
numberRef.current = number;El beneficio es un código muy simplista.
Usando useEffect() para registrar cambios de number :
useEffect( () => numberRef.current = number, [number] );Este parece más React-ish, pero ¿es realmente necesario? ¿No degrada realmente el rendimiento cuando se puede usar una asignación simple del punto n.º 1?
Usando un setter personalizado:
const [number, setNumberState] = useState(0); const numberRef = useRef(number); const setNumber = value => { setNumberState(value); numberRef.current = value; };¿Tener el mismo valor en el estado y la referencia es un patrón común con React? ¿Y alguna de estas 3 formas es más popular que otras por alguna razón? ¿Cuáles son las alternativas?
2021-10-17 EDITAR:
Dado que esto parece un escenario común, quería envolver toda esta lógica en un intuitivo
useInterval( () => console.log(`latest number value is: ${number}`), 1000 ) donde el parámetro useInterval siempre puede "acceder" al último estado.
Después de jugar un poco en CodeSandbox , me di cuenta de que no hay forma de que alguien más no haya pensado en una solución para esto.
He aquí, el hombre mismo, Dan Abramov, tiene una publicación de blog con una solución precisa para nuestra pregunta https://overreacted.io/making-setinterval-declarative-with-react-hooks/
Recomiendo encarecidamente leer el blog completo, ya que describe un problema general con la falta de coincidencia entre la programación React declarativa y las API imperativas. Dan también explica su proceso (paso a paso) de desarrollar una solución completa con la capacidad de cambiar el retraso del intervalo cuando sea necesario.
Aquí (CodeSandbox) puedes probarlo en tu caso particular.
Respuesta ORIGINALES:
1.
numberRef.current = number;Evitaría esto ya que generalmente queremos hacer actualizaciones de estado/ref en useEffect en lugar del método render.
En este caso particular, no tiene mucho impacto, sin embargo, si agregara otro estado y lo modificara -> se desencadenaría un ciclo de renderizado -> este código también se ejecutaría y asignaría un valor sin motivo (valor numérico no cambiaría).
2.
useEffect( () => numberRef.current = number, [number] );En mi humilde opinión, esta es la mejor manera de salir de las 3 formas que proporcionaste. Esta es una forma limpia/declarativa de "sincronizar" el estado administrado con el objeto ref mutable.
3.
const [number, setNumberState] = useState(0); const numberRef = useRef(number); const setNumber = value => { setNumberState(value); numberRef.current = value; }; En mi opinión, esto no es lo ideal. Otros desarrolladores están acostumbrados a React API y es posible que no vean su configurador personalizado y, en su lugar, usen un setNumberState predeterminado al agregar más lógica esperando que se use como una "fuente de la verdad" -> setInterval no obtendrá los datos más recientes.
Simplemente se ha olvidado de borrar el intervalo. Tienes que borrar el intervalo de renderizado.
useEffect(() => { const id = setInterval(() => { if (numberRef.current % 2 === 0) { console.log("Yay!"); } }, 1000); return () => clearInterval(id); }, []);Si no borra, esto seguirá creando un nuevo setInterval con cada clic. Eso puede conducir a un comportamiento no deseado.
Código simplificado:
const Foo = () => { const [number, setNumber] = useState(0); useEffect(() => { const id = setInterval(() => { if (number % 2 === 0) { console.log("Yay!"); } }, 1000); return () => clearInterval(id); }, [number]); return ( <div> <button type="button" onClick={() => setNumber(number + 1)}> Add one </button> <div>Number: {number}</div> </div> ); };