【问题标题】:React - How do I start a timer, then navigate a different route, then come back and have the timer still in sync with its original countdown?React - 如何启动计时器,然后导航不同的路线,然后返回并让计时器仍与其原始倒计时同步?
【发布时间】:2026-01-09 10:40:01
【问题描述】:

标题确实说明了一切。我已经看到一些关于如何防止组件卸载的问题,但我不确定这正是我需要的(或者至少不是我需要的全部)。

现在我从后台线程更新计时器,这样当浏览器选项卡进入后台时,它就不会进入睡眠状态。但我也希望它在我转到应用程序的不同屏幕时继续计数。 (我正在制作一个时间跟踪应用程序)。

谁能告诉我如何处理这个问题?

谢谢

我完整的clock.js - 不幸的是它现在嵌套在其他一些组件中,将逻辑提升到*组件将是一件苦差事。

时钟与后台任务结合使用,我已将其发布在其下方。

import React, { useEffect, useState, useRef, useContext } from 'react'
import './Clock.css'
import useSound from 'use-sound';
import bark from '../../Sounds/bong.wav';
import tweet from '../../Sounds/bong.wav';
import gong from '../../Sounds/opening_gong.wav'
import SkipNext from '@material-ui/icons/SkipNextOutlined';
import Stop from '@material-ui/icons/StopOutlined';
import PausePresentation from '@material-ui/icons/PauseOutlined';
import PlayCircleOutline from '@material-ui/icons/PlayArrowOutlined';
import MainContext from '../../MainContext'
import myWorker from '../../test.worker';



function Clock(props) {

  const context = useContext(MainContext)
  let breakPrefs = context.prefs

  const [timer, setTimer] = useState({
    onBreak: false,
    firstPageLoad: true,
    isPaused: true,
    time: 0,
    timeRemaining: 0,
    cycle: 0,
    skipped: false
  })

  const [playBark] = useSound(bark,
    { volume: 0.65 }
  );

  const [playTweet] = useSound(tweet,
    { volume: 0.20 }
  );

  const [playGong] = useSound(gong,
    { volume: 0.20 }
  );


  



  //sets up a worker thread to keep the clock running accurately when browser is in background
  const worker = useRef()
  useEffect(() => {
    worker.current = new myWorker()
    return () => {
      worker.current.terminate();
    }
  }, [])

  //updates time remaining in state from the worker thread every second
  useEffect(() => {
    const eventHander = e => {
      if(e.data === true){
        //here is where the sound should play
       
      }else{
        setTimer((timer) => ({
          ...timer,
          timeRemaining: e.data
        }))
      }

    }

    worker.current.addEventListener('message', eventHander)
    return () => {
      worker.current.removeEventListener('message', eventHander)
    }
  }, [])



  //stops the countdown from resetting during certain UI events 
  let allowCountdownRestart = false
  useEffect(() => {
    if (allowCountdownRestart) {
      allowCountdownRestart = false
    } else {
      allowCountdownRestart = true
    }
  }, [props, allowCountdownRestart])

  useEffect(() => {
    allowCountdownRestart = false
  }, [breakPrefs, props.noClockStop, context.handleAddProject, context.currentProject]);



  //resets the timer when the user selects a new cycle
  //couldn't pass in [props.cycle] for this because of an issue when user selects the same cycle twice
  useEffect(() => {

    if (allowCountdownRestart) {
      worker.current.postMessage({ message: "start", "time": props.cycle * 60 })

      setTimer((timer) => ({
        ...timer,
        time: props.cycle * 60,
        timeRemaining: props.cycle * 60,
        cycle: props.cycle,
        onBreak: false
      }));

    }
  }, [props])

 

  //starts the timer after it's reset 
  useEffect(() => {
    if (timer.time > 0) {
      worker.current.postMessage({ message: "start", "time": timer.time })
    }
  }, [timer.time])

  //listens for when take break is pressed
  useEffect(() => {
    if(props.takeBreak !== 0){
      setTimer((timer) => ({
        ...timer,
        onBreak: true
      }))
    }
  

  }, [props.takeBreak])

  //handles the automatic switch to a break after a regular cycle 
  useEffect(() => {


    if (timer.time === 0 && !timer.firstPageLoad) {
      setTimeout(function () {
        if (timer.onBreak) {
          timer.onBreak = false
        } else {

          
          // const breakDuration = breakPrefs["break_duration"] * 60
          // if (breakDuration !== 0) {
          //   worker.current.postMessage({ message: "start", "time": breakDuration })
          //   setTimer((timer) => ({
          //     ...timer,
          //     onBreak: true,
          //     time: breakDuration,
          //     timeRemaining: breakDuration
          //   }));
          // }

          if (!timer.skipped) {
            props.updateDBWithTask(timer.cycle)
            props.subtractFromTimeUntilBreak(timer.cycle, false) 

          }
          setTimer((timer) => ({
            ...timer,
            skipped: false
          }));
        }
      }, 1000);

    } else {
      if (timer.time === timer.timeRemaining) {
        timer.firstPageLoad = false
        handleResume()
      }
    }
  }, [timer.time, timer.time === timer.timeRemaining])


  //determines which sound to play, and resets the timer to its original state at the end of a cycle.
  useEffect(() => {

    if (timer.timeRemaining === 0) {

      if(!timer.onBreak){
        playTweet()
      }

      setTimer((timer) => ({
        ...timer,
        time: 0,
        isPaused: true
      }));

    }
  }, [timer.timeRemaining])



  //listens for pause/unpause and updates timer accordingly
  useEffect(() => {
    if (timer.isPaused) {
      worker.current.postMessage({ message: "pause", "time": timer.timeRemaining })


    } else {
      worker.current.postMessage({ message: "start", "time": timer.timeRemaining })
    }
  }, [timer.isPaused])



  const handlePause = e => {
    setTimer({ ...timer, isPaused: true })
  }

 

  const handleResume = e => {
    if (timer.time !== 0) {
      setTimer({
        ...timer,
        isPaused: false
      })
    }
  }

  const handleSkip = () => {

    const elapsedMinutes = Math.floor((timer.time - timer.timeRemaining) / 60)
    const remainingSeconds = (timer.time - timer.timeRemaining) - elapsedMinutes * 60;
    const roundedMinutes = remainingSeconds > 30 ? elapsedMinutes + 1 : elapsedMinutes
    
    if(roundedMinutes > 0 && !timer.onBreak){
      props.updateDBWithTask(roundedMinutes)
      props.subtractFromTimeUntilBreak(roundedMinutes, true)
    }

    setTimer({ ...timer, skipped: true, timeRemaining: 0 })
    worker.current.postMessage({ message: "stop", "time": 0 })
  }

  const handleStop = () => {
    setTimer({ ...timer, onBreak: true, cycle: 0, timeRemaining: 0 })
    worker.current.postMessage({ message: "stop", "time": 0 })
  }




  const timeFormat = (duration) => {

    if (duration > 0) {
      var hrs = ~~(duration / 3600);
      var mins = ~~((duration % 3600) / 60);
      var secs = ~~duration % 60;
      var ret = "";
      if (hrs > 0) {
        ret += "" + hrs + ":" + (mins < 10 ? "0" : "");
      }
      ret += "" + mins + ":" + (secs < 10 ? "0" : "");
      ret += "" + secs;
      return ret;
    } else {
      return "00:00"
    }
  }


  return <>

    <div className="floatLeft">
      <div id="timer">

        {timer.onBreak ?
          <div><h2 className="display-timer-header">On Break </h2> <h2 className="display-timer">{timeFormat(timer.timeRemaining)}</h2></div>
          : <div><h3 className="display-timer-header"> Time Left </h3> <h3 ref={context.timerRef} className="display-timer">{timeFormat(timer.timeRemaining)}</h3></div>}


        <div className="toolbar-container">
          <div className={`toolbar-icons ${props.taskBarOpen ? "taskbar-open" : ""}`}>
            <i className="tooltip"><Stop className="toolbar-icon" onClick={handleStop}></Stop>
              <span className="tooltiptext">Stop/Cancel</span></i>
            {!timer.isPaused ?
              <i className="tooltip pause"><PausePresentation className="toolbar-icon" onClick={handlePause}></PausePresentation>
                <span className="tooltiptext pause-tooltip">Pause</span></i>
              :
              <i className="tooltip pause"><PlayCircleOutline className="toolbar-icon" onClick={handleResume}></PlayCircleOutline>
                <span className="tooltiptext">Resume</span></i>
            }
            <i className="tooltip"><SkipNext className="toolbar-icon" onClick={handleSkip} ></SkipNext>
              <span className="tooltiptext">Finish Early</span></i>

          </div>
        </div>

      </div>
    </div>

  </>
}




export default Clock;

backgroundtask.js


let myInterval;
/* eslint-disable-next-line no-restricted-globals */
self.onmessage = function(evt) {
    clearInterval(myInterval);

      if(evt.data.message === 'pause' || evt.data.message === 'stop' || evt.data.message === 'skip'){
        postMessage(evt.data.time)
      }

    if (evt.data.message == "start" || evt.data.message == "break") {
        var i = evt.data.time;
        myInterval = setInterval(function() {
            i--;
            postMessage(i);
        }, 1000);
        
    } 
  
};

【问题讨论】:

  • 所以这将取决于计时器更新代码在哪里。可以附上一些sn-ps吗?
  • 您可以使用contextredux、API,或者将时间状态移动到树中更高的组件吗?
  • 不幸的是,我现在拥有的时间保持逻辑位于嵌套更深的组件之一中。
  • 我不确定发布代码会有多少帮助,我认为我遇到了架构/理论问题。
  • 有什么方法可以防止组件被卸载?或者也许在计时器 UI 的“顶部”打开跟踪 UI?

标签: reactjs timer countdowntimer


【解决方案1】:

您可以通过 Context 解决此问题,通过在此上下文下包装所有需要跟踪时间的组件(您现在可以使用任何上下文被包装的组件)

EX(来自上下文文件的片段)

  // firing request to pingPong API to update header data each 5 sec
  useInterval(() => {
    // You action
  }, YOUR_TIME);

  // context values
  const contextValue = useMemo(() => {
    return {
      state,
      myFunctionONEIFNeeded,
      myFunctionTWOIFNeeded,
      .
      .
    };
  }, [state]);

  return (
    <OnlineAssessmentContext.Provider value={contextValue}>
      {props.children}
    </OnlineAssessmentContext.Provider>
  );

对于 useInterval,这是我们为间隔构建的自定义钩子(您可以通过任何您喜欢的东西替换它):

import React, {useEffect, useRef, useState, useCallback} from 'react';
import {v4  as uuid} from 'uuid';
const useInterval = (callback, delay) => {
  const savedCallback = useRef();

  const [id ,setId] = useState(uuid());
  // Remember the latest callback.
  savedCallback.current = callback;

  // Set up the interval.
  useEffect(() => {
    function tick() {
      savedCallback.current();
    }

    if (delay !== null) {
      let id = setInterval(tick, delay);
      return () => clearInterval(id);
    }
  }, [delay, id]);

  //This Function To Reset Time Interval Depend On Call Reset
  const reset = useCallback(()=>{
    setId(uuid());
  },[]);
  
  return reset;
}

export default useInterval;

=====================

重要提示: 1- 如果状态发生任何变化,您需要确保重新渲染,并且更愿意在不更改全局上下文状态的情况下完成所需的工作,并且您可以考虑如何优化更新以适应需要

2-我们在return函数中使用Memo来防止在状态改变时重新渲染else,通常我们不需要监听函数更新else如果我们需要它,你可以检查或安装why-did-you-渲染

【讨论】:

    【解决方案2】:

    我相信如果我没记错的话,您可以将与时间相关的逻辑提升到父组件。

      const App = () => {
         return (
            <>
              <Timer />
              <Route 1 />
              <Route 2 />
            <>
         )
      }
    

    是否要隐藏 Timer 取决于您,但只要它在父级上,其他任何东西都无法改变它。

    【讨论】:

      【解决方案3】:

      当您导航到不同的路线时,您会卸载旧路线和其中的所有组件,从而丢失与这些组件关联的状态。在路由导航中保持状态的唯一方法是在路由切换上方保持状态。

      这可以通过多种方式完成:

      • 在父组件中维护状态并通过 props 传递。
      • 使用Context 为所有子组件提供上下文。通过 Context 提供状态时,可以通过父组件管理状态并将其传递到 Context - 或者您可以在 Context 内部提供状态更新功能(如链接文档中所示)。
      • 使用像Redux 这样的状态管理库。如果您的应用程序状态很复杂,我建议您研究一下 - 因为通过 Context 管理复杂状态可能更麻烦且容易出错。如果您的应用程序足够简单,这可能不会成为问题。

      【讨论】:

        最近更新 更多