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

React组件:CountdownCircleTimer与数字输入框不同步问题

问题与解决方案

问题描述

React组件中数字输入框与CountdownCircleTimer倒计时计时器无法同步。可以启动和暂停倒计时,但暂停后修改输入框的duration数值,两者会出现不同步的情况,且差距会随时间推移逐渐增大。

原代码

import React, { useState } from 'react';
import { CountdownCircleTimer } from 'react-countdown-circle-timer';
import styles from '../styles/Home.module.css';

export default function Spinner({
  setColor,
  color,
  setRotation,
  rotation,
  loop,
  setLoop,
}) {
  const [duration, setDuration] = useState(60);
  function handleDurationChange(e) {
    if (e.target.value > 0) {
      setDuration(e.target.value);
    }
  }
  const [isPlaying, setIsPlaying] = useState(false);

  const renderTime = ({ remainingTime }) => {
    if (remainingTime === 0 && loop === 'false') {
      setIsPlaying(false);
      return <div className={styles.timer}>Finished</div>;
    }
    return (
      <div className={styles.timer}>
        <div className={styles.text}>Remaining</div>
        <div className={styles.value}>{remainingTime}</div>
        <div className={styles.text}>seconds</div>
      </div>
    );
  };
  const [key, setKey] = useState(0);
  return (
    <div className={styles.card}>
      <div>
        <CountdownCircleTimer
          key={key}
          isPlaying={isPlaying}
          duration={duration}
          initialRemainingTime={duration}
          size={350}
          strokeWidth={30}
          colors={color}
          rotation={rotation}
          remainingTime={duration}
          onComplete={() => ({
            shouldRepeat: { loop },
            delay: 0,
            newInitialRemainingTime: { duration },
          })}
        >
          {renderTime}
        </CountdownCircleTimer>
      </div>
      <div>
        <div>
          <label className={styles.num} htmlFor='duration'></label>
          <input
            id='duration'
            type='number'
            value={duration}
            onChange={handleDurationChange}
            disabled={isPlaying}
          />
          <br></br>
          <label className={styles.btn} htmlFor='isPlaying'>
            {isPlaying ? 'Pause' : 'Play'}
          </label>
          <input
            id='isPlaying'
            type='checkbox'
            onChange={() =>
              setIsPlaying(!isPlaying) &&
              setDuration(duration) &&
              setColor(color)
            }
          />
          <br></br>
          <label className={styles.btn} htmlFor='reset'>
            Reset
          </label>
          <input
            onClick={() => setKey((prevKey) => prevKey + 1)}
            id='reset'
            type='checkbox'
            onChange={() => setDuration(60) && setColor('#f25758')}
          />
        </div>
      </div>
    </div>
  );
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>

问题分析

  1. 状态冲突:同时传递initialRemainingTime和remainingTime给CountdownCircleTimer,导致组件内部状态混乱;直接用duration作为剩余时间,修改时长后无法同步更新计时器当前剩余时间。
  2. 无效状态更新:Play/Pause按钮的onChange中,setDuration(duration)和setColor(color)没有实际修改值,且用&&连接状态更新会导致后续操作因短路逻辑不执行。
  3. 不合理的组件重置方式:依赖key重置计时器组件,逻辑冗余且容易引发意外问题;Reset按钮用checkbox类型不符合交互逻辑。

修复方案

  • 新增独立的remainingTime状态追踪当前剩余时间,与总时长duration分离。
  • 移除initialRemainingTime,通过remainingTime控制计时器当前剩余时间,duration作为总时长参数。
  • 暂停状态下修改duration时,同步更新remainingTime为新的时长值。
  • 简化Play/Pause和Reset的交互逻辑,移除无效操作,改用button实现Reset功能。

修复后的代码

import React, { useState, useEffect } from 'react';
import { CountdownCircleTimer } from 'react-countdown-circle-timer';
import styles from '../styles/Home.module.css';

export default function Spinner({
  setColor,
  color,
  setRotation,
  rotation,
  loop,
  setLoop,
}) {
  const [duration, setDuration] = useState(60);
  // 新增状态存储当前剩余时间
  const [remainingTime, setRemainingTime] = useState(duration);
  const [isPlaying, setIsPlaying] = useState(false);

  // 当duration修改且处于暂停状态时,同步更新剩余时间
  useEffect(() => {
    if (!isPlaying) {
      setRemainingTime(duration);
    }
  }, [duration, isPlaying]);

  function handleDurationChange(e) {
    const newValue = Number(e.target.value);
    if (newValue > 0) {
      setDuration(newValue);
    }
  }

  const renderTime = ({ remainingTime: timerRemaining }) => {
    if (timerRemaining === 0 && loop === 'false') {
      setIsPlaying(false);
      return <div className={styles.timer}>Finished</div>;
    }
    return (
      <div className={styles.timer}>
        <div className={styles.text}>Remaining</div>
        <div className={styles.value}>{timerRemaining}</div>
        <div className={styles.text}>seconds</div>
      </div>
    );
  };

  const handleReset = () => {
    setDuration(60);
    setRemainingTime(60);
    setColor('#f25758');
    setIsPlaying(false);
  };

  return (
    <div className={styles.card}>
      <div>
        <CountdownCircleTimer
          isPlaying={isPlaying}
          duration={duration}
          size={350}
          strokeWidth={30}
          colors={color}
          rotation={rotation}
          remainingTime={remainingTime}
          // 更新onComplete逻辑,使用当前duration作为重复时的初始剩余时间
          onComplete={() => ({
            shouldRepeat: loop === 'true',
            delay: 0,
            newRemainingTime: duration,
          })}
          // 监听剩余时间变化,同步到本地状态
          onUpdate={({ remainingTime: updatedTime }) => {
            setRemainingTime(updatedTime);
          }}
        >
          {renderTime}
        </CountdownCircleTimer>
      </div>
      <div>
        <div>
          <label className={styles.num} htmlFor='duration'></label>
          <input
            id='duration'
            type='number'
            value={duration}
            onChange={handleDurationChange}
            disabled={isPlaying}
          />
          <br />
          <label className={styles.btn} htmlFor='isPlaying'>
            {isPlaying ? 'Pause' : 'Play'}
          </label>
          <input
            id='isPlaying'
            type='checkbox'
            checked={isPlaying}
            onChange={() => setIsPlaying(!isPlaying)}
          />
          <br />
          {/* 改用button实现Reset功能,交互更合理 */}
          <button className={styles.btn} onClick={handleReset}>
            Reset
          </button>
        </div>
      </div>
    </div>
  );
}

关键修复点说明

  1. 分离时长与剩余时间:用duration存储总时长,remainingTime存储当前剩余时间,避免状态混淆。
  2. 同步时长修改:通过useEffect在暂停状态下修改duration时,自动同步remainingTime,保证输入框和计时器一致。
  3. 优化计时器回调:onComplete中使用newRemainingTime指定重复时的初始时间,onUpdate同步计时器内部剩余时间到本地状态。
  4. 简化交互逻辑:移除无效的状态更新,Reset改用button,交互更符合用户预期。

内容的提问来源于stack exchange,提问作者The_Rico

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.11 07:25:25