React游戏倒计时计时器二次运行不递减问题求助
问题
开发带关卡的游戏时,首次游戏结束后,第二次运行倒计时计时器无法正常递减。计时器会在restartTimer状态设为true时重新初始化,但二次运行仍不递减。
App.js 组件
import "./App.css"; import React, { useState, useEffect, useCallback } from "react"; import SingleCard from "./components/singleCard/SingleCard"; import Timer from "./components/timer/Timer"; import Modal from "./components/modal/Modal"; import soundOn from "./images/soundon.png"; import soundOff from "./images/soundoff.png"; import { Helmet } from "react-helmet"; const cardImages = [ { src: "/img/img-1.png", matched: false }, { src: "/img/img-2.png", matched: false }, { src: "/img/img-3.png", matched: false }, { src: "/img/img-4.png", matched: false }, { src: "/img/img-5.png", matched: false }, { src: "/img/img-6.png", matched: false }, ]; function App({ background, wrongAns, correctAns, deadSound, winSound }) { const [cards, setCards] = useState([]); const [turns, setTurns] = useState(0); const [choiceOne, setChoiceOne] = useState(null); const [choiceTwo, setChoiceTwo] = useState(null); const [disabled, setDisabled] = useState(false); const [isgameEnd, setGameEnd] = useState(false); const [timerStart, setTimerStart] = useState(false); const [playSound, setPlaySound] = useState(false); const [count, setCount] = useState(0); const [IsPlaying, setIsPlaying] = useState(true); const [isModalOpen, setModalIsOpen] = useState(false); const [restartTimer, setRestartTimer] = useState(false); const [isMute, setMute] = useState(false); const [loading, setLoading] = useState(true); function handleMute(state = false) { background.muted = state; wrongAns.muted = state; correctAns.muted = state; deadSound.muted = state; winSound.muted = state; setMute(state); } let timer; const toggleModal = () => { setModalIsOpen(true); }; const handlePlaySound = () => { setPlaySound(false); }; function AudioBgOnPause() { if (playSound === true) { background.pause(); setIsPlaying(false); } } function AudioBgOnResume() { if (IsPlaying === false) { setIsPlaying(true); } } window.AudioBgOnPause = AudioBgOnPause; window.AudioBgOnResume = AudioBgOnResume; if (playSound === false) { background.pause(); } else if (playSound === true && IsPlaying === true) { background.play(); } const playAgain = () => { shuffleCards(); setTurns(0); setChoiceOne(null); setChoiceTwo(null); setDisabled(false); setGameEnd(false); setTimerStart(false); setPlaySound(false); setCount(0); setIsPlaying(true); setModalIsOpen(false); setRestartTimer(true); setMute(false); }; const restartGame = () => { playAgain(); }; useEffect(() => { if (turns < 6 && isgameEnd === true) { setDisabled(true); setTimerStart(false); clearInterval(timer); if (playSound === true) { deadSound.play(); } setPlaySound(false); setTimeout(() => { toggleModal(); }, 2000); } }, [turns, isgameEnd]); useEffect(() => { if ( (turns === 6 && isgameEnd === false) || (turns === 6 && isgameEnd === true) ) { setRestartTimer(true); setTimerStart(false); if (playSound === true) { winSound.play(); } setPlaySound(playSound); shuffleCards(); } }, [turns, isgameEnd]); const shuffleCards = () => { const shuffleCards = [...cardImages, ...cardImages] .sort(() => Math.random() - 0.5) .map((card) => ({ ...card, id: Math.random() })); setCards(shuffleCards); setTurns(0); }; const handleChoice = (card) => { setTimerStart(true); background.loop = true; count === 1 ? setPlaySound(true) : setPlaySound(playSound); setCount(count + 1); choiceOne ? setChoiceTwo(card) : setChoiceOne(card); }; useEffect(() => { if (choiceOne && choiceTwo) { setDisabled(true); if (choiceOne.src === choiceTwo.src) { setCards((prevCards) => { return prevCards.map((card) => { if (card.src === choiceOne.src) { return { ...card, matched: true }; } else { return card; } }); }); if (playSound === true) { correctAns.play(); } setTurns((prevTurns) => prevTurns + 1); resetTurn(); } else { if (playSound === true) { wrongAns.play(); } setTimeout(() => resetTurn(), 500); } } }, [choiceOne, choiceTwo]); useEffect(() => { shuffleCards(); setCount(count + 1); }, []); const resetTurn = () => { setChoiceOne(null); setChoiceTwo(null); setDisabled(false); }; const onGameEnd = useCallback(() => { setGameEnd(!isgameEnd); }, [isgameEnd]); useEffect(() => { setTimeout(() => { setLoading(false); }, 4000); }, []); return ( <> <Helmet> <meta charSet="utf-8" /> <title>Match Maker</title> <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" /> </Helmet> <div className="App"> {loading && ( <div className="loader-container"> <div className="spinner"></div> </div> )} <> <div style={ loading ? { visibility: "hidden" } : { visibility: "inherit" } } > <div className="soundBtn"> {!isMute === true ? ( <div className="soundIcon" style={{ cursor: "pointer", khtmlUserSelect: "none", MozUserSelect: "none", OUserSelect: "none", userSelect: "none", }} onClick={() => handleMute(!isMute)} > <img src={soundOn} alt="soundOff" /> </div> ) : ( <div className="soundIcon" style={{ cursor: "pointer", khtmlUserSelect: "none", MozUserSelect: "none", OUserSelect: "none", userSelect: "none", }} onClick={() => handleMute(!isMute)} > <img src={soundOff} alt="soundOff" /> </div> )} </div> <div className="card-grid"> {cards.map((card) => ( <SingleCard key={card.id} card={card} handleChoice={handleChoice} flipped={ card === choiceOne || card === choiceTwo || card.matched } disabled={disabled} isModalOpen={isModalOpen} /> ))} </div> <div className="TimerAndTurnsInfo"> <Timer timerStart={timerStart} timer={timer} onGameEnd={onGameEnd} restartTimer={restartTimer} /> <p>matched {turns}</p> </div> </div> </> </div> {isModalOpen && ( <Modal handlePlaySound={handlePlaySound} restartGame={restartGame} /> )} </> ); } export default App;
Timer 组件
import React, { useEffect, useState } from "react"; import "./Timer.css"; const Child = ({ timerStart, timer, onGameEnd, restartTimer }) => { const [seconds, setSeconds] = useState(40); useEffect(() => { if (restartTimer === true) { setSeconds(40); } }, [seconds, restartTimer]); useEffect(() => { if (timerStart === true) { timer = setInterval(() => { if (seconds > 0) { setSeconds(seconds - 1); } if (seconds === 0) { onGameEnd(true); clearInterval(timer); } }, 1000); } return () => clearInterval(timer); }); return ( <p className="time"> Time{" "} <span className="span1"> <span>{seconds}s</span> </span> </p> ); }; const Timer = React.memo(({ timerStart, timer, onGameEnd, restartTimer }) => ( <Child timerStart={timerStart} timer={timer} onGameEnd={onGameEnd} restartTimer={restartTimer} /> )); export default Timer;
解决方案
问题根源
- 闭包捕获 stale 值:Timer组件的useEffect回调捕获了初始的
seconds值,每次调用setSeconds(seconds - 1)时始终使用第一次渲染的40,导致数值不更新。 - 未清理旧定时器:useEffect依赖项为空,每次组件渲染都会创建新定时器,旧定时器未正确清理,逻辑混乱。
- restartTimer状态未重置:App组件中
playAgain设置restartTimer=true后未重置为false,导致Timer组件的重置逻辑无法触发后续更新。 - 定时器实例跨组件管理错误:从父组件传递
timer变量到子组件,无法正确维护定时器实例。
修复步骤
1. 给App组件添加restartTimer重置逻辑
在App组件中新增以下useEffect,确保restartTimer触发后能重置为false,保证下次重启有效:
useEffect(() => { if (restartTimer) { setRestartTimer(false); } }, [restartTimer]);
2. 修改Timer组件,修复闭包与定时器管理
import React, { useEffect, useState } from "react"; import "./Timer.css"; const Child = ({ timerStart, onGameEnd, restartTimer }) => { const [seconds, setSeconds] = useState(40); let timer = null; // 处理定时器重置 useEffect(() => { if (restartTimer) { setSeconds(40); } }, [restartTimer]); // 处理定时器启动/停止 useEffect(() => { if (timerStart) { timer = setInterval(() => { setSeconds(prevSeconds => { const newSeconds = prevSeconds - 1; if (newSeconds === 0) { onGameEnd(true); clearInterval(timer); } return newSeconds; }); }, 1000); } else { if (timer) { clearInterval(timer); } } return () => { if (timer) { clearInterval(timer); } }; }, [timerStart, onGameEnd]); return ( <p className="time"> Time{" "} <span className="span1"> <span>{seconds}s</span> </span> </p> ); }; const Timer = React.memo(({ timerStart, onGameEnd, restartTimer }) => ( <Child timerStart={timerStart} onGameEnd={onGameEnd} restartTimer={restartTimer} /> )); export default Timer;
额外优化
- 移除App组件传递给Timer的
timer参数,Timer内部自行管理定时器实例,避免跨组件状态混乱。 - App组件
playAgain中设置timerStart=false确保旧定时器停止,用户点击卡片时再设置timerStart=true启动新定时器。
内容的提问来源于stack exchange,提问作者Asad Ur Rehman
相关产品推荐
相关产品推荐

