I am having troubles fixing a bug basically I need to iterate over a list of buttons and apply an animation and on the next iteration I remove the animation from the previous element, however, when running the code the animation is started twice at the beginning and one element remains stuck with the animation applied.
The following is the code of the component:
import type { NextPage } from 'next'
import Head from 'next/head'
import { useRouter } from 'next/router'
import { useEffect, useRef, useState } from 'react'
import { IoCheckmark, IoClose, IoHome, IoRefresh } from 'react-icons/io5'
import Page from '../components/page/Page'
import styles from '../styles/Play.module.css'
import { distance } from '../utils/distance'
import { randomInt, randomFloat } from '../utils/random'
function ShowSequence(props: any) {
const [index, setIndex] = useState(0);
const [sequence, setSequence] = useState(props.sequence);
const [timer, setTimer] = useState<any>();
useEffect(() => {
console.log(index)
if (index > 0) document.getElementById(sequence[index - 1])?.classList.toggle(styles.animate);
if (index < sequence.length) document.getElementById(sequence[index])?.classList.toggle(styles.animate);
else return clearInterval(timer);
setTimer(setTimeout(() => setIndex(index 1), 3000));
}, [index]);
return <div className={styles.button}>
{
props.map ? props.map.map((button: any) => {
return <button key={button.buttonId} className={styles.button} id={button.buttonId} style={{ top: button.y "px", left: button.x "px", backgroundColor: button.color }}></button>
}) : null
}
</div>;
}
function DoTask(props: any) {
return <div>
</div>;
}
function ChooseSequence(props: any) {
const [sequence, setSequence] = useState(props.sequence);
const [index, setIndex] = useState(0);
const [timer, setTimer] = useState<any>();
const [buttonMap, setButtonMap] = useState<any>({});
console.log(sequence);
return <div className={styles.button}>
{
props.map ? props.map.map((button: any) => {
return <button key={button.buttonId} className={styles.button} id={button.buttonId} style={{ top: button.y "px", left: button.x "px", backgroundColor: button.color }} onClick={(e) => {
let correctSequence = sequence[index] === button.buttonId;
e.currentTarget.classList.toggle(correctSequence ? styles.correctButton : styles.wrongButton);
buttonMap[button.buttonId] = correctSequence ? <IoCheckmark size={20} color={"white"}></IoCheckmark> : <IoClose size={20} color={"white"}></IoClose>;
setButtonMap(buttonMap);
setIndex(index 1);
}}>
{ (buttonMap[button.buttonId]) ? buttonMap[button.buttonId] : button.buttonId }
</button>
}) : null
}
</div>;
}
function Error(props: any) {
return <div className={styles.errorMenu}>
<h1>You lost!</h1>
<p>You reached level: {props.level}</p>
<div className={styles.container}>
<div className={styles.item}></div>
<div className={styles.item}></div>
<div className={styles.item}></div>
<div className={styles.item}></div>
<div className={styles.item}></div>
<div className={styles.item}></div>
<div className={styles.item}></div>
<div className={styles.item}></div>
<div className={styles.item}></div>
<div className={styles.item}></div>
</div>
<div className={styles.row}>
<button className={styles.retryButton} onClick={() => window.location.href = "/play"}><IoRefresh></IoRefresh></button>
<button className={styles.closeButton} onClick={() => window.location.href = "/"}><IoHome></IoHome></button>
</div>
</div>;
}
enum State {
SHOWSEQUENCE,
DOTASK,
CHOOSESEQUENCE,
ERROR
}
const Play: NextPage = () => {
let [state, setState] = useState<State>(State.SHOWSEQUENCE);
let [sequence, setSequence] = useState<number[]>([randomInt(1, 20), randomInt(1, 20), randomInt(1, 20), randomInt(1, 20)]);
let [map, setMap] = useState<any[]>();
let [level, setLevel] = useState(1);
let component;
useEffect(() => {
if (state === State.SHOWSEQUENCE) {
let newSequenceId = randomInt(1, 20);
setSequence((prevSequence: number[]) => [...prevSequence, newSequenceId])
}
}, [state]);
useEffect(() => {
let buttonIds = Array.from({ length: 20 }, (v, k) => k 1);
const { innerWidth, innerHeight } = window;
let colors: string[] = ["#c0392b", "#e67e22", "#27ae60", "#8e44ad", "#2c3e50"];
let buttonMap: any[] = [];
let rows = buttonIds.length / 10;
let columns = rows > 0 ? buttonIds.length / rows : buttonIds.length;
for (let row = 0; row < rows; row ) {
for (let col = 0; col < columns; col ) {
let color = colors[Math.floor(randomFloat() * colors.length)];
let x = innerWidth / columns * col 100;
let y = innerHeight / rows * row 100;
let offsetX = (randomFloat() < .5) ? -1 : 1 * randomFloat() * ((innerWidth / columns) - 100);
let offsetY = (randomFloat() < .5) ? -1 : 1 * randomFloat() * ((innerHeight / rows) - 100);
if (x offsetX 100 > innerWidth) offsetX -= ((x offsetX) - innerWidth) 100;
if (y offsetY 100 > innerHeight) offsetY -= ((y offsetY) - innerHeight) 100;
buttonMap.push({ buttonId: buttonIds[row * columns col], x: x offsetX, y: y offsetY, color })
}
}
setMap(buttonMap);
}, [])
switch (state) {
case State.SHOWSEQUENCE:
component = <ShowSequence map={map} sequence={sequence} changeState={() => setState(State.DOTASK)}></ShowSequence>;
break;
case State.DOTASK:
component = <DoTask changeState={() => setState(State.CHOOSESEQUENCE)} one rror={() => setState(State.ERROR)}></DoTask>
break;
case State.CHOOSESEQUENCE:
component = <ChooseSequence map={map} sequence={sequence} changeState={() => setState(State.SHOWSEQUENCE)} one rror={() => setState(State.ERROR)}></ChooseSequence>
break;
}
return (
<Page color="blue">
{ state === State.ERROR ? <Error level={level}></Error> : null }
{component}
</Page>
)
}
export default Play
CodePudding user response:
To cleanup the timer in the useEffect, you must return a function.
The previous element index is 1 less the current index for non-zero indexes or the last element in the array of buttons when the current index is the first item.
const prevElIndex = index == 0 ? props.map.length - 1 : index - 1;
Also, you need to check if the previous element has the animation class before toggling the class. This takes care of the first time the animation starts to run (the previous element would not have the animation class).
if (
document
.getElementById(sequence[prevElIndex])
?.classList.contains(styles.animate)
) {
document
.getElementById(sequence[prevElIndex])
?.classList.toggle(styles.animate);
}
Altogether, your effect would be along these lines:
const [index, setIndex] = useState(0);
const [sequence, setSequence] = useState(props.sequence);
const [timer, setTimer] = useState<number>();
useEffect(() => {
const prevElIndex = index == 0 ? props.map.length - 1 : index - 1;
if (
document
.getElementById(sequence[prevElIndex])
?.classList.contains(styles.animate)
) {
document
.getElementById(sequence[prevElIndex])
?.classList.toggle(styles.animate);
}
document.getElementById(sequence[index])?.classList.toggle(styles.animate);
setTimer(setTimeout(() => setIndex((index 1) % sequence.length), 3000));
return () => clearTimeout(timer);
}, [index]);
A working Stackblitz showing this in action.