Pollux Septimus Posted May 10, 2023 Posted May 10, 2023 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;
GSAP Helper Posted May 10, 2023 Posted May 10, 2023 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.
Cassie Posted May 10, 2023 Posted May 10, 2023 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 May 10, 2023 Author Posted May 10, 2023 @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
Cassie Posted May 10, 2023 Posted May 10, 2023 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 May 10, 2023 Author Posted May 10, 2023 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. Untitled (2).mp4
Pollux Septimus Posted May 10, 2023 Author Posted May 10, 2023 useEffect(() => { ScrollTrigger.refresh(); }, [activeTab, isMobile]); Forgot to also show the code.
Cassie Posted May 10, 2023 Posted May 10, 2023 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 May 10, 2023 Author Posted May 10, 2023 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.
Rodrigo Posted May 10, 2023 Posted May 10, 2023 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! 1
Pollux Septimus Posted May 10, 2023 Author Posted May 10, 2023 @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 May 11, 2023 Author Posted May 11, 2023 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. 1
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