You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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;

解决方案

问题根源

  1. 闭包捕获 stale 值:Timer组件的useEffect回调捕获了初始的seconds值,每次调用setSeconds(seconds - 1)时始终使用第一次渲染的40,导致数值不更新。
  2. 未清理旧定时器:useEffect依赖项为空,每次组件渲染都会创建新定时器,旧定时器未正确清理,逻辑混乱。
  3. restartTimer状态未重置:App组件中playAgain设置restartTimer=true后未重置为false,导致Timer组件的重置逻辑无法触发后续更新。
  4. 定时器实例跨组件管理错误:从父组件传递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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.09 21:05:17