I need to display the time it took for me to execute a particularly complex function as it is being executed.
Seems simple enough, but I can't make this happen.
I've implemented a simple Chronometer component, which works fine.
The idea is to start it right before I execute the algorithm and then stop it when it finishes. So basically my component would look like this:
const App = () => {
const [isRunning, setIsRunning] = React.useState(false)
// run the chronometer on click and start executing the algorithm
const runAlgorithm = () => {
setIsRunning(true)
someAlgorithmThatWillTakeLong()
// top once its done
setIsRunning(false)
}
return (
<div>
<button onClick={() => runAlgorithm()}>Execute Algorithm and time it</button>
<Chronometer isRunning={isRunning}/>
</div>
)
}
It is however ignored and I am not sure how to get both these things executing at once. Am I missing something?
Here is a working sample of what my code currently looks like:
const Chronometer = ({isRunning}) => {
const [milisseconds, setMilliseconds] = React.useState(0)
React.useEffect(() => {
let interval = null
if (isRunning) {
interval = setInterval(() => {
setMilliseconds((ms) => ms + 1)
}, 1)
} else {
clearInterval(interval)
}
return () => {
clearInterval(interval)
}
}, [isRunning])
return (
<div>
<h3>Solution time:</h3>
<h4>{milisseconds/100}</h4>
</div>
)
}
const someAlgorithmThatWillTakeLong = () => {
for (let i=0; i<999999; i++) {
// just run whatever
}
console.warn('done!')
return
}
const App = () => {
const [isRunning, setIsRunning] = React.useState(false)
const runAlgorithm = () => {
setIsRunning(true)
someAlgorithmThatWillTakeLong()
setIsRunning(false)
}
return (
<div>
<button onClick={() => runAlgorithm()}>Execute Algorithm and time it</button>
<button style={{ width: 150 }} onClick={() => setIsRunning(!isRunning)}>{isRunning ? 'Stop' : 'Test chronometer'}</button>
<Chronometer isRunning={isRunning}/>
</div>
)
}
ReactDOM.render(
<App />,
document.getElementById('app')
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.0/umd/react-dom.production.min.js"></script>
<div id="app"></div>
Related
I'm implementing stopwatch in ReactJs this is how my code looks as of now:
const App: React.FC = () => {
const [seconds, setSeconds] = useState(0);
const [isPaused, setIsPaused] = useState(false);
const secondsToTimerFormat = (seconds: number): string => {
console.log(seconds)
return (seconds-seconds%60)/60+":"+seconds%60
}
const manipulateTimer = (toPauseTimer: boolean) => {
setIsPaused(toPauseTimer);
}
useEffect(() => {
if(!isPaused){
setTimeout(() => {
setSeconds(seconds + 1)
}, 1000)
}
}, [seconds, isPaused])
return (
<div className="App">
{secondsToTimerFormat(seconds)}
<div>
<button onClick={() => {manipulateTimer(true)}}>Pause</button>
<button onClick={() => {manipulateTimer(false)}}>Resume</button>
<button onClick={() => {
setSeconds(0);
}}>Reset</button>
</div>
</div>
);
}
I'm expecting this to work normally. But the "Reset" button is not working as expected.
If I click on "Reset" after 13 seconds, this is the console.log() output.
If I add a new variable inside useEffect(), say something like let execute: boolean = true and then set it to false in useEffect() clean up, everything is working as expected.
So, I know the fix, but I want to know the reason behind the current behaviour. I understand that when I click on reset, there is already a useEffect() running with seconds value as 13. But since its setTimeout() ends in one second and at the same time, I'm doing setSeconds(0), why would the previous useEffect() run multiple times before coming to halt?
Issues like this usually arise because the timers being used are not being cleared between renders. Also, when the next state depends on the current state, it is better to use the second form of the state setter function which takes the current state as the parameter and returns the next state. Modify the useEffect as given below to get this to work:
useEffect(() => {
let timer;
if (!isPaused) {
timer = setTimeout(() => {
setSeconds((seconds) => seconds + 1);
}, 1000);
}
return () => {
if (timer) clearTimeout(timer);
};
}, [seconds, isPaused]);
Try using setInterval and separate methods for handling the timer state:
import { useState } from "react";
export default function App() {
const [seconds, setSeconds] = useState(0);
const [intervalId, setIntervalId] = useState(0);
const secondsToTimerFormat = (seconds) => {
console.log(seconds);
return (seconds - (seconds % 60)) / 60 + ":" + (seconds % 60);
};
const handleStart = () => {
const id = setInterval(() => {
setSeconds((prev) => prev + 1);
}, 1000);
setIntervalId(id);
};
const handlePause = () => {
clearInterval(intervalId);
};
const handleReset = () => {
handlePause();
setSeconds(0);
};
return (
<div className="App">
{secondsToTimerFormat(seconds)}
<div>
<button
onClick={() => {
handlePause();
}}
>
Pause
</button>
<button
onClick={() => {
handleStart();
}}
>
Resume
</button>
<button
onClick={() => {
handleReset();
}}
>
Reset
</button>
</div>
</div>
);
}
Link to sandbox
I've tried different ways, but It doesn't works.
[...]
const [automatic, setAutomatic] = useState(false);
[...]
var startAuto;
useEffect(() => {
if (!automatic) {
console.log("stop");
clearInterval(startAuto);
} else {
startAuto = setInterval(() => {
changeQuestion("+");
}, 5 * 1000);
}
}, [automatic]);
[...]
<Button
onPress={() => setAutomatic(!automatic)}
title="turn on/off"
/>
[...]
It works when I put a setTimeout outside the useEffect, that way:
setTimeout(() => { clearInterval(startAuto); alert('stop'); }, 10000);
But I want to have a button to start / stop
Your var startAuto; is redeclared on each render, and since changing the state causes a re-render, it never holds the reference to the interval, which is never cleared.
Use the useEffect cleanup function to clear the interval. Whenever automatic changes, it would call the cleanup (if returned by the previous invocation), and if automatic is true it would create a new interval loop, and return a new cleanup function of the current interval.
useEffect(() => {
if(!automatic) return;
const startAuto = setInterval(() => {
changeQuestion("+");
}, 5 * 1000);
return () => {
clearInterval(startAuto);
};
}, [automatic]);
Working example:
const { useState, useEffect } = React;
const Demo = () => {
const [automatic, setAutomatic] = useState(false);
const [question, changeQuestion] = useState(0);
useEffect(() => {
if(!automatic) return;
const startAuto = setInterval(() => {
changeQuestion(q => q + 1);
}, 5 * 100);
return () => {
clearInterval(startAuto);
};
}, [automatic]);
return (
<div>
<button
onClick={() => setAutomatic(!automatic)}
>
turn {automatic ? 'off' : 'on'}
</button>
<p>{question}</p>
</div>
);
}
ReactDOM
.createRoot(root)
.render(<Demo />);
<script crossorigin src="https://unpkg.com/react#18/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#18/umd/react-dom.development.js"></script>
<div id="root"></div>
For example, you can check and use this hook:
https://usehooks-ts.com/react-hook/use-interval
export default function Component() {
// The counter
const [count, setCount] = useState<number>(0)
// Dynamic delay
const [delay, setDelay] = useState<number>(1000)
// ON/OFF
const [isPlaying, setPlaying] = useState<boolean>(false)
useInterval(
() => {
// Your custom logic here
setCount(count + 1)
},
// Delay in milliseconds or null to stop it
isPlaying ? delay : null,
)
const handleChange = (event: ChangeEvent<HTMLInputElement>) => {
setDelay(Number(event.target.value))
}
return (
<>
<h1>{count}</h1>
<button onClick={() => setPlaying(!isPlaying)}>
{isPlaying ? 'pause' : 'play'}
</button>
<p>
<label htmlFor="delay">Delay: </label>
<input
type="number"
name="delay"
onChange={handleChange}
value={delay}
/>
</p>
</>
)
}
Following is the code which increments the value at once after 4 sec, though I am expecting the batch of update should increment the valus after 4 sec only on multiple clicks.
Ex. - Let us say, I clicked the "Async Increase" button 5 times, then after 4 sec the counter increases to 1,2,3,4,5 but I want after 4 sec it should increment making it 1 then after 4 sec it should increment it to 2, then after 4 sec it should increase to 3 and so on.
Let me know how can I fix this.
Code -
const UseStateCounter = () => {
const [value, setValue] = useState(0);
const reset = () => {
setValue(0);
}
const asyncIncrease = () => {
setTimeout(() => {
setValue(prevValue => prevValue + 1);
}, 4000);
}
const asyncDecrease = () => {
setTimeout(() => {
setValue(prevValue => prevValue - 1);
}, 4000);
}
return <>
<section style={{margin: '4rem 0'}}>
<h3>Counter</h3>
<h2>{value}</h2>
<button className='btn' onClick={asyncDecrease}>Async Decrease</button>
<button className='btn' onClick={reset}>Reset</button>
<button className='btn' onClick={asyncIncrease}>Async Increase</button>
</section>
</>
};
export default UseStateCounter;
To do that, wait for the previous change to finish before you start the next one. For instance, one way to do that is with a promise chain; see comments:
// Promise-ified version of setTimeout
const timeout = (ms) => new Promise(resolve => setTimeout(resolve, ms));
const UseStateCounter = () => {
const [value, setValue] = useState(0);
// Remember the promise in a ref we initialize
// with a fulfilled promise
const changeRef = useRef(Promise.resolve());
/* Alternatively, if there's a lot of initialization logic
or object construction, you might use `null` above
and then:
if (!changeRef.current) {
changeRef.current = Promise.resolve();
}
*/
const reset = () => {
queueValueUpdate(0, false);
};
// A function to do the queued update
const queueValueUpdate = (change, isDelta = true) => {
changeRef.current = changeRef.current
// Wait for the previous one to complete, then
.then(() => timeout(4000)) // Add a 4s delay
// Then do the update
.then(() => setValue(prevValue => isDelta ? prevValue + change : change));
};
const asyncIncrease = () => {
queueValueUpdate(1);
};
const asyncDecrease = () => {
queueValueUpdate(-1);
};
// Sadly, Stack Snippets can't handle the <>...</> form
return <React.Fragment>
<section style={{ margin: '4rem 0' }}>
<h3>Counter</h3>
<h2>{value}</h2>
<button className='btn' onClick={asyncDecrease}>Async Decrease</button>
<button className='btn' onClick={reset}>Reset</button>
<button className='btn' onClick={asyncIncrease}>Async Increase</button>
</section>
</React.Fragment>;
};
export default UseStateCounter;
Live Example:
const {useState, useRef} = React;
// Promise-ified version of setTimeout
const timeout = (ms) => new Promise(resolve => setTimeout(resolve, ms));
const UseStateCounter = () => {
const [value, setValue] = useState(0);
// Remember the promise in a ref we initialize
// with a fulfilled promise
const changeRef = useRef(Promise.resolve());
/* Alternatively, if there's a lot of initialization logic
or object construction, you might use `null` above
and then:
if (!changeRef.current) {
changeRef.current = Promise.resolve();
}
*/
const reset = () => {
queueValueUpdate(0, false);
};
// A function to do the queued update
const queueValueUpdate = (change, isDelta = true) => {
changeRef.current = changeRef.current
// Wait for the previous one to complete, then
.then(() => timeout(4000)) // Add a 4s delay
// Then do the update
.then(() => setValue(prevValue => isDelta ? prevValue + change : change));
};
const asyncIncrease = () => {
queueValueUpdate(1);
};
const asyncDecrease = () => {
queueValueUpdate(-1);
};
// Sadly, Stack Snippets can't handle the <>...</> form
return <React.Fragment>
<section style={{ margin: '4rem 0' }}>
<h3>Counter</h3>
<h2>{value}</h2>
<button className='btn' onClick={asyncDecrease}>Async Decrease</button>
<button className='btn' onClick={reset}>Reset</button>
<button className='btn' onClick={asyncIncrease}>Async Increase</button>
</section>
</React.Fragment>;
};
ReactDOM.render(<UseStateCounter />, document.getElementById("root"));
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.2/umd/react-dom.development.js"></script>
Note: Normally I make a big noise about handling promise rejections, but none of the promise stuff above will ever reject, so I'm comfortable not bothering with catch in queueValueUpdate.
I am making a slider that has arrows to scroll through the items only if the items are too wide for the div. It works, except when resizing the window to make it large enough, the arrows don't disappear. The arrows should only appear if scroll arrows is true.
{scrollArrows && (
<div className="arrow arrow-left" onClick={goLeft}>
<
</div>
)}
But when I console.log it, even if the arrows are there, scroll arrows ALWAYS is false. Here is the relevant code:
import "./ProjectRow.css";
import Project from "../Project/Project";
import React, { useEffect, useState } from "react";
const ProjectRow = (props) => {
const rowRef = React.useRef();
const [hasLoaded, setHasLoaded] = useState(false);
const [scrollArrows, setScrollArrows] = useState(false);
const [left, setLeft] = useState(0);
const [rowWidth, setRowWidth] = useState(null);
function debounce(fn, ms) {
let timer;
return () => {
clearTimeout(timer);
timer = setTimeout(() => {
timer = null;
fn.apply(this, arguments);
}, ms);
};
}
useEffect(() => {
const setVariables = () => {
console.log(scrollArrows);
if (rowRef.current?.offsetWidth < rowRef.current?.scrollWidth) {
setRowWidth(rowRef.current.offsetWidth);
if (!scrollArrows) {
console.log("scrollArrows true now");
setScrollArrows(true);
}
} else if (scrollArrows) {
setScrollArrows(false);
}
};
if (!hasLoaded) setVariables();
const debouncedHandleResize = debounce(setVariables, 300);
window.addEventListener("resize", debouncedHandleResize);
setHasLoaded(true);
return () => {
window.removeEventListener("resize", debouncedHandleResize);
};
}, []);
const goLeft = () => {
const projectWidth = rowRef.current.childNodes[2].firstChild.offsetWidth;
if (rowRef.current.childNodes[2].firstChild.getBoundingClientRect().x < 0)
setLeft(left + projectWidth + 20);
};
const goRight = () => {
const projectWidth = rowRef.current.childNodes[2].firstChild.offsetWidth;
if (
rowRef.current.childNodes[2].lastChild.getBoundingClientRect().x +
projectWidth >
rowWidth
)
return setLeft(left - (projectWidth + 20));
};
return (
<div className="project-row" ref={rowRef}>
<h3 className="project-row-title light-gray bg-dark-gray">
{props.data.groupName}
</h3>
<hr className="gray-bar" />
<div className="slider" style={{ left: `${left}px` }}>
{props.data.projects.map((project, i) => (
<Project data={project} key={i} />
))}
</div>
{scrollArrows && (
<div className="arrow arrow-left" onClick={goLeft}>
<
</div>
)}
{scrollArrows && (
<div className="arrow arrow-right" onClick={goRight}>
>
</div>
)}
</div>
);
};
export default ProjectRow;
The "scroll arrows true now" gets logged, but scrollArrows variable stays false, even after waiting to resize or just resizing between two widths that need it.
EDIT: Fixed it by removing the if on the else if. I figured that might be the issue, but I don't know why it was preventing it from functioning properly, so it feels bad.
Your 'useEffect' doesn't look well structured. You have to structure it this way:
React.useEffect(()=>{
// Functions
window.addEventListener('resize', ...
return ()=>{
window.removeEventListener('resize', ...
}
},[])
Please follow this answer and create a Hook: Why useEffect doesn't run on window.location.pathname changes?
Try changing that and it might update your value and work.
I'm trying to use setInterval in React but stuck on something I don't properly understand.
The code is:
const Countdown = () => {
const [countdownSecond, setCountdownSecond] = React.useState(0);
function x() {
console.log(countdownSecond);
setCountdownSecond(countdownSecond + 1);
}
return (
<>
<button onClick={() => setInterval(x, 1000)}>Start</button>
{countdownSecond}
</>
);
}
The issue is that console always logs to 0. I'm not sure why is that. What concept am I misunderstanding?
You can do:
const Countdown = () => {
const [countdownSecond, setCountdownSecond] = React.useState(0);
const [start, setStart] = React.useState(false);
React.useEffect(()=>{
const interval = null
if (start) {
interval = setInterval(()=>{
setCountdownSecond(countdownSecond + 1);
}, 1000);
}
return ()=>{if (interval !== null) clearInterval(interval)};
},[start]);
function x(e) {
e.preventDefault();
console.log(countdownSecond);
setStart(!start);
}
return (
<>
<button onClick={(e) => x(e)}>{start?"Stop":"Start"}</button>
<p>Countdown: {countdownSecond}</p>
</>
);
}