In this blog, I attempt to create my own version of an animated counter component that I keep finding all over the web.
I’ll show you how I went about it, but I would love feedback. Do you know a better way to do this?
Leave a comment or shoot me an email at jason.melton2@gmail.com
.
Tutorial
Table of Contents
- Preliminary Junk
- Count Component
- Increment Function
- Conclusion
Preliminary Junk
I set up a create-react-app
, deleted a bunch of default stuff, and a file structure like this:
I added some basic CSS to the App component — height
, width
, and flex box
to center all its contents.
.App {
width: 100%;
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
}
I also set up a JSON file containing the data that I will map into Count components.
{
"counts": [
{
"id": "001",
"label": "A Really Big Number",
"number": "900000",
"duration": "2"
},
{
"id": "002",
"label": "Ice Cream Flavors",
"number": "5321",
"duration": "2"
},
{
"id": "003",
"label": "Customers Served",
"number": "100",
"duration": "2"
},
{
"id": "004",
"label": "Complaints",
"number": "0",
"duration": "2"
}
]
}
Count Component
The object of my Count component is to accept some data about how the count should run and render the animation.
First, I set up a basic component.
import React from 'react';
//styling
import './Count.css';
const Count = props => {
// label of counter
// number to increment to
// duration of count in seconds
const {label, number, duration } = props.data
return (
<div className="Count">
<h3>
<i>{label}: {number}</i>
</h3>
</div>
);
}
export default Count;
Count gets props of a data item from data.json. I destructured the label
, number
, and duration
from the props.
Using JSX, I return the label
and number
as a header.
Later, I will change number
so that it animates, but for now I can style the hard-coded version of what I’m building.
.Count {
padding: 2rem;
margin: 1rem;
border-radius: 2em;
box-shadow: 1px 2px 2px #0D3B66;
background-color: #FAF0CA;
display: flex;
align-items: center;
justify-content: center;
color: #0D3B66;
}
Increment Function
I set up a function that increments from 0 to the desired number in these three steps:
1) Set up a useState
hook that saves our display number and, when updated, will trigger a render of the component.
The hook looks like this:
// number displayed by component
const [count, setCount] = useState("0");
I update the JSX to display count
instead of number
.
return (
<div className="Count">
<h3>
<i>{label}: {count}</i>
</h3>
</div>
);
2) Set up a useEffect
hook that calculates the count and increment time.
useEffect()
takes an anonymous function that will handle the count. I create variables start
and end
. start
is set to 0.
Initially, I used number
as my end. However, for large numbers, this would take all night. Instead, I only increment the first three digits of the number and paste the the rest of back before updating the count.
I calculate the rate of each increment by dividing the duration (seconds) by the number of increments I plan on doing and multiply by 1000 to convert to milliseconds.
Image for post
useEffect(() => {
let start = 0;
// first three numbers from props
const end = parseInt(number.substring(0,3))
// if zero, return
if (start === end) return;
// find duration per increment
let totalMilSecDur = parseInt(duration);
let incrementTime = (totalMilSecDur / end) * 1000;
// dependency array
}, [number, duration]);
I was hoping to speed up the interval to make up for how long it would take to increment large numbers, but
setInterval()
has a minimum duration of 10 milliseconds. Any number less than 10 will reset back to 10.
3) In that same useEffect hook, I employ setInterval()
to increment the count with side effect of re-rendering the component.
I add one to start and call setCount()
to update my useState
hook. I convert start
to a string and, if it’s a large number, I concat the rest of the number that I previously chopped off.
// timer increments start counter
// then updates count
// ends if start reaches end
let timer = setInterval(() => {
start += 1;
setCount(String(start) + number.substring(3))
if (start === end) clearInterval(timer)
}, incrementTime);
The entire component will now look like this:
import React, { useEffect, useState } from 'react';
//styling
import './Count.css';
const Count = props => {
// label of counter
// number to increment to
// duration of count in seconds
const {label, number, duration } = props.data
// number displayed by component
const [count, setCount] = useState("0")
useEffect(() => {
let start = 0;
// first three numbers from props
const end = parseInt(number.substring(0,3))
// 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;
setCount(String(start) + number.substring(3))
if (start === end) clearInterval(timer)
}, incrementTime);
// dependency array
}, [number, duration]);
return (
<div className="Count">
<h3>
<i>{label}: {count}</i>
</h3>
</div>
);
}
export default Count;
Conclusion
I read through several articles about this sort of animation and combined their ideas with my instinct to make this abstract reusable component.
I am not sure what I came up with is the best method. For example setInterval
had limitations I didn’t foresee. I would love some feedback. Feel free to comment or shoot me an email at jason.melton2@gmail.com
.
Best, Jason
Top comments (1)
thanks for reading my dude