Search code examples
javascriptreactjscounter

Using JavaScript to create an animated counter with React.js


I have four counters that I would like to animate (incrementing the count from 0 to a specific number) using JavaScript. My code is the following:

const allCounters = document.querySelectorAll('.counterClass');
counters.forEach(allCounters => {
  const updateCounter = () => {
    const end = +allCounters.getAttribute('data-target');
    const count = +allCounters.innerText;
    const increment = end / 200;
    if (count < end) {
      allCounters.innerText = count + increment;
      setTimeout(updateCounter, 1);
    } else {
      allCounters.innerText = end;
    }
  };
  updateCounter();
});

In React, I wasn't sure how to get it to run. I tried including the code after the using dangerouslySetInnerHTML, but that's not working. (I'm new to React).

I appreciate any assistance you could give me. Thanks so much!

Right before I posted my question, I found a plug-in (https://github.com/glennreyes/react-countup) that could do it, but wondered if it's still possible using JS. Thanks!


Solution

  • While using React, try to avoid direct DOM operations (both query and modifications). Instead, let React do the DOM job:

    const Counter = ({start, end}) = {
      // useState maintains the value of a state across
      // renders and correctly handles its changes
      const {count, setCount} = React.useState(start);
      // useMemo only executes the callback when some dependency changes
      const increment = React.useMemo(() => end/200, [end]);
    
      // The logic of your counter
      // Return a callback to "unsubscribe" the timer (clear it)
      const doIncrement = () => {
        if(count < end) {
          const timer = setTimeout(
            () => setCount(
              count < (end - increment)
                ? count + increment
                : end
            ),
            1);
          return () => clearTimeout(timer);
        }
      }
    
      // useEffect only executes the callback once and when some dependency changes
      React.useEffect(doIncrement, [count, end, increment]);
    
      // Render the counter's DOM
      return (
        <div>{count}</div>
      )
    }
    
    const App = (props) => {
      // Generate example values:
      // - Generate 5 counters
      // - All of them start at 0
      // - Each one ends at it's index * 5 + 10
      // - useMemo only executes the callback once and
      //   when numCounters changes (here, never) 
      const numCounters = 5;
      const countersExample = React.useMemo(
        () => new Array(numCounters)
          .fill(0)
          .map( (c, index) => ({
            start: 0,
            end: index*5 + 10,
          })
        ),
        [numCounters]
      );
    
      return (
        <div id="counters-container">
          {
            // Map generated values to React elements
            countersExample
              .map( (counter, index) => <Counter key={index} {...counter}/> )
          }
        </div>
      )
    }