React countup animation starts immediately after the page loading , should start when scrolled to the component (without jquery)
Asked Answered
R

12

19

I have a react single page app, with multiple components. For the 5th component(visible only when scrolled down) I have a counter . Now I am using react-countup library to achieve the counter function. However , the counter starts soon as the page is loaded . Is it possible for countup to begin once we scroll down to the component. Animation happens only once(which is good)after the page is loaded, but I would like the counter not to begin soon after the page is loaded, but when user scrolls down to the component the first time. My code looks like this:

    render() {
         return (
         <div className={style.componentName}>
         <h2>Heading</h2>
         <div className={style.col}>
         <div>My counter</div>
         <CountUp className={style.countup} decimals={1} start={0} end={25} suffix=" %" duration={3} />
        </div>
        </div>)}

Updated code:

    import CountUp, { startAnimation } from 'react-countup';
    import VisibilitySensor from 'react-visibility-sensor';

    class className extends Component {

        state = {
            scrollStatus: true
        };


        onVisibilityChange = isVisible => {
            if (isVisible) {
                if (this.state.scrollStatus) {
                    startAnimation(this.myCountUp);
                    this.setState({ scrollStatus: false });
                }
            }
        }
    render() {
             return (
            <div className={style.componentName}>
             <h2>Heading</h2>
             <VisibilitySensor onChange={this.onVisibilityChange} offset = {{ top: 
              10}} delayedCall>
             <CountUp className={style.countup} decimals={1} start={0} end={25} 
             suffix=" %" duration={3} ref={countUp => { this.myCountUp= countUp;}}/>
             </VisibilitySensor>
            </div>)}
}
Roter answered 26/6, 2018 at 13:27 Comment(0)
M
35

The API may have changed since last year. I manage to make this work with this code now :

import React from "react";
import CountUp from "react-countup";
import VisibilitySensor from 'react-visibility-sensor';

const MyComponent = () => (
  <>
    <CountUp end={100} redraw={true}>
        {({ countUpRef, start }) => (
            <VisibilitySensor onChange={start} delayedCall>
                <span ref={countUpRef} />
            </VisibilitySensor>
        )}
    </CountUp>
  </>
);

export default App;

I use this component inside a tab, so the redraw={true} prop is only here to redraw the animation on tabChange.

Modena answered 3/9, 2019 at 15:10 Comment(7)
this should be answer with current versionJerk
How do I make it run just once istead of everytime it enters viewport?Verbalize
Clean and simple. Thanks.Pleasance
Works for me after hours of fighting with the two libraries trying other approaches. Thanks!Putnem
findDOMNode is deprecated in StrictMode and work arounds?Quezada
export default not should be export default MyComponent ?Bobstay
It is worth mentioning that in React CountUp 5.X there is a prop called enableScrollSpy which enables starting the animation when target is in view. Thanks to @coding-spidy and his answer link for highlighting this solution which is the easiest and simplest one.Brandenburg
B
9

Per React CountUp's README, you can use the startAnimation hook to manually kick off the animation. Combine this with something like react-visibility-sensor, and you can wait to kick off the animation until it is visible in the user's browser.

import React, {Component} from 'react';
import CountUp, {startAnimation} from 'react-countup';
import './App.css';
import VisibilitySensor from 'react-visibility-sensor';

const style = {
  componentName: {},
  col: {},
  countup: {},
};

class App extends Component {
  constructor(props) {
    super(props);
    this.onVisibilityChange = this.onVisibilityChange.bind(this); // Bind for appropriate 'this' context
  }

  onVisibilityChange(isVisible) {
    if (isVisible) {
      startAnimation(this.myCountUp);
    }
  }

  render() {
    return (
      <div className={style.componentName}>
        <h2>Heading</h2>
        <div className={style.col}>
          <div>My counter</div>
          <VisibilitySensor
            onChange={this.onVisibilityChange}
            delayedCall // Prevents react apps triggering elements as visible before styles are loaded
          >
            <CountUp className={style.countup} decimals={1} start={0} end={25} suffix=" %" duration={3}
                     ref={countUp => { this.myCountUp = countUp; }} // From react-countup README 
            />
          </VisibilitySensor>
        </div>
      </div>
    );
  }
}

export default App;

As is, it will startAnimation every time you scroll to the countup. If you want to only do that once, just add a piece of state that gets set after the first render (and then prevent it from doing startAnimation again based on that altered state).

Less elegant (not recommended) ways to accomplish the same effect might include:

  • Use the built-in animation triggers (i.e. changing the props duration, end, start) by setting them equal to some state that changes when the user scrolls down
  • Leveraging the onStart prop, called before the animation starts, to delay starting the animation until the user scrolls down

EDIT: Update to address your second question

Unfortunately, it looks like the react-countup library doesn't expose a way to prevent startAnimation on startup.

But we can hack together a fairly elegant fix by manipulating the end prop using state instead:

import React, {Component} from 'react';
import CountUp, {startAnimation} from 'react-countup';
import './App.css';
import VisibilitySensor from 'react-visibility-sensor';

const style = {
  componentName: {},
  col: {},
  countup: {},
};

class App extends Component {
  state = {
    didViewCountUp: false
  };


  onVisibilityChange = isVisible => {
    if (isVisible) {
      this.setState({didViewCountUp: true});
    }
  }

  render() {
    return (
      <div className={style.componentName}>
        <h2 style={{fontSize: '40em'}}>Heading</h2>
        <VisibilitySensor onChange={this.onVisibilityChange} offset={{
          top:
            10
        }} delayedCall>
          <CountUp className={style.countup} decimals={1} start={0} end={this.state.didViewCountUp ? 25 : 0}
                   suffix=" %" duration={3} />
        </VisibilitySensor>
      </div>)
  }
}

export default App;
Bahaism answered 26/6, 2018 at 14:20 Comment(3)
used state to set a value after first render. It works perfectly! Thank you very much. :) But one small bug,the counter at the beginning is at end count=25 and animation starts on scroll to the component. i.e there's a small amount of time before animation of 0-25 start.Roter
Could you edit your question with the updated code? Will help to try it out and maybe can suggest a fix in the context of what you haveBahaism
I updated the code. it is exactly as the code that ur answer. Using state to do the "startAnimation" just once.Roter
V
4

Here's my implementation. It just runs once and also doesn't re-render every time the component enters viewport to check visibility.

Dependencies:
1. react-countup v.4.3.2
2. react-visibility-sensor v.5.1.1

import React, { useState } from "react";
import CountUp from "react-countup";
import VisibilitySensor from "react-visibility-sensor";

const Ticker = ({ className, ...rest }) => {
  const [viewPortEntered, setViewPortEntered] = useState(false);

  return (
    <CountUp {...rest} start={viewPortEntered ? null : 0}>
      {({ countUpRef }) => {
        return (
          <VisibilitySensor
            active={!viewPortEntered}
            onChange={isVisible => {
              if (isVisible) {
                setViewPortEntered(true);
              }
            }}
            delayedCall
          >
            <h4 className={className} ref={countUpRef} />
          </VisibilitySensor>
        );
      }}
    </CountUp>
  );
};

export default Ticker;

Here's how to use it:

<Ticker className="count" end={21} suffix="M+" />
Verbalize answered 17/1, 2020 at 11:49 Comment(0)
L
4

Simply add below prop

enableScrollSpy: true

<CountUp enableScrollSpy={true} end={75}/>
Largo answered 11/11, 2022 at 10:43 Comment(1)
Easiest solution of all as it's built-in already. Thank you for saving hours.Wrought
B
3

Here is my solution using class based component Note: import two Libraries first run this code react-visibility-sensor react-countup

import React from "react";
import CountUp from "react-countup";
import VisibilitySensor from 'react-visibility-sensor';

class CountDown extends React.Component {
    render() {
        return (
            
                <React.Fragment>
                    <CountUp start={0} end={100} prefix="+" duration="2">

                        {({ countUpRef, start }) => (
                            <VisibilitySensor onChange={start} delayedCall>
                                <span ref={countUpRef} />
                            </VisibilitySensor>
                        )}

                    </CountUp>
                </React.Fragment>
           

        )
    }
}

export default CountDown;
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
Battue answered 9/7, 2021 at 5:22 Comment(0)
E
1

The docs for that library have a way to manually start the counter. I would use that approach to start the counter once a user has scrolled to the required distance.

import React, { Component } from 'react';
import CountUp, { startAnimation } from 'react-countup';

const MyComponent = () => (
  <div>
    <CountUp className="CountUp" start={0} end={100} duration={3} ref={(countUp) => {
      this.myCountUp = countUp;
    }} />
    <button className="Button" onClick={(event) => {
      startAnimation(this.myCountUp);
    }}>Count me up!</button>
  </div>
);

export default App;

Link to Github. Read the README at the very bottom.

Endodontist answered 26/6, 2018 at 14:17 Comment(0)
F
1

you can have look at my functional component to achieve this

import React from "react";
    import { Box } from "@material-ui/core";
    import CountUp from "react-countup";
    import VisibilitySensor from "react-visibility-sensor";

    export default function Counter() {
      const [focus, setFocus] = React.useState(false);
      return (
        <Box component="div">
          <CountUp start={focus ? 0 : null} end={100} duration={5} redraw={true}>
            {({ countUpRef }) => (
              <div>
                <span ref={countUpRef} />
                <VisibilitySensor
                  onChange={isVisible => {
                    if (isVisible) {
                      setFocus(true);
                    } 
                  }}
                >
                  <a>+</a>
                </VisibilitySensor>
              </div>
            )}
          </CountUp>
        </Box>
      );
    }
Flux answered 8/1, 2020 at 5:25 Comment(2)
This runs everytime component enters viewport while scrolling. Doesn't answer the question asked.Verbalize
hi @Sushmit I have edited the code you can take a look at it, I think it resolves your issueFlux
P
0

react-visibility-sensor doesn't seem to be maintained at the moment and produces warnings related to findDOMNode which is deprecated.

I've used react-waypoint in my functional component which makes the animation only trigger once when the user has the child of the waypoint component in view.

    // Ticker.tsx

import React, { useState } from "react";
import CountUp from "react-countup";
import { Waypoint } from "react-waypoint";

type TickerProps = {
  end: number;
  suffix: string;
}

const Ticker: React.FC<TickerProps> = ({ end, suffix }) => {
  const [viewPortEntered, setViewPortEntered] = useState(false);

  const onVWEnter = () => {
    setViewPortEntered(true);
  }

    return (
      <Waypoint onEnter={onVWEnter} >
        <div>
          {viewPortEntered && <CountUp end={end} suffix={suffix} start={0} /> }
        </div>
      </Waypoint>
    );
};

export default Ticker;
Puglia answered 1/3, 2022 at 17:3 Comment(0)
F
0
<CountUp
        className={styles.number}
        start={0}
        end={number}
        enableScrollSpy={true}
        scrollSpyDelay={0}
        suffix={index !== -1 ? "+" : ""}
/>

try this scroll spy should do the work

Fistulous answered 7/11, 2022 at 4:59 Comment(0)
T
0

Here's an alternative solution using react-in-viewport package. Idea is to define this element and use <CountUpInViewport> instead of <CountUp>.

This example fires on the first entry. One could do variations on this to refire every time it enters/leaves the viewport.


import React from 'react';
import { useRef } from 'react';
import CountUp from 'react-countup';
import { useInViewport } from 'react-in-viewport';

let CountUpInViewport = props => {
  const ref = useRef();
  const { enterCount } = useInViewport(ref);
  return (
    <div ref={ref}>
      {
        enterCount > 0 ?
        <CountUp key='in' {...props}></CountUp> :
        <CountUp key='out' {...props}></CountUp>  // ensures drawn when not visible
      }
    </div>
  );
}
Tyro answered 8/1, 2023 at 6:48 Comment(0)
I
0

You can simply add the enableScrollSpy flag to achieve this.

<CountUp end={25} enableScrollSpy />

Also, If you want to start the animation every time it comes into focus, add the scrollSpyOnce flag and make it false.

<CountUp end={25} enableScrollSpy scrollSpyOnce={false} />
Intussuscept answered 14/12, 2023 at 5:45 Comment(0)
S
0

wrap it with <Suspense></Suspense>, this worked for me in nextjs

Suellensuelo answered 17/5 at 5:49 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.