I have the following usage of my hook, but it doesn't use the new timerDuration when I update inside my input:
const [secondsBetweenRepsSetting, setSecondsBetweenRepsSetting] = useState(DEFAULT_SECONDS_BETWEEN_REPS)
const {secondsLeft, isRunning, start, stop} = useTimer({
duration: secondsBetweenRepsSetting,
onExpire: () => sayRandomExerciseName(),
onTick: () => handleTick(),
});
const onTimeBetweenRepsChange = (event: any) => {
const secondsBetweenRepsSettingString = event.target.value;
const secondsBetweenRepsSettingInt = parseInt(secondsBetweenRepsSettingString)
setSecondsBetweenRepsSetting(secondsBetweenRepsSettingInt)
}
return <React.Fragment>
<input type="number" name="secondsBetweenRepsSetting" value={secondsBetweenRepsSetting} onChange={onTimeBetweenRepsChange}/>
</React.Fragment>
And here is the implementation of the useTimer hook, which I'm not sure why it's not getting my duration update?
import { useState } from 'react';
import Validate from "../utils/Validate";
import useInterval from "./useInterval";
export default function useTimer({ duration: timerDuration, onExpire, onTick}) {
const [duration] = useState(timerDuration)
const [secondsLeft, setSecondsLeft] = useState(timerDuration)
const [isRunning, setIsRunning] = useState(false)
function start() {
setIsRunning(true)
}
function stop() {
setIsRunning(false)
}
function handleExpire() {
Validate.onExpire(onExpire) && onExpire();
}
useInterval(() => {
const secondsMinusOne = secondsLeft - 1;
setSecondsLeft(secondsMinusOne)
if(secondsMinusOne <= 0) {
setSecondsLeft(duration) // Reset timer automatically
handleExpire()
} else {
Validate.onTick(onTick) && onTick();
}
}, isRunning ? 1000 : null)
return {secondsLeft, isRunning, start, stop, }
}
Remove the line:
const [duration] = useState(timerDuration);
You are already getting duration from timerDuration, just use that.
Related
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 } from 'react';
import { atom, useAtom } from 'jotai';
import { startTimeAtom, auctionStatusAtom } from '../../atoms';
import { toLocalDateTime } from '../../utility';
import Countdown from 'react-countdown';
import { getCurrentAuctionStatus } from '../../services/api';
async function getAuctionStatus() {
let response = await getCurrentAuctionStatus(WpaReactUi.auction_id);
return await response.payload();
}
const Counter = () => {
// component states
const [startTime, setStartTime] = useAtom(startTimeAtom);
const [auctionStatus, setAuctionStatus] = useAtom(auctionStatusAtom);
useEffect(() => {
getAuctionStatus().then((response) => {
setAuctionStatus(response.status);
setStartTime(toLocalDateTime(response.end_time, WpaReactUi.time_zone));
});
}, [auctionStatus]);
//
const handleRenderer = ({ completed, formatted }) => {
if (completed) {
console.log("auction status now is:", auctionStatus);
setTimeout(() => {
if (auctionStatus === 'future') {
getAuctionStatus().then((response) => {
setAuctionStatus(response.status);
});
}
}, 2000)
}
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>
);
});
};
console.log('starttime now:', startTime);
return (
startTime && (
<div className="bidAuctionCounterContainer">
<div className="bidAuctionCounterInner">
<Countdown
key={auctionStatus}
autoStart={true}
id="bidAuctioncounter"
date={startTime}
intervalDelay={0}
precision={3}
renderer={handleRenderer}
/>
</div>
</div>
)
);
};
export default Counter;
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);
});
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,
];
};
import { format } from 'date-fns'import { useEffect, useState } from 'react';import { useDispatch, useSelector } from "react-redux";import {selectServerTime,getServerTime} from "../../app/appSlice";
export type TimerProps = {updateInterval : numberdisplayFormat : string}
export function RealDateTime({updateInterval = 60 , displayFormat = "HH:mm"} : TimerProps){
//local
const [today, setDate] = useState(null);
const dispatch = useDispatch();
const serverTime = useSelector(selectServerTime);
const startLocalTimer = () => {
const timer = setInterval (()=>{
const addMlSeconds = 1000;
console.log("##### today",today);
console.log("##### serverTime",serverTime);
if (serverTime !== null){
const newDateObj = new Date(today + addMlSeconds).getTime() ;
setDate(newDateObj)
}else{
setDate(Date.now());
}
},1000);
}
useEffect(() => {
startLocalTimer();
const serverTimer = setInterval(() => {
dispatch(getServerTime());
},10000);
return () => {
clearInterval(serverTimer);
}
}, []);
useEffect(() => {
setDate(serverTime) // updates the current date from server every time the server is updete the state
}, [dispatch,serverTime]);
return (
<>
{today && format(today, displayFormat)}
</>
)
}
The code never enters this condition if (serverTime !== null)
although serverTime gets a value at some point in the beginning
I want to update my clock from the server every time the selector receives a new value, if any time has arrived from the server my selector holds it and with it I will initialize the time in the UI
I am trying to merge these two hooks to get the direction and the percentage of the scroll.
useScrollPercentage.js
import { useRef, useState, useEffect } from "react";
export default function useScrollPercentage() {
const scrollRef = useRef(null);
const [scrollPercentage, setScrollPercentage] = useState(NaN);
const reportScroll = e => {
setScrollPercentage(getScrollPercentage(e.target));
};
useEffect(
() => {
const node = scrollRef.current;
if (node !== null) {
node.addEventListener("scroll", reportScroll, { passive: true });
if (Number.isNaN(scrollPercentage)) {
setScrollPercentage(getScrollPercentage(node));
}
}
return () => {
if (node !== null) {
node.removeEventListener("scroll", reportScroll);
}
};
},
[scrollPercentage]
);
return [scrollRef, Number.isNaN(scrollPercentage) ? 0 : scrollPercentage];
}
function getScrollPercentage(element) {
if (element === null) {
return NaN;
}
const height = element.scrollHeight - element.clientHeight;
return Math.round((element.scrollTop / height) * 100);
}
useScrollDirection.js
import {useState, useEffect} from 'react';
import _ from 'lodash';
export default function useScrollDirection({
ref,
threshold,
debounce,
scrollHeightThreshold,
}) {
threshold = threshold || 10;
debounce = debounce || 10;
scrollHeightThreshold = scrollHeightThreshold || 0;
const [scrollDir, setScrollDir] = useState(null);
const debouncedSetScrollDir = _.debounce(setScrollDir, debounce);
useEffect(() => {
let lastScrollY = ref?.current?.scrollTop;
let lastScrollDir;
let ticking = false;
const hasScrollHeightThreshold =
ref?.current?.scrollHeight - ref?.current?.clientHeight >
scrollHeightThreshold;
const updateScrollDir = () => {
const scrollY = ref?.current?.scrollTop;
if (
Math.abs(scrollY - lastScrollY) < threshold ||
!hasScrollHeightThreshold
) {
ticking = false;
return;
}
const newScroll = scrollY > lastScrollY ? 'Down' : 'Up';
if (newScroll !== lastScrollDir) {
debouncedSetScrollDir(newScroll);
}
lastScrollY = scrollY > 0 ? scrollY : 0;
lastScrollDir = newScroll;
ticking = false;
};
const onScroll = () => {
if (!ticking) {
window.requestAnimationFrame(updateScrollDir);
ticking = true;
}
};
ref?.current?.addEventListener('scroll', onScroll);
return () => window.removeEventListener('scroll', onScroll);
}, []);
return scrollDir;
}
Currently I am using it like this
App.js
const [scrollRef, scrollPercentage] = useScrollPercentage();
const scrollDirection = useScrollDirection({ref: scrollRef});
const handleScroll = () => {
console.log(scrollPercentage, scrollDirection);
}
return (
<Modal ref={scrollRef} onScroll={handleScroll}>
// Scrollable Content
</Modal>
)
The problem is that since I don't want to trigger useScrollDirection for every scroll percentage unless the scroll direction changed only it should trigger. Also is it possible to merge these hooks?
Any help is appreciated
I'm trying to implement countdown timer on my own just to know hooks more. I know there are libraries out there but don't want to use it. the problem with my code is, I cannot get updated state inside "timer" function which is updated in start timer function I'm trying to implement timer that will have triggers to start, stop, & resume & can be manually trigger. by other component that is using the countdown component
import React, { useState } from 'react';
const Countdown = ({ countDownTimerOpt }) => {
const [getObj, setObj] = useState({
formatTimer: null,
countDownTimer: 0,
intervalObj: null,
});
const { formatTimer, countDownTimer, intervalObj } = getObj;
if (countDownTimerOpt > 0 && intervalObj === null) {
startTimer();
}
function startTimer() {
const x = setInterval(() => {
timer();
}, 1000);
setObj((prev) => ({
...prev,
countDownTimer: countDownTimerOpt,
intervalObj: x,
}));
}
function timer() {
var days = Math.floor(countDownTimer / 24 / 60 / 60);
var hoursLeft = Math.floor(countDownTimer - days * 86400);
var hours = Math.floor(hoursLeft / 3600);
var minutesLeft = Math.floor(hoursLeft - hours * 3600);
var minutes = Math.floor(minutesLeft / 60);
var remainingSeconds = countDownTimer % 60;
const formatTimer1 =
pad(days) +
':' +
pad(hours) +
':' +
pad(minutes) +
':' +
pad(remainingSeconds);
if (countDownTimer === 0) {
clearInterval(intervalObj);
} else {
setObj((prev) => ({
...prev,
formatTimer: formatTimer1,
countDownTimer: prev['countDownTimer'] - 1,
}));
}
}
function pad(n) {
return n < 10 ? '0' + n : n;
}
return <div>{formatTimer ? formatTimer : Math.random()}</div>;
};
export default Countdown;
import React, { useState, useEffect } from 'react';
import Timer from '../../components/countdown-timer/countdown.component';
const Training = () => {
const [getValue, setValue] = useState(0);
useEffect(() => {
const x = setTimeout(() => {
console.log('setTimeout');
setValue(10000);
}, 5000);
return () => clearInterval(x);
}, []);
return <Timer countDownTimerOpt={getValue} />;
don't want to use any set interval inside training page as the countdown component will also be used in exam page
Usually with hooks I would combine your functionality into a custom hook and use it in different places.
const useTimer = (startTime) => {
const [time, setTime] = useState(startTime)
const [intervalID, setIntervalID] = useState(null)
const hasTimerEnded = time <= 0
const isTimerRunning = intervalID != null
const update = () => {
setTime(time => time - 1)
}
const startTimer = () => {
if (!hasTimerEnded && !isTimerRunning) {
setIntervalID(setInterval(update, 1000))
}
}
const stopTimer = () => {
clearInterval(intervalID)
setIntervalID(null)
}
// clear interval when the timer ends
useEffect(() => {
if (hasTimerEnded) {
clearInterval(intervalID)
setIntervalID(null)
}
}, [hasTimerEnded])
// clear interval when component unmounts
useEffect(() => () => {
clearInterval(intervalID)
}, [])
return {
time,
startTimer,
stopTimer,
}
}
You can of course add a reset function or do other changes but use could look like this:
const Training = () => {
const { time, startTimer, stopTimer } = useTimer(20)
return <>
<div>{time}</div>
<button onClick={startTimer}>start</button>
<button onClick={stopTimer}>stop</button>
</>
}
You can create a useCountDown Hook as follow (In Typescript) :
Gist
import { useEffect, useRef, useState } from 'react';
export const useCountDown: (
total: number,
ms?: number,
) => [number, () => void, () => void, () => void] = (
total: number,
ms: number = 1000,
) => {
const [counter, setCountDown] = useState(total);
const [startCountDown, setStartCountDown] = useState(false);
// Store the created interval
const intervalId = useRef<number>();
const start: () => void = () => setStartCountDown(true);
const pause: () => void = () => setStartCountDown(false);
const reset: () => void = () => {
clearInterval(intervalId.current);
setStartCountDown(false);
setCountDown(total);
};
useEffect(() => {
intervalId.current = setInterval(() => {
startCountDown && counter > 0 && setCountDown(counter => counter - 1);
}, ms);
// Clear interval when count to zero
if (counter === 0) clearInterval(intervalId.current);
// Clear interval when unmount
return () => clearInterval(intervalId.current);
}, [startCountDown, counter, ms]);
return [counter, start, pause, reset];
};
Usage Demo: https://codesandbox.io/s/usecountdown-hook-56lqv
Recently I picked up a project that has d3-flame-graph on it and the graph is displayed according to the Filters defined on another component.
My issue is that when searching with new parameters I can't seem to clean the previous chart and I was wondering if someone could help me. Basically what I'm having right now is, when I first enter the page, the loading component, then I have my graph and when I search for a new date I have the loading component but on top of that I still have the previous graph
I figured I could use flamegraph().destroy() on const updateGraph but nothing is happening
import React, { FC, useEffect, useRef, useState, useCallback } from 'react'
import { useParams } from 'react-router-dom'
import moment from 'moment'
import * as d3 from 'd3'
import { flamegraph } from 'd3-flame-graph'
import Filters, { Filter } from '../../../../../../components/Filters'
import { getFlamegraph } from '../../../../../../services/flamegraph'
import { useQueryFilter } from '../../../../../../hooks/filters'
import FlamegraphPlaceholder from '../../../../../../components/Placeholders/Flamegraph'
import css from './flamegraph.module.css'
import ToastContainer, {
useToastContainerMessage,
} from '../../../../../../components/ToastContainer'
const defaultFilters = {
startDate: moment().subtract(1, 'month'),
endDate: moment(),
text: '',
limit: 10,
}
const getOffSet = (divElement: HTMLDivElement | null) => {
if (divElement !== null) {
const padding = 100
const minGraphHeight = 450
// ensure that the graph has a min height
return Math.max(
window.innerHeight - divElement.offsetTop - padding,
minGraphHeight
)
} else {
const fallBackNavigationHeight = 300
return window.innerHeight - fallBackNavigationHeight
}
}
const Flamegraph: FC = () => {
const [queryFilters, setQueryFilters] = useQueryFilter(defaultFilters)
const [fetching, setFetching] = useState(false)
const [graphData, setGraphData] = useState()
const {
messages: toastMessages,
addMessage: addMessageToContainer,
removeMessage: removeMessageFromContainer,
} = useToastContainerMessage()
const flameContainerRef = useRef<HTMLDivElement | null>(null)
const flameRef = useRef<HTMLDivElement | null>(null)
const graphRef = useRef<any>()
const graphDataRef = useRef<any>()
const timerRef = useRef<any>()
const { projectId, functionId } = useParams()
let [sourceId, sourceLine] = ['', '']
if (functionId) {
;[sourceId, sourceLine] = functionId.split(':')
}
const createGraph = () => {
if (flameContainerRef.current && flameRef.current) {
graphRef.current = flamegraph()
.width(flameContainerRef.current.offsetWidth)
.height(getOffSet(flameRef.current))
.cellHeight(30)
.tooltip(false)
.setColorMapper(function(d, originalColor) {
// Scale green component proportionally to box width (=> the wider the redder)
let greenHex = (192 - Math.round((d.x1 - d.x0) * 128)).toString(16)
return '#FF' + ('0' + greenHex).slice(-2) + '00'
})
}
}
const updateGraph = (newData: any) => {
setGraphData(newData)
graphDataRef.current = newData
if (graphRef.current) {
if (newData === null) {
graphRef.current.destroy()
graphRef.current = null
} else {
d3.select(flameRef.current)
.datum(newData)
.call(graphRef.current)
}
}
}
const fetchGraph = (filters: Filter) => {
setFetching(true)
getFlamegraph(
Number(projectId),
filters.startDate ? filters.startDate.unix() : 0,
filters.endDate ? filters.endDate.unix() : 0,
sourceId,
sourceLine
)
.then(graphData => {
if (!graphRef.current) {
createGraph()
}
updateGraph(graphData)
})
.catch(({ response }) => {
updateGraph(null)
if (response.data) {
addMessageToContainer(response.data.message, true)
}
})
.finally(() => {
setFetching(false)
})
}
const onResize = useCallback(() => {
clearTimeout(timerRef.current)
timerRef.current = setTimeout(() => {
if (graphRef.current && flameContainerRef.current) {
graphRef.current.width(flameContainerRef.current.offsetWidth)
d3.select(flameRef.current)
.datum(graphDataRef.current)
.call(graphRef.current)
}
}, 500)
}, [])
useEffect(() => {
fetchGraph(queryFilters)
window.addEventListener('resize', onResize)
return () => {
window.removeEventListener('resize', onResize)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
const onChangeFilters = (filters: Filter) => {
setQueryFilters(filters)
fetchGraph(filters)
}
return (
<div className={css.host}>
<Filters
defaultValues={queryFilters}
searching={fetching}
onSearch={onChangeFilters}
/>
<div className={css.flameBox}>
<div className={css.flameContainer} ref={flameContainerRef}>
<div ref={flameRef} />
</div>
{fetching || !graphData ? (
<FlamegraphPlaceholder loading={fetching} />
) : null}
</div>
<ToastContainer
messages={toastMessages}
toastDismissed={removeMessageFromContainer}
/>
</div>
)
}
export default Flamegraph
Firstly, flamegraph() creates a new instance of flamegraph, you'd need to use graphref.current.destroy(). Secondly, you'd want to destroy this not when the data has already been loaded, but just as it starts to load, right? Because that's the operation that takes time.
Consider the following:
const cleanGraph = () => {
if (graphref.current !== undefined) {
graphref.current.destroy()
}
}
const fetchGraph = (filters: Filter) => {
setFetching(true)
cleanGraph()
getFlamegraph(
Number(projectId),
filters.startDate ? filters.startDate.unix() : 0,
filters.endDate ? filters.endDate.unix() : 0,
sourceId,
sourceLine
)
...
}