Jump to content
Search Community

Issue using scrolltrigger with multiple timelines in react components

Antlers test
Moderator Tag

Recommended Posts

Hi all,

 

We've been playing with scrolltrigger to fade elements in and out as they enter and leave the viewport. We're using react and are creating a timeline inside each react component, a page may be constituted of any number of these components. Each component is initialized after loading data via an API. So there's a loading delay.

 

The issue is that the components don't always load at the same time. Sometimes components at the top of the page will load after the components at the bottom of the page, depending on the API response speed. So the components at the bottom of the page initialize the GSAP timeline before the page above is fully rendered (either because the data or images ar still laoding), which then breaks the start/end points of the scrolltrigger. This results in some components fadding out too soon and therefore being blank as you scroll. We've tried adding a timeout to the GSAP initialization on each component and this works. If we init GSAP after a few seconds, the scrolltrigger works as expected but obviously this isn't a very accurate way of doing things as pages may take longer or less to load.

 

We're wondering what's the best work around this issue. Do we need to preload all the components, data and images before rendering and before calling GSAP? This would be a huge refactor and major work just to get animations to work. Are there any easier solutions available we're not aware of?

 

I'm sorry for not providing an example, this would not be a simple demo to produce since it involves multiple components and API end points. Hopefully someone can give us some pointers based on the info provided. I hope it makes sense.

Link to comment
Share on other sites

Hi and welcome to the GreenSock forums.

 

You should wait for the API response to come  back to render the components (conditional rendering), since you don't need to show them before the data is loading, you can create a loading boolean and depending on that render a loading component or the actual components. If you're using hooks you can use a useEffect when the loading boolean is updated in order to initialize your GSAP code. This is pretty much the workflow when I use Apollo with React and GraphQL, the apollo client provides a couple of hooks that are very useful to do that.

 

Of course this will depend on what exactly you're using to fetch your data, but the general idea would be something like this:

import React, { useEffect, useState } from "react";
import axios from "axios";

const App = () => {
  const [loading, setLoading] = useState(true);
  
  useEffect(() => {
    axios.get("your.api.url")
      .then((res) => {
        // this could be undefined if you want to use a more strict check like typeof
        setLoading(false);
      })
      .catch((err) => console.log(err));
  }, []);
  
  useEffect(() => {
    if (!loading) {
      // Init GSAP code here
    }
  }, [loading]);
  
  if (loading) {
    return <h1>Loading...</h1>;
  }
  return <div>
    // Regular content here
  </div>;
};

This approach uses axios for getting the data, but that will depend on the approach you're using.

 

Happy Tweening!!!

  • Like 2
Link to comment
Share on other sites

Thanks for your reply. I feel like a dummy because I didn't explain myself very well. We are initializing GSAP after loading the data but at the point the images haven't loaded yet. It's the images loading and shifting the DOM height that breaks the GSAP animations below that point. I've pasted an example component below. Hopefully it helps.

 

 

import { getLocalisationStrings, getTextIntro } from 'lib/services';
import { checkForMissingLocalisationStrings } from 'lib/utils/utils';
import { gsap } from 'gsap';
import React from 'react';
import TextIntro from '../components/common/TextIntro';

const i18nCodes = [
  'TextIntro.Title',
  'TextIntro.Text',
  'TextIntro.ButtonLabel'
];

class TextIntroContainer extends React.Component {
  constructor(props) {
    super(props);

    this.uid = `${this.props.attrs.apiId}__${Math.ceil(Math.random() * 100000)}`;

    this.state = {
      appStatus: 'init' // Can be 'init', 'busy' or 'ready'
    };
  }

  componentDidMount() {
    this.loadData();
  }

  loadData() {
    this.setState(() => ({
      appStatus: 'init'
    }));

    Promise.all([
      getLocalisationStrings(undefined, i18nCodes).promise,
      getTextIntro(undefined, this.props.attrs.apiId).promise
    ])
      .then(responses => {
        const i18n = checkForMissingLocalisationStrings(responses[0].translations);
        const data = responses[1];

        this.setState(() => ({
          i18n,
          data,
          appStatus: 'ready'
        }));

        this.setAnimation();
      })
      .catch(() => {
        this.setState(() => ({
          appStatus: 'error'
        }));
      });
  }

  setAnimation() {
    // target only this component by ID
    const id = `.text-intro${this.uid}`;

    // animation timeline connected to scroll
    const tl = gsap.timeline({
      scrollTrigger: {
        trigger: id,
        start: 'center 90%',
        end: 'center 10%',
        scrub: true
        // markers: true
      }
    });

    // fade in
    tl.from(`${id} .text-intro__title`, {
      duration: 4, y: '+=50', autoAlpha: 0, ease: 'Circ.easeOut'
    });
    tl.from(`${id} .text-intro__text`, {
      duration: 4, y: '+=50', autoAlpha: 0, ease: 'Circ.easeOut'
    }, '-=3');
    tl.from(`${id} .text-intro__button`, {
      duration: 4, y: '+=50', autoAlpha: 0, ease: 'Circ.easeOut'
    }, '-=3');
    // fade out
    tl.to(`${id} .text-intro__title`, {
      duration: 4, y: '-=50', autoAlpha: 0, ease: 'Circ.easeIn'
    }, '+=8');
    tl.to(`${id} .text-intro__text`, {
      duration: 4, y: '-=50', autoAlpha: 0, ease: 'Circ.easeIn'
    }, '-=3');
    tl.to(`${id} .text-intro__button`, {
      duration: 4, y: '-=50', autoAlpha: 0, ease: 'Circ.easeIn'
    }, '-=3');
  }

  render() {
    return (
      <>
        <TextIntro
          id={this.uid}
          i18n={this.state.i18n}
          data={this.state.data}
          modifiers='text-intro--small'
        />
      </>
    );
  }
}

export default TextIntroContainer;

 

Link to comment
Share on other sites

Then just create a simple image loader. If you have all the images' urls then you can create an array with them, loop through it and preload the images:

const images = [];
const loaded = false;

const loadPromises = images.map((image) => {
  return new Promise((resolve, reject) => {
    const img = new Image();
    img.src = image;
    img.onload = () => resolve(true);
  });
});

Promise.all(loadPromises)
  .then(() => {
    loaded = true;
  })
  .catch((err) => console.log(err));

That's how I'd do it.

 

Happy Tweening!!!

  • Like 2
Link to comment
Share on other sites

That's not a bad idea but would need to be a global loader. Not sure that would work withc ompoenets loading at different speeds. When the component loads the date, registers any images with the gloabl loader and when all the images in the image loader have loaded an event is dispatched to initialize all GSAp animations in all components. It might work. Thank you.

Link to comment
Share on other sites

I can think of two options.

 

  1. Use a lazyloading callback and the refresh method from ScrollTrigger.
  2. Use static height for your images with CSS and media queries, placeholder images for the ones still loading and a preloader indicator/spinner. This is normally referred as optimistic UI. Also you avoid layout shifting, something I really, REALLY don't like.

Happy Tweening!!!

  • Like 2
Link to comment
Share on other sites

12 minutes ago, ZachSaucier said:

Not accounting for dynamically added elements is one of the most common ScrollTrigger mistakes.

Oh I wasn't aware of that Scrolltrigger.refresh() method. How taxing would it be to call it everytime each component has finished rendering?

 

If I have 8 components on a page, each dispatching a global event when ready, and each component listening to that event and calling Scrolltrigger.refresh(). Would calling that method 8 times x 8 components slow down the user experience?

 

Link to comment
Share on other sites

4 minutes ago, Antlers said:

Would calling that method 8 times x 8 components slow down the user experience?

There's only one way to know for sure: test :) I'm guessing it's fine. But if you wanted to avoid it you could try and keep track of how many components have loaded and only refresh when all of them have loaded. I'm not great with React so I don't know how to do that off the top of my head.

  • Like 1
Link to comment
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now
  • Recently Browsing   0 members

    • No registered users viewing this page.
×
×
  • Create New...