Create a web app in Javascript or any framework of it of your choice (React, Angular, Vue), which takes a number and a duration as an input and prints the number starting from 0 incrementing it by 1 in the given duration.
Recommended: Please stop here and think of creating a solution yourself.
Number increment counter in React.
I am going to implement this solution in React, but feel free to choose any framework of your choice as ultimately every thing under the hood is javascript only.
First, lets create the functional component that will take input.
//App.js import React, { useState } from "react"; const App = () => { const [number, setNumber] = useState(0); const [duration, setDuration] = useState(0); const [start, setStart] = useState(false); //If any input changes reset const basicReset = () => { setStart(false); }; //store number const numberChangeHandler = (e) => { const { value } = e.target; setNumber(value); basicReset(); }; //store duration const durationChangeHandler = (e) => { const { value } = e.target; setDuration(value); basicReset(); }; const startHandler = () => { // trigger the animation }; const resetHandler = () => { window.location.reload(); }; return ( <main style={{ width: "500px", margin: "50px auto" }}> <section className="input-area"> <div> <div> <label htmlFor="number">Number:</label>{" "} <input id="number" type="number" value={number} onChange={numberChangeHandler} /> </div> <div> <label htmlFor="duration">Duration:</label>{" "} <input id="duration" type="number" value={duration} onChange={durationChangeHandler} /> </div> </div> <br /> <div> <button onClick={startHandler}>start</button>{" "} <button onClick={resetHandler}>reset</button> </div> </section> </main> ); }; export default App;
Solution 1 : Using setInterval.
If you are a straight forward developer like me who likes to try the familiar approaches, then the first thing that comes to your mind is using a setInterval function.
It is extremely simple to come up with a solution using setInterval, all we have to do is,
Calculate the time interval at which the setInterval should be called in order to increment the number.
We can do that with this simple formula (duration / number) * 1000
, for example (2 / 1000) * 1000
= 2
, which means we have to increment the counter every 2 milliseconds to reach from 0 to 1000 in 2 seconds.
Now there are two ways in which you can implement this in react,
- Ref to the DOM element and increment the count directly in each interval call.
- Update the state and let react update the count.
Both of these approaches does not affect the time because all we are doing is updating a single DOM element, if we had to update multiple nested DOM elements then we should be using the second approach.
Using setInterval
//CountMethods.js import React, { useEffect, useState, useRef } from "react"; //setInterval const CountSetInterval = (props) => { const intervalRef = useRef(); const countRef = useRef(); // label of counter // number to increment to // duration of count in seconds const { number, duration } = props; // number displayed by component const [count, setCount] = useState("0"); // calc time taken for computation const [timeTaken, setTimeTaken] = useState(Date.now()); useEffect(() => { let start = 0; // first three numbers from props const end = parseInt(number); // if zero, return if (start === end) return; // find duration per increment let totalMilSecDur = parseInt(duration); let incrementTime = (totalMilSecDur / end) * 1000; // timer increments start counter // then updates count // ends if start reaches end let timer = setInterval(() => { start += 1; //update uisng state setCount(String(start)); //update using ref // countRef.current.innerHTML = start; if (start === end) { clearInterval(timer); const diff = Date.now() - timeTaken; setTimeTaken(diff / 1000); //uncomment this when using ref // setCount(String(start)); } }, incrementTime); // dependency array }, [number, duration]); return ( <> <span ref={countRef} className="Count"> {count} </span>{" "} {" "} {number === count && ( <span> | Took : <b>{timeTaken}</b> seconds to complete </span> )} </> ); };
I have also added a time log to determine exactly how much time it takes to increment the count in order to make sure we are progressing in right direction.
Let’s call this function on the click of start button inside input function.
import React, { useState } from "react"; import { CountSetInterval } from "./CountMethods"; const App = () => { const [number, setNumber] = useState(0); const [duration, setDuration] = useState(0); const [start, setStart] = useState(false); //If any input changes reset const basicReset = () => { setStart(false); }; //store number const numberChangeHandler = (e) => { const { value } = e.target; setNumber(value); basicReset(); }; //store duration const durationChangeHandler = (e) => { const { value } = e.target; setDuration(value); basicReset(); }; const startHandler = () => { // trigger the animation setStart(true); }; const resetHandler = () => { window.location.reload(); }; return ( <main style={{ width: "500px", margin: "50px auto" }}> <section className="input-area"> <div> <div> <label>Number:</label>{" "} <input type="number" value={inputValue} onChange={inputChangeHandler} /> </div> <div> <label>Duration:</label>{" "} <input type="number" value={duration} onChange={durationChangeHandler} /> </div> </div> <br /> <div> <button onClick={startHandler}>start</button>{" "} <button onClick={resetHandler}>reset</button> </div> </section> <br /> <section className="result-area"> <div> SetInterval:{" "} {(start && ( <CountSetInterval label={"count"} number={inputValue} duration={parseInt(duration)} /> )) || 0} </div> </section> </main> ); }; export default App;
Output
Weird!, right?
It is taking longer than we expected to increment the count even though we are doing every thing properly.
Well, it turns out that setInterval function is not behaving as we have thought it should.
Using setTimeout
Lets change the approach and try to implement the same logic using setTimeout.
We can mimic the setInterval function using setTimeout by recursively calling the same function.
Using the same calculation, lets implement this.
//setTimeout const CountSetTimeout = (props) => { const intervalRef = useRef(); const countRef = useRef(); // label of counter // number to increment to // duration of count in seconds const { number, duration } = props; // number displayed by component const [count, setCount] = useState("0"); // calc time taken for computation const [timeTaken, setTimeTaken] = useState(Date.now()); useEffect(() => { let start = 0; // first three numbers from props const end = parseInt(number); // if zero, return if (start === end) return; // find duration per increment let totalMilSecDur = parseInt(duration); let incrementTime = (totalMilSecDur / end) * 1000; // timer increments start counter // then updates count // ends if start reaches end let counter = () => { intervalRef.current = setTimeout(() => { start += 1; //update using state setCount(String(start)); //update using ref // countRef.current.innerHTML = start; counter(); if (start === end) { clearTimeout(intervalRef.current); const diff = Date.now() - timeTaken; //uncomment this when using ref // setCount(String(start)); setTimeTaken(diff / 1000); } }, incrementTime); }; //invoke counter(); // dependency array }, [number, duration]); return ( <> <span ref={countRef} className="Count"> {count} </span>{" "} {" "} {number === count && ( <span> | Took : <b>{timeTaken}</b> seconds to complete </span> )} </> ); };
Let us see what happens when we invoke this function on the click of start button.
import React, { useState } from "react"; import { CountSetTimeout } from "./CountMethods"; const App = () => { const [number, setNumber] = useState(0); const [duration, setDuration] = useState(0); const [start, setStart] = useState(false); //If any input changes reset const basicReset = () => { setStart(false); }; //store number const numberChangeHandler = (e) => { const { value } = e.target; setNumber(value); basicReset(); }; //store duration const durationChangeHandler = (e) => { const { value } = e.target; setDuration(value); basicReset(); }; const startHandler = () => { // trigger the animation setStart(true); }; const resetHandler = () => { window.location.reload(); }; return ( <main style={{ width: "500px", margin: "50px auto" }}> <section className="input-area"> <div> <div> <label>Number:</label>{" "} <input type="number" value={inputValue} onChange={inputChangeHandler} /> </div> <div> <label>Duration:</label>{" "} <input type="number" value={duration} onChange={durationChangeHandler} /> </div> </div> <br /> <div> <button onClick={startHandler}>start</button>{" "} <button onClick={resetHandler}>reset</button> </div> </section> <br /> <section className="result-area"> <div> SetInterval:{" "} {(start && ( <CountSetTimeout label={"count"} number={inputValue} duration={parseInt(duration)} /> )) || 0} </div> </section> </main> ); }; export default App;
Output
This is taking more time than the setInterval.
Why is this happening?.
If you read the definition of each of these methods you will realize that.
- setTimeout: This method sets a timer which executes a function or specified piece of code once the timer expires.
- setInterval: This method repeatedly calls a function or executes a code snippet, with a fixed time delay between each call.
Which means the time specified for either of these functions are the minimum time, but it can take longer than that.
After some research on MDN, I found out that there are two major reasons which is causing the delay.
1. Clamping.
In modern browsers, setTimeout()/setInterval() calls are throttled to a minimum of once every 4 ms when successive calls are triggered due to callback nesting (where the nesting level is at least a certain depth), or after certain number of successive intervals.
2.Execution context
The timer can also fire later when the page (or the OS/browser itself) is busy with other tasks. One important case to note is that the function or code snippet cannot be executed until the thread that called setTimeout() has terminated.
It turns out that, setTimeout or setInterval function won’t function properly when
- Delay is less than 4 ms.
- There is execution happening inside timer functions which is blocking the next execution.
If we can somehow avoid using the timer functions, we should be able to solve this problem. But is there a way without using them?.
After googling for some time, I found out that there is a solution for this.
Using requestAnimationFrame method.
Using this method we can come up with a solution which would increment the count from 0 to the specified number in given duration.
First, let us understand what is this method.
According to MDN –
The window.requestAnimationFrame() method tells the browser that you wish to perform an animation and requests that the browser calls a specified function to update an animation before the next repaint. The method takes a callback as an argument to be invoked before the repaint.
In simple terms what this method does is ask’s browser to perform animation, which in turn refreshes the screen very fast. At-least 60 frames per second to perform animations.
Now how this is useful in creating a increment counter?.
Read this text from MDN for better understanding.
You should call this method whenever you’re ready to update your animation onscreen. This will request that your animation function be called before the browser performs the next repaint. The number of callbacks is usually 60 times per second, but will generally match the display refresh rate in most web browsers as per W3C recommendation.
This function takes a callback function and pass current timestamp to that callback function as argument.
Now using a startTime
variable which stores the time before invoking the function and this current timestamp which we receive in the callback every time, we can recursively invoke this function for the given duration and using a good calculation we can increment the count.
Considering at-least 60 frames are refreshed in one second, which means if we have to count from 0 to 1000 in 1 second we should be incrementing the number by 60/1000 = ~16.667
using which we can come with clever calc which will increment the number based on much time has passed of animation.
Depend upon how often this function is invoked we will see an increment animation happening on the screen.
For bigger numbers this is not incrementing the count by 1 but still the animation is happening so fast that human eyes will not be able to differentiate.
Note:- In gif you should be able to see this.
//Animation const countAnimate = (obj, initVal, lastVal, duration) => { let startTime = null; //get the current timestamp and assign it to the currentTime variable let currentTime = Date.now(); //pass the current timestamp to the step function const step = (currentTime) => { //if the start time is null, assign the current time to startTime if (!startTime) { startTime = currentTime; } //calculate the value to be used in calculating the number to be displayed const progress = Math.min((currentTime - startTime) / duration, 1); //calculate what to be displayed using the value gotten above obj.innerHTML = Math.floor(progress * (lastVal - initVal) + initVal); //checking to make sure the counter does not exceed the last value (lastVal) if (progress < 1) { window.requestAnimationFrame(step); } else { window.cancelAnimationFrame(window.requestAnimationFrame(step)); // add time diff const diff = currentTime - startTime; const elm = document.createElement("SPAN"); elm.innerHTML = ` | Took : <b>${diff / 1000}</b> seconds to complete`; obj.appendChild(elm); } }; //start animating window.requestAnimationFrame(step); };
Lets test it out.
import React, { useState, useRef } from "react"; import { countAnimate } from "./CountMethods"; const App = () => { const [number, setNumber] = useState(0); const [duration, setDuration] = useState(0); const [start, setStart] = useState(false); const countRef = useRef(); //If any input changes reset const basicReset = () => { setStart(false); countRef.current.innerHTML = "0"; }; //store number const numberChangeHandler = (e) => { const { value } = e.target; setNumber(value); basicReset(); }; //store duration const durationChangeHandler = (e) => { const { value } = e.target; setDuration(value); basicReset(); }; const startHandler = () => { // trigger the animation setStart(true); countAnimate( countRef.current, 0, parseInt(inputValue), parseInt(duration) * 1000 ); }; const resetHandler = () => { window.location.reload(); }; return ( <main style={{ width: "500px", margin: "50px auto" }}> <section className="input-area"> <div> <div> <label>Number:</label>{" "} <input type="number" value={inputValue} onChange={inputChangeHandler} /> </div> <div> <label>Duration:</label>{" "} <input type="number" value={duration} onChange={durationChangeHandler} /> </div> </div> <br /> <div> <button onClick={startHandler}>start</button>{" "} <button onClick={resetHandler}>reset</button> </div> </section> <br /> <section className="result-area"> <div> Animate: <span ref={countRef}>0</span> </div> </section> </main> ); }; export default App;
Output
It is working fine, isn’t it?.
Comparison
Watch this video to get better understanding of animations in browser.
Please try it out yourself, code is available on github repo.
I hope you learned something today, if you think this would be helpful for others, please do share it. If you have any other approach let me know in the comment section.