Reputation: 105
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
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
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