Intenté traducir un código de ejemplo de class a functional component y enfrenté el problema.
el archivo de destino está en components/Wheel/index.js
Función clave que causa el problema
const selectItem = () => { if (selectedItem === null) { const selectedItem = Math.floor(Math.random() * items.length); console.log(selectedItem); setSelectedItem(selectedItem); } else { setSelectedItem(null); let t= setTimeout(() => { selectItem() }, 500); clearTimeout(t); } }; La primera vez es normal, a partir de la segunda vez, se necesitan 2 clics para que la rueda gire. Tuve que agregar clearTimeout() o se generó un bucle infinito , pero no sucede lo mismo en el original.
Ejemplo de trabajo original en class
Mi versión en functional component .
Gracias.
Qué excelente matiz de ganchos que has descubierto. Cuando llama a selectItem en el tiempo de espera, el valor de selectedItem que se captura en el ámbito léxico es el último valor (no nulo).
Hay dos respuestas, una respuesta simple y una mejor respuesta de trabajo.
La respuesta simple es que puede lograrlo simplemente separando las funciones: https://codesandbox.io/s/spinning-wheel-game-forked-cecpi
Se parece a esto:
const doSelect = () => { setSelectedItem(Math.floor(Math.random() * items.length)); }; const selectItem = () => { if (selectedItem === null) { doSelect(); } else { setSelectedItem(null); setTimeout(doSelect, 500); } };Ahora, sigue leyendo si te atreves.
La respuesta complicada corrige la solución del problema si items.length puede cambiar entre el momento en que se configura un temporizador y se dispara:
https://codesandbox.io/s/spinning-wheel-game-bifurcado-wmeku
Volver a renderizar (es decir, configurar el estado) en un tiempo de espera causa complejidad: si el componente se volvió a renderizar entre el tiempo de espera, entonces su devolución de llamada podría haber capturado accesorios/estado "obsoletos". Así que están pasando muchas cosas aquí. Intentaré describirlo lo mejor que pueda:
const [selectedItem, setSelectedItem] = useState(null); // we're going to use a ref to store our timer const timer = useRef(); const { items } = props; // this is just the callback that performs a random select // you can see it is dependent on items.length from props const doSelect = useCallback(() => { setSelectedItem(Math.floor(Math.random() * items.length)); }, [items.length]); // this is the callback to setup a timeout that we do // after the user has clicked a "second" time. // it is dependent on doSelect const doTimeout = useCallback(() => { timer.current = setTimeout(() => { doSelect(); timer.current = null; }, 500); }, [doSelect]); // Here's the tricky thing: if items.length changes in between // the time we rerender and our timer fires, then the timer callback will have // captured a stale value for items.length. // The way we fix this is by using this effect. // If items.length changes and there is a timer in progress we need to: // 1. clear it // 2. run it again // // In a perfect world we'd be capturing the amount of time remaining in the // timer and fire it exactly (which requires another ref) // feel free to try and implement that! useEffect(() => { if (!timer.current) return; clearTimeout(timer.current); doTimeout(); // it's safe to ignore this warning because // we know exactly what the dependencies are here }, [items.length, doTimeout]); const selectItem = () => { if (selectedItem === null) { doSelect(); } else { setSelectedItem(null); doTimeout(); } };