Antlers Posted October 29, 2020 Share Posted October 29, 2020 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 More sharing options...
Rodrigo Posted October 29, 2020 Share Posted October 29, 2020 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!!! 2 Link to comment Share on other sites More sharing options...
Antlers Posted October 29, 2020 Author Share Posted October 29, 2020 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 More sharing options...
Rodrigo Posted October 29, 2020 Share Posted October 29, 2020 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!!! 2 Link to comment Share on other sites More sharing options...
Antlers Posted October 29, 2020 Author Share Posted October 29, 2020 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 More sharing options...
Antlers Posted October 30, 2020 Author Share Posted October 30, 2020 Just thought of an edge case where this solution doesn't work. Lazy loaded images. Not all images can be preloaded because they are only loaded as needed. Does anyone have any other suggestions? Link to comment Share on other sites More sharing options...
Rodrigo Posted October 30, 2020 Share Posted October 30, 2020 I can think of two options. Use a lazyloading callback and the refresh method from ScrollTrigger. 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!!! 2 Link to comment Share on other sites More sharing options...
ZachSaucier Posted October 30, 2020 Share Posted October 30, 2020 Not accounting for dynamically added elements is one of the most common ScrollTrigger mistakes. 1 Link to comment Share on other sites More sharing options...
Antlers Posted October 30, 2020 Author Share Posted October 30, 2020 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 More sharing options...
ZachSaucier Posted October 30, 2020 Share Posted October 30, 2020 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. 1 Link to comment Share on other sites More sharing options...
Recommended Posts
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 accountSign in
Already have an account? Sign in here.
Sign In Now