Advanced GSAP Animation in React

 

Are you working with React and looking to really advance your GSAP animation skills? You’re in the right place. This guide contains advanced techniques and some handy tips from expert animators in our community.

This is not a tutorial, s…

 

Are you working with React and looking to really advance your GSAP animation skills? You’re in the right place. This guide contains advanced techniques and some handy tips from expert animators in our community.

This is not a tutorial, so feel free to dip in and out as you learn, think of it as a collection of recommended techniques and best practices to use in your projects.

Why GSAP?

Animating with GSAP gives you unprecedented levels of control and flexibility. You can reach for GSAP to animate everything — from simple DOM transitions to SVG, three.js, canvas or WebGL — your imagination is the limit. More importantly, you can rely on us. We obsess about performance, optimizations and browser compatibility so that you can focus on the fun stuff. We’ve actively maintained and refined our tools for over a decade and there are no plans to stop. Lastly, if you ever get stuck, our friendly forum community is there to help.

Going forward we will assume a comfortable understanding of both GSAP and React.

If you’re starting out with either we highly recommend reading our foundational article first – GSAP Animations in React.

 

Component CommunicationComponent Communication

In the last article, we covered creating our first animation, and how to create and control timelines within a React component. But there are times where you may need to share a timeline across multiple components or construct animations from elements that exist in different components.

In order to achieve this, we need a way to communicate between our components.

There are 2 basic approaches to this.

  1. a parent component can send down props, e.g. a timeline
  2. a parent component can pass down a callback for the child to call, which could add animations to a timeline.

Passing down a timeline propPassing down a timeline prop

Note that we are using useState instead of useRef with the timeline. This is to ensure the timeline will be available when the child renders for the first time.

function Box({ children, timeline, index }) {
  const el = useRef();
  // add 'left 100px' animation to timeline
  useEffect(() => {    
    timeline.to(el.current, { x: -100 }, index * 0.1);
  }, [timeline]);
  
  return <div className="box" ref={el}>{children}</div>;
}

function Circle({ children, timeline, index, rotation }) {
  const el = useRef();
  // add 'right 100px, rotate 360deg' animation to timeline
  useEffect(() => {   
    timeline.to(el.current, {  rotate: rotation, x: 100 }, index * 0.1);
  }, [timeline, rotation]);
  
  return <div className="circle" ref={el}>{children}</div>;
}

function App() {    
  const [tl, setTl] = useState(() => gsap.timeline());
   
  return (
    <div className="app">   
      <Box timeline={tl} index={0}>Box</Box>
      <Circle timeline={tl} rotation={360} index={1}>Circle</Circle>
    </div>
  );
}

 

Passing down a callback to build a timelinePassing down a callback to build a timeline

function Box({ children, addAnimation, index }) {
  const el = useRef();
  // return a 'left 100px' tween
  useEffect(() => {   
    const animation = gsap.to(el.current, { x: -100 });
    addAnimation(animation, index);
    
    return () => animation.progress(0).kill();
  }, [addAnimation, index]);
  
  return <div className="box" ref={el}>{children}</div>;
}

function Circle({ children, addAnimation, index, rotation }) {
  const el = useRef();
  // return a 'right 100px, rotate 360deg' tween
  useEffect(() => {  
    const animation = gsap.to(el.current, { rotate: rotation, x: 100 });
    addAnimation(animation, index);
    
    return () => animation.progress(0).kill();
  }, [addAnimation, index, rotation]);
  
  return <div className="circle" ref={el}>{children}</div>;
}


function App() {
  // define a timeline
  const [tl, setTl] = useState(() => gsap.timeline());
  // pass a callback to child elements, this will add animations to the timeline
  const addAnimation = useCallback((animation, index) => {    
    tl.add(animation, index * 0.1);
  }, [tl]);
  
     
  return (
    <div className="app">   
      <Box addAnimation={addAnimation} index={0}>Box</Box>
      <Circle addAnimation={addAnimation} index={1} rotation="360">Circle</Circle>
    </div>
  );
}

 

React ContextReact Context

Passing down props or callbacks might not be ideal for every situation.

The component you’re trying to communicate with may be deeply nested inside other components, or in a completely different tree. For situations like this, you can use React’s Context.

Whatever value your Context Provider provides will be available to any child component that uses the useContext hook.

const SelectedContext = createContext();
  
function Box({ children, id }) {
  const el = useRef();
  const selected = useContext(SelectedContext);

  useEffect(() => {
    gsap.to(el.current, {
      // animate x by 200 if the box ID matches the selected context value
      x: selected === id ? 200 : 0
    });
  }, [selected, id]);

  return <div className="box" ref={el}>{children}</div>;
}
  
function App() {  
    
  // Any component can read the value passed to the provider, no matter how deeply nested.
  // In this example, we're passing "2" as the current value.
  return (
    <SelectedContext.Provider value="2">
      <Box id="1">Box 1</Box>
      <Box id="2">Box 2</Box>
      <Box id="3">Box 3</Box>
    </SelectedContext.Provider>
  );
}

 

Imperative CommunicationImperative Communication

Passing around props or using Context works well in most situations, but using those mechanisms cause re-renders, which could hurt performance if you’re constantly changing a value, like something based on the mouse position.

To bypass React’s rendering phase, we can use the useImperativeHandle hook, and create an API for our component.

const Circle = forwardRef((props, ref) => {
  const el = useRef();
    
  useImperativeHandle(ref, () => {           
    
    // return our API
    return {
      moveTo(x, y) {
        gsap.to(el.current, { x, y });
      }
    };
  }, []);
  
  return <div className="circle" ref={el}></div>;
});

Whatever value the imperative hook returns will be forwarded as a ref

function App() {    
  const circleRef = useRef();
       
  useEffect(() => {    
    // doesn't trigger a render!
    circleRef.current.moveTo(300, 100);
  }, []);
    
  return (
    <div className="app">   
      <Circle ref={circleRef} />
    </div>
  );
}

 

Creating reusable animationsCreating reusable animations

Creating reusable animations is a great way to keep your code clean while reducing your app’s file size. The simplest way to do this would be to call a function to create an animation.

function fadeIn(target, vars) {
  return gsap.from(target, { opacity: 0, ...vars });
}

function App() {    
  const box = useRef();
    
  useLayoutEffect(() => {
    const animation = fadeIn(box.current, { x: 100 });
  }, []);
  
  return <div className="box" ref={box}>Hello</div>;
}

For a more declarative approach, you can create a component to handle the animation.

function FadeIn({ children, vars }) {
  const el = useRef();
  
  useLayoutEffect(() => {    
    gsap.from(el.current.children, { 
      opacity: 0,
      ...vars
    });        
  }, []);
  
  return <span ref={el}>{children}</span>;
}
  
function App() {      
  return (
    <FadeIn vars={{ x: 100 }}>
      <div className="box">Box</div>
    </FadeIn>
  );
}

If you want to use a React Fragment or animate a function component, you should pass in a ref for the target(s).

 

Using gsap.effectsUsing gsap effects

GSAP provides a way to create reusable animations with registerEffect()

function GsapEffect({ children, targetRef, effect, vars }) {  
  
  useLayoutEffect(() => {        
    if (gsap.effects[effect]) {
      gsap.effects[effect](targetRef.current, vars);
    }
  }, [effect]);
    
  return <>{children}</>;
}

function App() {      
  const box = useRef();
  
  return (
    <GsapEffect targetRef={box} effect="spin">
      <Box ref={box}>Hello</Box>
    </GsapEffect>
  );
}

 

Exit animationsExit animations

To animate elements that are exiting the DOM, we need to delay when React removes the element. We can do this by changing the component’s state after the animation has completed.

function App() {      
  const boxRef = useRef();
  const [active, setActive] = useState(true);
  
  const remove = () => {
    gsap.to(boxRef.current, {
      opacity: 0,
      onComplete: () => setActive(false)
    });
  };
  
  return (
    <div>
      <button onClick={remove}>Remove</button>
      { active ? <div ref={boxRef}>Box</div> : null }
    </div>
  );
}

The same approach can be used when rendering elements from an array.

function App() {    
  
  const [items, setItems] = useState([
    { id: 0 },
    { id: 1 },
    { id: 2 }
  ]);
  
  const removeItem = (value) => {
    setItems(prev => prev.filter(item => item !== value));
  }
  
  const remove = (item, target) => {
    gsap.to(target, {
      opacity: 0,
      onComplete: () => removeItem(item)
    });
  };
  
  return (
    <div>
      {items.map((item) => (
        <div key={item.id} onClick={(e) => remove(item, e.currentTarget)}>
          Click Me
        </div>
      ))}
    </div>
  );
}

However – you may have noticed the layout shift – this is typical of exit animations. The Flip plugin can be used to smooth this out.

In this demo, we’re tapping into Flip’s onEnter and onLeave to define our animations. To trigger onLeave, we have to set display: none on the elements we want to animate out.

 

Custom HooksCustom Hooks

If you find yourself reusing the same logic over and over again, there’s a good chance you can extract that logic into a custom hook. Building your own Hooks lets you extract component logic into reusable functions.

Let’s take another look at registerEffect() with a custom hook

function useGsapEffect(target, effect, vars) {
  const [animation, setAnimation] = useState();
  
  useLayoutEffect(() => {
    setAnimation(gsap.effects[effect](target.current, vars));    
  }, [effect]);
  
  return animation;
}

function App() {      
  const box = useRef();
  const animation = useGsapEffect(box, "spin");
  
  return <Box ref={box}>Hello</Box>;
}

 

Here are some custom hooks we’ve written that we think you may find useful:

useSelectoruseSelector

Memoises GSAP’s selector utility.

see demo on codepen

 function useSelector() {
  const ref = useRef();
  const q = useMemo(() => gsap.utils.selector(ref), [ref]);
  return [q, ref];
}

Usage:

function App() {
  const [q, ref] = useSelector();

  useEffect(() => {
    gsap.to(q(".box"), { x: 200 });
  }, []);

  return (
    <div ref={ref}>
      <div className="box">Hello</div>
    </div>
  );
}

 

useArrayRefuseArrayRef

Adds refs to an array.

see demo on codepen

function useArrayRef() {
  const refs = useRef([]);
  refs.current = [];
  return [refs, (ref) => ref && refs.current.push(ref)];
}

Usage:

function App() {
  const [refs, setRef] = useArrayRef();

  useEffect(() => {
    gsap.to(refs.current, { x: 200 });
  }, []);

  return (
    <div>
      <div className="box" ref={setRef}>Box 1</div>
      <div className="box" ref={setRef}>Box 2</div>
      <div className="box" ref={setRef}>Box 3</div>
    </div>
  );
}

 

useStateRefuseStateRef

This hook helps solve the problem of accessing stale values in your callbacks. It works exactly like useState, but returns a third value, a ref with the current state.

see demo on codepen

function useStateRef(defaultValue) {
  const [state, setState] = useState(defaultValue);
  const ref = useRef(state);

  const dispatch = useCallback((value) => {
    ref.current = typeof value === "function" ? value(ref.current) : value;
    setState(ref.current);
  }, []);

  return [state, dispatch, ref];
}

Usage:

const [count, setCount, countRef] = useStateRef(5);
const [gsapCount, setGsapCount] = useState(0);  

useEffect(() => {
  gsap.to(box.current, {
    x: 200,
    repeat: -1,
    onRepeat: () => setGsapCount(countRef.current)
  });
}, []);

 

useIsomorphicLayoutEffectuseStateRef

You might see a warning if you use server-side rendering (SSR) with useLayoutEffect. You can get around this by conditionally using useEffect during server rendering. This hook will return useLayoutEffect when the code is running in the browser, and useEffect on the server.

see demo on codepen

const useIsomorphicLayoutEffect = typeof window !== "undefined" 
  ? useLayoutEffect 
  : useEffect;

Usage:

function App() {
  const box = useRef();

  useIsomorphicLayoutEffect(() => {
    gsap.from(box.current, { opacity: 0 });
  }, []);

  return (
    <div>
      <div className="box" ref={box}>Hello</div>
    </div>
  );
}


 

If there is anything you’d like to see included in this article, or if you have any feedback, please leave a comment below so that we can smooth out the learning curve for future animators.

Good luck with your React projects and happy tweening!

 


Print Share Comment Cite Upload Translate
APA
Blog | Sciencx (2024-03-29T15:24:55+00:00) » Advanced GSAP Animation in React. Retrieved from https://www.scien.cx/2021/08/17/advanced-gsap-animation-in-react-2/.
MLA
" » Advanced GSAP Animation in React." Blog | Sciencx - Tuesday August 17, 2021, https://www.scien.cx/2021/08/17/advanced-gsap-animation-in-react-2/
HARVARD
Blog | Sciencx Tuesday August 17, 2021 » Advanced GSAP Animation in React., viewed 2024-03-29T15:24:55+00:00,<https://www.scien.cx/2021/08/17/advanced-gsap-animation-in-react-2/>
VANCOUVER
Blog | Sciencx - » Advanced GSAP Animation in React. [Internet]. [Accessed 2024-03-29T15:24:55+00:00]. Available from: https://www.scien.cx/2021/08/17/advanced-gsap-animation-in-react-2/
CHICAGO
" » Advanced GSAP Animation in React." Blog | Sciencx - Accessed 2024-03-29T15:24:55+00:00. https://www.scien.cx/2021/08/17/advanced-gsap-animation-in-react-2/
IEEE
" » Advanced GSAP Animation in React." Blog | Sciencx [Online]. Available: https://www.scien.cx/2021/08/17/advanced-gsap-animation-in-react-2/. [Accessed: 2024-03-29T15:24:55+00:00]
rf:citation
» Advanced GSAP Animation in React | Blog | Sciencx | https://www.scien.cx/2021/08/17/advanced-gsap-animation-in-react-2/ | 2024-03-29T15:24:55+00:00
https://github.com/addpipe/simple-recorderjs-demo