Jump to content
Search Community

React/NextJS component initializing twice despite useGSAP hook.

ryanmac test
Moderator Tag

Go to solution Solved by ryanmac,

Recommended Posts

I can't figure out for the life of me why my component is initializing twice (and in the process registering event handlers twice that torch my animations).   I thought the useGSAP hook mitigated this issue but perhaps I misunderstand or am utilizing it incorrectly. 

Thanks in advance.

"use client"

import React, { useRef } from "react";
import { gsap } from "gsap";
import { ScrollTrigger } from "gsap/ScrollTrigger";
import { useGSAP } from "@gsap/react";
import "./styles/skewgallery.css";

export const SkewGallery = ({ items }) => {

  gsap.registerPlugin(useGSAP, ScrollTrigger);

  const mainRef = useRef();
  const overlayRef = useRef();
  const timelineRef = useRef(null);

  useGSAP(
    (context, contextSafe) => {
        
      const itemsArray = gsap.utils.toArray(".skew-item");

      console.log("initializing");

      if (!timelineRef.current) {
        timelineRef.current = gsap.timeline({ paused: true });
      }

      itemsArray.forEach((item) => {
        gsap.fromTo(
          item,
          {
            opacity: 0,
            yPercent: 20,
            ease: "power4.out",
            stagger: 1,
          },
          {
            opacity: 1,
            yPercent: 0,
            scrollTrigger: {
              trigger: item,
              start: "top bottom",
              end: "50% 85%",
              scrub: 1,
            },
          }
        );

        const handleClick = contextSafe(() => {
          const timeline = timelineRef.current;

          console.log("doing it");

          timeline.clear();

          timeline.to(
            item,
            {
              rotation: 5,
              scale: 1.5,
              duration: 0.75,
              zIndex: 2,
              ease: "Back.easeOut",
            },
            "<"
          );

          timeline.to(
            overlayRef.current,
            {
              zIndex: 1,
              opacity: 0.85,
            },
            "<"
          );

          itemsArray.forEach((otherItem) => {
            if (otherItem !== item) {
              timeline.to(
                otherItem,
                {
                  scale: 0.95,
                  duration: 0.5,
                  ease: "power2.out",
                },
                "<"
              );
            }
          });

          timeline.play();
        });

        item.addEventListener("click", handleClick);

        return () => {
          item.removeEventListener("click", handleClick);
        };
      });

      console.log("overlay init");
      overlayRef.current.addEventListener("click", () => {
        console.log("overlay clicked");
        timelineRef.current.reverse();
      });
    },
    { scope: mainRef, dependencies: [] }
  );

  return (
    <>
      <div ref={mainRef} className="skew">
        <div ref={overlayRef} className="skew-overlay"></div>
        <div className="skew-container grid grid-cols-4 gap-5 p-5 items-center h-full w-full">
          {items.map((image, index) => (
            <div
              key={index}
              className="skew-item bg-slate-500 min-h-[300px] h-full border-2 border-gray-900 rounded-lg overflow-hidden bg-cover bg-no-repeat"
            >
              <h2>{image.title}</h2>
            </div>
          ))}
        </div>
      </div>
    </>
  );
};

export default SkewGallery;

 

Link to comment
Share on other sites

  • ryanmac changed the title to React/NextJS component initializing twice despite useGSAP hook.

Hi @ryanmac and welcome to the GSAP Forums!

 

Basically you have an issue of where you're removing the event listener, you're creating a return statement inside the forEach loop, that actually is not going to do anything in there:

useGSAP(
  (context, contextSafe) => {
    const itemsArray = gsap.utils.toArray(".skew-item");
    //...
    itemsArray.forEach((item) => {
      gsap.fromTo(
        // ...
      );
      const handleClick = contextSafe(() => {
        //...
      });
      item.addEventListener("click", handleClick);
      return () => {
        item.removeEventListener("click", handleClick);
      };
    });
    // ...
  },
  { scope: mainRef, dependencies: [] }
);

The return statement should be at the top of the useGSAP hook's callback:

useGSAP(
  (context, contextSafe) => {
    const itemsArray = gsap.utils.toArray(".skew-item");
    let handleClick;
    //...
    itemsArray.forEach((item) => {
      gsap.fromTo(
        // ...
      );
      handleClick = contextSafe(() => {
        //...
      });
      item.addEventListener("click", handleClick);
    });
    // ...
    return () => {
      itemsArray.forEach((item) => item.removeEventListener("click", handleClick));
    };
  },
  { scope: mainRef }
);

Also there is no need to pass an empty array as dependencies in the config object of the useGSAP hook, if no array is passed, the hook uses an empty array by default.

 

Finally if you keep having issues, please create a minimal demo using this starter template of our Stackblitz collection:

https://stackblitz.com/edit/gsap-react-basic-f48716

 

Here is our Stackblitz collection just in case you want to check other demos:

https://stackblitz.com/@gsap-dev/collections/gsap-react-starters

 

Hopefully this helps.

Happy Tweening!

Link to comment
Share on other sites

  • Solution

Thank you!  I went ahead and applied those changes but my events still seem to be firing twice.   I moved the component to stackblitz so you could take a look.

https://stackblitzstarterskgd3hx-gyvp--3000--9e2d28a3.local-credentialless.webcontainer.io

UPDATE: 

Figured it out.  The solution, for my anyway, was to:

 

1. attach the event listener directly on the element
2. create and leverage a ref array to grab the correct instance via index.
 

  const itemRefs = useRef([]);
  const timelineRef = useRef(gsap.timeline({ paused: true }));
  const activeItem = useRef(null);

  itemRefs.current = [];

  const addToItemRefs = (el) => {
    if (el && !itemRefs.current.includes(el)) {
      itemRefs.current.push(el);
    }
  };

  const toggleClick = (clickedItem) => {
    if (activeItem.current === clickedItem) {
        timelineRef.current.reverse();
      return;
    }
    
    // timeline code... 

 

<div className="skew-container grid grid-cols-4 gap-5 p-5 items-center h-full w-full">
          {items.map((image, index) => (
            <div
              ref={addToItemRefs}
              key={index}
              onClick={() => toggleClick(itemRefs.current[index])}
              className="skew-item bg-slate-500 min-h-[300px] h-full border-2 border-gray-900 rounded-lg overflow-hidden bg-cover bg-no-repeat"
            >
              <h2>{image.title}</h2>
            </div>
          ))}
        </div>

 

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