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

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;
解决方案

核心问题分析

  1. 渲染中更新状态警告:handleRenderer是Countdown的渲染回调,在其中调用setAuctionStatus属于在渲染过程中更新其他组件状态,违反React规则。
  2. 倒计时停止:statusUpdateCount是普通变量,修改后不会触发组件重渲染,导致逻辑失效;同时状态更新后没有正确触发倒计时重启。
  3. 状态来回切换:在渲染回调中直接根据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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.05 10:21:02