Jump to content
Search Community

ScrollTrigger Start position does not change if other component are changing height in React

Pollux Septimus
Moderator Tag

Recommended Posts

Pollux Septimus
Posted

Hello,

I am using ScrollTrigger in React and I've noticed that the start position doesn't update when a component before it changes height. I've tried refreshing the ScrollTrigger on Complete, but since the different sections are in different components, this doesn't work for me.  Sorry for the lack of a minimal demo but the animations are a bit complex for me to be able to quickly put a minimal demo together. 

Component that changes size

import { useState, useEffect, useRef } from 'react';
import gsap from 'gsap';
import { ScrollTrigger } from 'gsap/all';

import useIsMobile from 'hooks/useIsMobile';

import styles from './styles.module.css';

const Tabs = ({ config }) => {
  const [activeTab, setActiveTab] = useState(0);
  const [tabsHeight, setTabsHeight] = useState();

  const isMobile = useIsMobile(501);
  const isTablet = useIsMobile(1200);

  const tl = useRef();
  const tabsContainerRef = useRef();
  const tabsBG = useRef();

  useEffect(() => {
    setTabsHeight(tabsBG.current.getBoundingClientRect().height);
  }, [isMobile]);

  useEffect(() => {
    gsap.registerPlugin(ScrollTrigger);

    const context = gsap.context(() => {
      tl.current = gsap.timeline({
        scrollTrigger: {
          trigger: tabsContainerRef.current,
          start: 'top bottom-=50',
          end: 'bottom',
          toggleActions: 'restart none none reverse',
        },
      });

      tl.current
        .from(tabsBG.current, {
          width: 45,
          height: 45,
          duration: 0.5,
        })
        .from('#tabsID', {
          opacity: 0,
        });
    });

    return () => context.revert();
  }, [isMobile, isTablet]);

  return (
    <div ref={tabsContainerRef} className={styles.tabsContainer}>
      <div className={styles.tabsNav}>
        <div ref={tabsBG} className={styles.tabsBG}>
          {config.map((item, index) => (
            <div
              key={index}
              id='tabsID'
              className={`${styles.tab} ${
                activeTab === index && styles.tabActive
              }`}
              onClick={() => setActiveTab(index)}
            >
              {item.tab}
            </div>
          ))}
        </div>
      </div>
      <div>{config[activeTab]?.content}</div>
    </div>
  );
};

export default Tabs;

The Component below:

import { useState, useEffect, useRef } from 'react';
import AnimatedImage from 'components/AnimatedImage';
import gsap from 'gsap';
import { Link } from 'react-router-dom';

import useIsMobile from 'hooks/useIsMobile';

import styles from './styles.module.css';

const ProjectsSectionDesktop = ({ projectsData }) => {
  const [hover, setHover] = useState(false);
  const [animLength, setAnimLength] = useState(false);
  const [height, setHeight] = useState();

  const projectsContainerRef = useRef();
  const fadeInTl = useRef();
  const scrollTl = useRef();
  const trackRef = useRef();
  const titleRef = useRef();
  const pinContainerRef = useRef();

  const isMobile = useIsMobile(1200);

  useEffect(() => {
    setAnimLength(trackRef.current.offsetWidth);

    console.log(height);

    const context = gsap.context(() => {
      const projects = gsap.utils.toArray('#projectsContainer');

      gsap.set(projects, {
        xPercent: 50,
        opacity: 0,
      });

      fadeInTl.current = gsap.timeline({
        scrollTrigger: {
          trigger: pinContainerRef.current,
          start: 'top bottom',
          preventOverlaps: true,
          markers: true,
        },
      });

      fadeInTl.current
        .to(titleRef.current, {
          opacity: 0.1,
          duration: 2,
          ease: 'power1.out',
        })
        .to(
          projects,
          {
            xPercent: 0,
            opacity: 1,
            duration: 1,
            stagger: 0.25,
            ease: 'power2.out',
          },
          '<'
        );
    });

    return () => context.revert();
  }, [projectsData, isMobile]);

  useEffect(() => {
    setHeight(projectsContainerRef.current.getBoundingClientRect().top);

    const scrollContext = gsap.context(() => {
      scrollTl.current = gsap.timeline({
        scrollTrigger: {
          trigger: projectsContainerRef.current,
          start: 'top top',
          end: `+=${animLength}`,
          pin: true,
          scrub: 0.5,
          preventOverlaps: true,
        },
      });

      scrollTl.current
        .to(trackRef.current, {
          xPercent: -100,
          ease: 'none',
        })
        .to(
          titleRef.current,
          {
            xPercent: -50,
            ease: 'none',
          },
          '<'
        );
    });

    return () => scrollContext.revert();
  }, [projectsData, animLength, isMobile, height]);

  return (
    <div ref={pinContainerRef} style={{ border: '1px solid red' }}>
      <div ref={projectsContainerRef} className={styles.projectsContainer}>
        <h1 ref={titleRef} className={styles.title}>
          Projects
        </h1>
        <div ref={trackRef} className={styles.projectsTrack}>
          {projectsData?.map((project, index) => (
            <div
              id='projectsContainer'
              key={index}
              className={styles.projectContainer}
            >
              <Link to={`/projects/${project.id}`} className={styles.link}>
                <div
                  id='projectImageContainer'
                  className={styles.projectImageContainer}
                  onMouseEnter={() => setHover(true)}
                  onMouseLeave={() => setHover(false)}
                >
                  <AnimatedImage src={project.image} playAnim={hover} />
                </div>
              </Link>
            </div>
          ))}
        </div>
      </div>
    </div>
  );
};

export default ProjectsSectionDesktop;

2023-05-1010_50_30-ReactApp.thumb.png.80a3ce608ed285e4f01b4338495a3e8f.png2023-05-1010_50_47-ReactApp.thumb.png.2b8c283328adbba75b896500c6cb527f.png

GSAP Helper
Posted

It's pretty tough to troubleshoot without a minimal demo - the issue could be caused by CSS, markup, a third party library, your browser, an external script that's totally unrelated to GSAP, etc. Would you please provide a very simple CodePen or CodeSandbox that demonstrates the issue? 

 

Please don't include your whole project. Just some colored <div> elements and the GSAP code is best (avoid frameworks if possible). See if you can recreate the issue with as few dependancies as possible. If not, incrementally add code bit by bit until it breaks. Usually people solve their own issues during this process! If not, then at least we have a reduced test case which greatly increases your chances of getting a relevant answer.

 

Here's a starter CodePen that loads all the plugins. Just click "fork" at the bottom right and make your minimal demo

See the Pen aYYOdN by GreenSock (@GreenSock) on CodePen.

 

If you're using something like React/Next/Vue/Nuxt or some other framework, you may find StackBlitz easier to use. We have a series of collections with different templates for you to get started on these different frameworks: React/Next/Vue/Nuxt.

 

Once we see an isolated demo, we'll do our best to jump in and help with your GSAP-specific questions. 

Posted

Hi there,

 

ScrollTrigger.refresh refreshes all ScrollTriggers, so components shouldn't make a difference.

 

Quote

I've tried refreshing the ScrollTrigger on Complete

 

Unless you're just targeting one trigger and it's the wrong one?...

There's no refresh call in the code you provided so I can't really see what you're doing wrong and advise.
 

I also don't understand this sentence, sorry, maybe a typo?

Quote

start position doesn't update when a component before it changes height


ScrollTrigger has to be told to refresh after you make changes in the DOM, if you're setting height, I'd probably use a GSAP tween to do that and then use an onComplete to call ScrollTrigger.refresh()

Maybe this helps, sorry I can't advise any more than this. If you can get a demo together it's be much easier to see where you're going wrong.

Pollux Septimus
Posted

@GSAP Helper Hello, The templates were very nice and I manage to build this minimal demo. It behaves exactly like it does in my project.

 

@Cassie Hello Cassie, I have added ScrollTrigger.refresh() to the onComplete callback function of the last tween in the animation sequence, on the first component. There is a very high chance that I haven't done it right. I also apologize for the improper grammar.

I meant to say that if a component's height changes through any means, and there is a different component below it that uses ScrollTrigger, the start indicator that is visible with the markers: true option does not update to reflect the new position of the component. With the minimal demo that I've linked above it should be clear. :)

 

Thank you for your answer

Posted

Thanks for the demo! I can't find that call but that's not where it needs to be.

 

ScrollTrigger.refresh is just a way to say "hey ScrollTrigger, I've changed something, can you update the positions" So in your case, as you're updating the height state, you need to react to that change.

Something like this, set up an effect that gets called when the height var changes and then refresh. 

const [height, setHeight] = useState(100);

useEffect(() => {
  console.log('height', height);
  ScrollTrigger.refresh();
}, [height]);

 

https://stackblitz.com/edit/react-lqtgoc?file=src%2Fcomponents%2FProjectsSectionDesktop%2Findex.jsx,src%2Fcomponents%2FSize%2Findex.jsx,src%2FApp.js

Pollux Septimus
Posted

Thank you! I manage to fix the issue by refreshing the scrollTrigger whenever the tabs are changed. Unfortunately, this creates another issue where the tabs animation runs twice. The first time runs because it's supposed to and the second time because of the refresh. I hope it's clear in the video.

 

 

Pollux Septimus
Posted
  useEffect(() => {
    ScrollTrigger.refresh();
  }, [activeTab, isMobile]);

Forgot to also show the code.

Posted

That snippet and a video aren't really enough to offer help I'm afraid. Sounds like you've got in a bit of a React state tangle...
 

Can you make a minimal demo?

Also this may help

 

Pollux Septimus
Posted

Ill Try

Pollux Septimus
Posted

This is all I could do. For some reason, the styling does not work, but at least the animations work and also the issue is visible. Hope it helps.

Posted

Hi,

 

I've been looking around your las link for a bit and I can't find a simple solution for this. As @Cassie mentions this most likely has to do with the way you're handling state changes. You have multiple effect hooks in your components checking for different properties. Also you have this in a component:

const isMobile = 'sfddsf';

useEffect(() => {
  setAnimLength(trackRef.current.offsetWidth);

  console.log(height);

  const context = gsap.context(() => {
    
  });

  return () => context.revert();
}, [projectsData, isMobile]);

You're passing a constant as a dependency in the array of your hook. That doesn't make a lot of sense.

 

Also we always recommend using useLayoutEffect to ensure that the DOM has been changed and rendered when running the code. I created an ultra simplified example that shows that running ScrollTrigger.refresh() should have the effect you're looking for:

See the Pen gOBzLro by GreenSock (@GreenSock) on CodePen.

 

Unfortunately we don't have the time resources to comb through an entire codebase trying to find the problem. Even if you already reduced it to it's bare minimum, it's still a lot.

 

Sorry I can't be of more assistance. 

Happy Tweening!

  • Like 1
Pollux Septimus
Posted

@Rodrigo Hello, Thank you for your time. 

I am aware that the code is quite a lot, I was hoping this is something common with React and it has a simple solution. Regarding the useLayoutEffect, I had more issues using it than not. I will try to find a solution myself and If I will I'll let you know. 

Pollux Septimus
Posted

I believe I have found a viable solution.  I have implemented a short delay so that everything is invisible during the refresh process. Additionally, I have modified the animation based on this delay.

As for the dependency passed as a constant in the array, it is actually a custom hook that returns a boolean value depending on whether the user is on a mobile device or not. I didn't want to copy the entire hook, so I just set it to a random string to avoid any errors.

 

Thank you all very much for your help. 

  • Like 1

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...