react-countdown无法重置/二次渲染及状态更新报错问题
问题描述
我需要在拍卖状态变更后重置倒计时。从API获取三种状态:future、live、expired:
- 状态为
future时,时间戳对应拍卖开始时间 - 状态为
live时,时间戳对应拍卖结束时间
当前代码通过useEffect调用API获取初始数据,传入Countdown组件能正常工作,但第一次倒计时结束后,在handleRenderer中检查状态并更新auctionStatus,useEffect监测到状态变化会重新调用API获取新时间戳——虽然新时间戳能显示,但第二次倒计时会停止。
同时出现以下警告:
strument.js:108 Warning: Cannot update a component (`BiddingBlock`) while rendering a different component (`Countdown$1`). To locate the bad setState() call inside `Countdown$1`, follow the stack trace as described in https://reactjs.org/link/setstate-in-render at Countdown$1 (webpack-internal:///./node_modules/react-countdown/dist/index.es.js:311:5) at div at div at Counter (webpack-internal:///./src/frontend/components/Counter/Counter.js:78:65) at div at section at main at div at div at div at BiddingBlock (webpack-internal:///./src/frontend/components/BiddingBlock/BiddingBlock.js:85:65) at div at SocketProvider (webpack-internal:///./src/frontend/services/socketProvider.js:60:23) at QueryClientProvider (webpack-internal:///./node_modules/react-query/es/react/QueryClientProvider.js:39:21) at Provider (webpack-internal:///./node_modules/jotai/esm/index.mjs:692:3) at App (webpack-internal:///./src/frontend/App.js:41:24)
另外状态表现异常:第一次倒计时结束后,handleRenderer收到completed=true时,状态会来回切换。
相关代码如下:
import React, { useEffect, useState } from 'react'; import { atom, useAtom } from 'jotai'; import { useQuery } from 'react-query'; import { startTimeAtom, auctionStatusAtom, winningLosingTextAtom } from '../../atoms'; import { toLocalDateTime } from '../../utility'; import Countdown from 'react-countdown'; import Api from '../../services/api2'; async function getAuctionStatus() { return await Api.getAuctionStatus(); } const Counter = () => { // let countdownApi = null; let statusUpdateCount = true; // component states const [startTime, setStartTime] = useAtom(startTimeAtom); const [auctionStatus, setAuctionStatus] = useAtom(auctionStatusAtom); // this flag is used to trigger useEffect after any sort of change in auctionStatus const [flag, setFlag] = useState(true); useEffect(() => { getAuctionStatus().then((response) => { setAuctionStatus(response.status); setStartTime(toLocalDateTime(response.end_time, WpaReactUi.time_zone)); // countdownApi && countdownApi.start(); // later remove this }); }, [auctionStatus, flag, statusUpdateCount]); /** * It takes a formatted string and returns a JSX element to display the remaining time in the timer. * * @param {string} formatted - a string that contains the remaining time in the timer, formatted as an object * * @returns {JSX.Element} - a JSX element containing the remaining time in the timer, * displayed in divs with key-value pairs */ const displayCountDown = (formatted) => { return Object.keys(formatted).map((key) => { return ( <div key={`${key}`} className={`countDown bordered ${key}-box`}> <span className={`num item ${key}`}>{formatted[key]}</span> <span>{key}</span> </div> ); }); }; const CompletionResponse = () => { return <span className='auction-ended-text'>Auction ended</span>; }; /** * handleRenderer is a function that takes an object with two properties, completed and formatted, * and returns a JSX component depending on the value of the completed property. * * @param {Object} props - an object with two properties: * - completed {boolean} - indicates if the timer has completed * - formatted {string} - the current time left in the timer, formatted as a string * * @returns {JSX.Element} - a JSX component, either the <CompletionResponse /> component if the timer has completed, * or the displayCountDown(formatted) component if the timer is still running */ const handleRenderer = ({ completed, formatted }) => { if (completed) { if (statusUpdateCount) { setTimeout(() => { if (auctionStatus === 'future') { getAuctionStatus().then((response) => { console.log('setting auction status', response); setAuctionStatus(response.status); setFlag(!flag); statusUpdateCount = false; }); } }, 1000); } if (auctionStatus === null || auctionStatus === 'future') { return <span className='please-wait-text'>Auction is about to go live, Happy bidding!</span>; } else { // TODO later fix this, need to add API change setAuctionStatus('expired'); return <CompletionResponse />; } } return displayCountDown(formatted); }; return ( startTime && ( <div className="bidAuctionCounterContainer"> <div className="countdown-container"> <Countdown key={startTime} autoStart={true} id="bidAuctioncounter" date={startTime} intervalDelay={0} precision={3} renderer={handleRenderer} /> </div> </div> ) ); }; export default Counter;
解决方案
核心问题分析
- 渲染中更新状态警告:
handleRenderer是Countdown的渲染回调,在其中调用setAuctionStatus属于在渲染过程中更新其他组件状态,违反React规则。 - 倒计时停止:
statusUpdateCount是普通变量,修改后不会触发组件重渲染,导致逻辑失效;同时状态更新后没有正确触发倒计时重启。 - 状态来回切换:在渲染回调中直接根据
completed设置状态,导致组件反复渲染触发状态变更。
修改后的代码
import React, { useEffect, useState } from 'react'; import { atom, useAtom } from 'jotai'; import { startTimeAtom, auctionStatusAtom } from '../../atoms'; import { toLocalDateTime } from '../../utility'; import Countdown from 'react-countdown'; import Api from '../../services/api2'; async function getAuctionStatus() { return await Api.getAuctionStatus(); } const Counter = () => { const [startTime, setStartTime] = useAtom(startTimeAtom); const [auctionStatus, setAuctionStatus] = useAtom(auctionStatusAtom); // 用React状态替代普通变量,确保变更能触发重渲染 const [statusUpdateCount, setStatusUpdateCount] = useState(true); // 初始化或状态变更时获取拍卖数据 useEffect(() => { const fetchAuctionData = async () => { const response = await getAuctionStatus(); setAuctionStatus(response.status); setStartTime(toLocalDateTime(response.end_time, WpaReactUi.time_zone)); }; fetchAuctionData(); }, [auctionStatus]); // 倒计时完成后的处理逻辑 const handleCountdownComplete = async () => { if (!statusUpdateCount) return; // 避免重复触发 setStatusUpdateCount(false); if (auctionStatus === 'future') { const response = await getAuctionStatus(); setAuctionStatus(response.status); // 重置状态允许后续更新 setStatusUpdateCount(true); } else { setAuctionStatus('expired'); } }; const displayCountDown = (formatted) => { return Object.keys(formatted).map((key) => { return ( <div key={`${key}`} className={`countDown bordered ${key}-box`}> <span className={`num item ${key}`}>{formatted[key]}</span> <span>{key}</span> </div> ); }); }; const CompletionResponse = () => { return <span className='auction-ended-text'>拍卖已结束</span>; }; const handleRenderer = ({ completed, formatted }) => { if (completed) { if (auctionStatus === null || auctionStatus === 'future') { return <span className='please-wait-text'>拍卖即将开始,祝您竞价愉快!</span>; } else { return <CompletionResponse />; } } return displayCountDown(formatted); }; return ( startTime && ( <div className="bidAuctionCounterContainer"> <div className="countdown-container"> <Countdown key={startTime} autoStart={true} id="bidAuctioncounter" date={startTime} intervalDelay={0} precision={3} renderer={handleRenderer} onComplete={handleCountdownComplete} // 使用组件的onComplete回调处理完成逻辑 /> </div> </div> ) ); }; export default Counter;
关键修改说明
- 移除渲染函数中的
setAuctionStatus调用,改用Countdown的onComplete回调处理倒计时完成逻辑,彻底解决渲染中更新状态的警告。 - 将
statusUpdateCount改为React状态useState(true),确保变量变更能触发组件重渲染,修复逻辑失效问题。 - 统一数据获取逻辑到
useEffect,仅依赖auctionStatus触发,避免不必要的重复请求。 - 清理倒计时完成后的逻辑,避免状态来回切换,确保状态变更只在合适时机触发。
内容的提问来源于stack exchange,提问作者Muzammil Hussain
相关产品推荐
相关产品推荐

