Jump to content
Search Community

Best practices with GSAP in React TypeScript App ( in a hook ? )

Julien test
Moderator Tag

Go to solution Solved by Rodrigo,

Recommended Posts

Hello GSAP community. 

I've already used GSAP in multiple react TS projects. Every thing works fine. I'm not posting for any issue :) 

BUT , I'm still a bit in the "fog" when it comes to be sure of the best practices in this particular stack ( React + TS ).

 

In a very basic way, I'll declare any needed ref in any functional component : 

 
export const AnyFunctionalComponent: FunctionComponent<AnyFunctionalComponentProps> = () => {
 
const animationTargetRef = useRef<HTMLDivElement>(null);
const scrollTriggerTargetRef = useRef<HTMLDivElement>(null);
gsap.registerPlugin(ScrollTrigger);
 

 then I use a useLayoutEffect ( to be sure all ref have a .current as the DOM is supposed to be completely rendered in the useLayoutEffect ) with the ref as depedancies and I double check that the ref exist as current before creating anyAnimation as gsap , and finally I add the anyAnimation.kill() to ensure every thing is clear if  ref used for gsap are unmounted.

 

useLayoutEffect(() => {
 
const animationTargetCurrent = animationTargetRef.current;
const scrollerDiv = document.getElementById("scroller");
const scrollTriggerCurrent = scrollTriggerTargetRef.current;
 
if (animationTargetCurrent && scrollTriggerCurrent && scrollerDiv) {
const anyAnimation = gsap.fromTo(
animationTargetCurrent,
{
opacity: 0,
y: 50,
},
{
y: 0,
opacity: 1,
 
duration: 1,
ease: "none",
scrollTrigger: {
trigger: scrollTriggerCurrent,
start: "top bottom",
scroller: scrollerDiv,
end: 200,
scrub: 1,
},
}
);
return () => {
anyAnimation.kill();
};
}
 
}, [animationTargetRef, scrollTriggerTargetRef]);
 

First of all, is this the good way to clear everything ? 

 

Another step for me , to clear a bit my code is to put the animation code in a useCallback function with the same ref dependancies  : 

const anyAnimation = useCallback(() => {
const animationTargetCurrent = animationTargetRef.current;
const scrollerDiv = document.getElementById("scroller");
const scrollTriggerCurrent = scrollTriggerTargetRef.current;
 
if (animationTargetCurrent && scrollTriggerCurrent && scrollerDiv) {
const anyAnimation = gsap.fromTo(
animationTargetCurrent,
{
opacity: 0,
y: 50,
},
{
y: 0,
opacity: 1,
 
duration: 1,
ease: "none",
scrollTrigger: {
trigger: scrollTriggerCurrent,
start: "top bottom",
scroller: scrollerDiv,
end: 200,
scrub: 1,
},
}
);
return () => {
anyAnimation.kill();
};
}
}, [animationTargetRef, scrollTriggerTargetRef]);
 
useLayoutEffect(() => {
anyAnimation();
 
}, [animationTargetRef, scrollTriggerTargetRef]);
 

 

I'm not sure the the useCallback, neither the useCallback dependancies are really necessary , as  it's already protected (memoized ? ) as the call is made in the useLayoutEffect with the same dependancies ? Right ? 

 

And last, this is not clear enough for me. I'd like to move all animation function anyAnimation in a hook to which we could pass the ref as arguments, to re-use the same code if needed, and clear the component that use it. 

BUT > we can't call a hook in a useEffect ( or in a useLayoutEffect ) . 

Any suggestions  to write this ? Any source that could help ?  

In the same time, the first part of the question remains very important. What's the best way to clear any gsap animation and refs .

 

Thanks in advance for your help/and suggestions.

 

 

 

 

 

 

Link to comment
Share on other sites

  • Solution

Hi @Julien and welcome to the GreenSock forums!

 

The first thing I'd like to clear is that there is no need to register a GSAP Plugin or Effect inside the component's function. The reason? Well, everytime that component re-renders you'll be registering that plugin again and there is no need for that. Normally I like to register all the GSAP plugins the App uses at the top level file:

import React, { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import gsap from "gsap";
import { Flip } from "gsap/Flip";
import { ScrollTrigger } from "gsap/ScrollTrigger";

import App from './App';

import './style.css';

gsap.registerPlugin(Flip, ScrollTrigger);

const rootElement = document.getElementById('root');
const root = createRoot(rootElement);

root.render(
  <StrictMode>
    <App />
  </StrictMode>
);

That should be enough and you shouldn't have any issues with that.

 

Since version 3.11 we have GSAP Context that helps a lot with React and other UI frameworks with scoping, selectors and cleanup when something gets unmounted/removed:

https://greensock.com/docs/v3/GSAP/gsap.context()

 

We strongly recommend using Context in React and it's ecosystem.

 

You can use a useCallback for an event driven animation that you'd like to create everytime that event is triggered without any issues, just be aware of cleaning up that particular GSAP instance in a cleanup section (useCallback doesn't have a cleanup section that I'm aware of). But you can also leverage GSA Context for that as well:

const ctx = useRef();

// Event Handler
const clickEventHandler = () => {
  ctx.current.onClick();
};

useLayoutEffect(() => {
  ctx.current = gsap.context((self) => {
    // use any arbitrary string as a name; it'll be added to the Context object, so in this case you can call onClick() later...
    self.add("onClick", (e) => {
      gsap.to(...); // <-- gets added to the Context!
    });

  }, myRef);
  
  return () => ctx.current.revert();
}, []);

Also you can store the GSAP Context instance on state and reference it directly on your JSX:

const [ctx, setCtx] = useState();

useLayoutEffect(() => {
  const _ctx = gsap.context((self) => {
    // use any arbitrary string as a name; it'll be added to the Context object, so in this case you can call ctx.onClick() later...
    self.add("onClick", (e) => {
      gsap.to(...); // <-- gets added to the Context!
    });

  }, myRef);
  setCtx(_ctx);
  
  return () => ctx.revert();
}, []);

return (
  <div>
    <button onClik={ctx.onClick}>
  	  Click
    </button>
  </div>
);

 

2 hours ago, Julien said:

And last, this is not clear enough for me. I'd like to move all animation function anyAnimation in a hook to which we could pass the ref as arguments, to re-use the same code if needed, and clear the component that use it. 

BUT > we can't call a hook in a useEffect ( or in a useLayoutEffect ) . 

You can either create a Higher Order Component:

https://greensock.com/react-advanced#reusable-animations

 

Or you can create a custom Hook:

https://greensock.com/react-advanced#hooks

 

Also keep in mind that sometimes a custom Hook could be a bit overkill, depending on the project. In those cases you can create a JS file (gsap-helpers.js for example) and simply export your methods there:

export const createAnyAnimation = (target, scrollTriggerTrigger, scroller) => {
  const anyAnimation = gsap.fromTo(
    target, {
      opacity: 0,
      y: 50,
    }, {
      y: 0,
      opacity: 1,
      duration: 1,
      ease: "none",
      scrollTrigger: {
        trigger: scrollTriggerTrigger,
        start: "top bottom",
        scroller: scroller,
        end: 200,
        scrub: 1,
      },
    }
  );
  return anyAnimation;
};

Then use it like this:

import { createAnyAnimation } from "../src/gsap-helpers";

useLayoutEffect(() => {
  const ctx = gsap.context(() => {
    t = createAnyAnimation(target, scrollTriggerTartet, scroller);
  });
  
  return () => ctx.revert();
}, []);

 

2 hours ago, Julien said:

BUT > we can't call a hook in a useEffect ( or in a useLayoutEffect ) . 

Mhh... why not? This approach is totally fine, excessive IMHO, but totally fine:

// File /src/hooks/useAnyAnimation.js
import {
  useEffect
} from "react";

const useAnyAnimation = () => {
  const createAnyAnimation = () => {
    const anyAnimation = gsap.fromTo(
      target, {
        opacity: 0,
        y: 50,
      }, {
        y: 0,
        opacity: 1,
        duration: 1,
        ease: "none",
        scrollTrigger: {
          trigger: scrollTriggerTrigger,
          start: "top bottom",
          scroller: scroller,
          end: 200,
          scrub: 1,
        },
      }
    );
    return anyAnimation;
  };

  return {
    createAnyAnimation
  };
};

export default useAnyAnimation;
import useAnyAnimation from "src/hooks/useAnyAnimation";

const myComponent = () => {
  const { createAnyAnimation } = useAnyAnimation();
  
  useLayoutEffect(() => {
    const ctx = gsap.context(() => {
      const t = createAnyAnimation();
    });
    
    return () => ctx.revert();
  }, []);
};

Have a good read to these articles:

In order to get a better grasp of how to use GSAP in a React setup.

 

Happy Tweening!

  • Like 3
Link to comment
Share on other sites

Wow ! Thanks a lot @Rodrigo ! 

A full answer / guide / course in so little time. I knew for a while that Greensock team was available to help their users ( when it was Action Script  time, it was only Jack , but he was there ! ). 

I should have searched and found the article you suggested me to read ( hooks and re-usable higher components ), but didn't. Just crossed the way of the new context article ( and was not sure of the new way to clean ). 

Hope this post will be a new way to find all these articles and a clear explanation of how it's possible to use GSAP in this stack.

Still thanks @Rodrigo

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