Muzammil Hussain
Muzammil Hussain

Reputation: 105

react-countdown is not reseting or re-rendering second time

What I am trying to do is to update the reset the countdown after changing the status.

There are three status that i am fetching from API .. future, live and expired

If API is returning future with a timestamp, this timestamp is the start_time of the auction, but if the status is live then the timestamp is the end_time of the auction.

So in the following code I am calling api in useEffect to fetch initial data pass to the Countdown and it works, but on 1st complete in handleRenderer i am checking its status and updating the auctionStatus while useEffect is checking the updates to recall API for new timestamp .. so far its working and 2nd timestamp showed up but it is stopped ... means not counting down time for 2nd time.

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;

But getting this error

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)
e

Also its acting funky, when 1st countdown end it return complete true value to handleRenderer so there i check its auctionStatus, but its going back and forth.

Upvotes: 0

Views: 912

Answers (2)

Adam Morsi
Adam Morsi

Reputation: 499

Have a look at the following useCountdown hook:

https://codepen.io/AdamMorsi/pen/eYMpxOQ

const DEFAULT_TIME_IN_SECONDS = 60;

const useCountdown = ({ initialCounter, callback }) => {
  const _initialCounter = initialCounter ?? DEFAULT_TIME_IN_SECONDS,
    [resume, setResume] = useState(0),
    [counter, setCounter] = useState(_initialCounter),
    initial = useRef(_initialCounter),
    intervalRef = useRef(null),
    [isPause, setIsPause] = useState(false),
    isStopBtnDisabled = counter === 0,
    isPauseBtnDisabled = isPause || counter === 0,
    isResumeBtnDisabled = !isPause;

  const stopCounter = useCallback(() => {
    clearInterval(intervalRef.current);
    setCounter(0);
    setIsPause(false);
  }, []);

  const startCounter = useCallback(
    (seconds = initial.current) => {
      intervalRef.current = setInterval(() => {
        const newCounter = seconds--;
        if (newCounter >= 0) {
          setCounter(newCounter);
          callback && callback(newCounter);
        } else {
          stopCounter();
        }
      }, 1000);
    },
    [stopCounter]
  );

  const pauseCounter = () => {
    setResume(counter);
    setIsPause(true);
    clearInterval(intervalRef.current);
  };

  const resumeCounter = () => {
setResume(0);
    setIsPause(false);
  };

  const resetCounter = useCallback(() => {
    if (intervalRef.current) {
      stopCounter();
    }
    setCounter(initial.current);
    startCounter(initial.current - 1);
  }, [startCounter, stopCounter]);

  useEffect(() => {
    resetCounter();
  }, [resetCounter]);

  useEffect(() => {
    return () => {
      stopCounter();
    };
  }, [stopCounter]);

  return [
    counter,
    resetCounter,
    stopCounter,
    pauseCounter,
    resumeCounter,
    isStopBtnDisabled,
    isPauseBtnDisabled,
    isResumeBtnDisabled,
  ];
};

Upvotes: 0

Mohammad Tbeishat
Mohammad Tbeishat

Reputation: 1066

You use auctionStatus as a dependency for useEffect. And when response.status is the same, the auctionStatus doesn't change, so your useEffect won't be called again.

For answering your comment on how to resolve the issue.. I am not sure of your logic but I'll explain by this simple example.

export function App() {
  // set state to 'live' by default
  const [auctionStatus, setAuctionStatus] = React.useState("live") 
  React.useEffect(() => {
    console.log('hello')
    changeState()
  }, [auctionStatus])

  function changeState() {
    // This line won't result in calling your useEffect
    // setAuctionStatus("live") // 'hello' will be printed one time only.

    // You need to use a state value that won't be similar to the previous one. 
    setAuctionStatus("inactive") // useEffect will be called and 'hello' will be printed twice.
  }
}

You can simply use a flag instead that will keep on changing from true to false like this:

const [flag, setFlag] = React.useState(true)

useEffect(() => {
  // ..
}, [flag])

// And in handleRenderer
getAuctionStatus().then((response) => {
  setFlag(!flag);
});

Upvotes: 1

Related Questions