Jump to content
Search Community

Flip and React state falling out of sync

henryb test
Moderator Tag

Go to solution Solved by Rodrigo,

Recommended Posts

Hello,
I am having trouble with keeping the state that controls various parts of a layout in sync with the use of Flip toggle on that page. A little context:

I am working on a image gallery page that uses GSAP's Flip plugin to toggle between two layout states: a grid four-column view and a full single-column view. The state (`isGridView`) is used to control the srcSet that is provided to the images — so that the appropriate size images are rendered depending on the layout. Additionally other parts of the layout depend on the state but must be sequenced in the correct order of the Flip animation, i.e. when we transition to the grid view, we need to fade in captions for the images after the animation completes, but when we transition to the full view we need to  fade out those captions before the animation.

My problem: when the page transitions a couple of times, the state stops being matched to the correct Flipped layout. This results in captions showing in the full view (incorrect) or images appearing blurry (incorrect srcSet).

I have tried to set up my Flip to depend on the state of the layout. However my main difficulty is not knowing when / how best to update the React state before / during / after the Flip transition. I am currently using onComplete, but that only updates after the captions have completed their staggered fade-in, and it is very plausible that the user clicks to transition again before the stagger animation has completed, and as a result the state never gets updated for that cycle. I have tried to test with onStart and onUpdate, but onStart means that the srcSet changes too early, resulting in flashes during the transition, and onUpdate seems unreliable for keeping the state in sync in my testing too.

I have a feeling I am not setting this up in the best React way. I would be so grateful if anyone has time to take a glance at my StackBlitz reproduction linked in this post to see where I am going wrong. I currently have a function `performLayoutFlip` that is called to do the Flip transition; it could be nice to have a timeline that gets reversed, but I need to account for controlling the captions (fade in after a delay vs fade out immediately) so I don't know how I would manage a timeline that isn't exactly symmetrical. (PS If you click on the images to transition a few times you should see the captions and images come out of sync). Let me know if I can help clarify anything, many thanks in advance! :) 
Stackblitz minimal reproduction
 

Link to comment
Share on other sites

  • henryb changed the title to Flip and React state falling out of sync
  • Solution

Hi,

 

I've been fiddling a bit with your demo and besides the captions issue I can't reproduce other problems in your demo.

 

I think the issue stems from the delay you have in your instance for showing the captions:

gsap.to(imageElementsArray('.caption'), {
  delay: 2, // <- HERE
  duration: 0.5,
  opacity: 1,
  stagger: 0.15,
  ease: 'circ.inOut',
  onComplete: () => {
    // The srcSet on each image is controlled by tracking isGridView state
    setIsGridView(true)
  }
})

I think a safer approach is to use the onComplete callback from the Flip instance. As soon as I remove that delay, the captions are no longer visible.

 

Is worth mentioning that using the onComplete callback from the Flip instance also can cause this behaviour since when you toggle before the Flip instance is completed the onComplete callback will still be triggered. You could kill that Flip instance to prevent that callback from being called or use a simple boolean to prevent the captions tween from being created at all:

const performLayoutFlip = contextSafe((container, scrollToId, captions) => {
  const selector = gsap.utils.selector(container);
  const state = Flip.getState(
    selector('.grid-container img, .flex-container img')
  );
  container.classList.toggle('grid-container');
  container.classList.toggle('flex-container');

  Flip.from(state, {
    duration: 2,
    ease: 'power4.out',
    scale: false,
    fade: true,
    onComplete: () => {
      if (captions && showCaptions.current) {
        gsap.to('.caption', {
          duration: 0.5,
          opacity: 1,
          stagger: 0.15,
          ease: 'circ.inOut',
          onComplete: () => {
            setIsGridView(true);
          },
        });
      }
      if (document && scrollToId) {
        document
          .getElementById(scrollToId)
          .scrollIntoView({ behavior: 'smooth', block: 'center' });
      }
    },
  });
});

const clickHandler = contextSafe((event) => {
  const container = imagesRef?.current;
  const imageElementsArray = gsap.utils.selector(container);
  const switchingToGridView = container.classList.contains('flex-container');

  if (event.target.tagName === 'IMG') {
    if (switchingToGridView) {
      performLayoutFlip(container, null, true);
      showCaptions.current = true;
    } else {
      showCaptions.current = false;
      gsap.killTweensOf('.caption');
      // Fade out all captions before Flip
      gsap.to('.caption', {
        duration: 0.5,
        opacity: 0,
        ease: 'circ.inOut',
        onComplete: () => {
          performLayoutFlip(container, event.target.id);
          setIsGridView(false);
        },
      });
    }
  }
});

Also all of this is not needed at all:

const container = imagesRef?.current;
const imageElementsArray = gsap.utils.selector(container);

// Then
gsap.to(imageElementsArray('.caption'), {/**/});

You already set your scope in the useGSAP hook to imagesRef so running this anywhere inside the useGSAP scope or any contextSafe call:

gsap.utils.toArray(".caption");

Will return an array of the elements with the caption class that are in the scope of imagesRef.current, so that is quite redundant and wasteful. In fact since that is called inside the contextSafe scope you can just do this:

gsap.to('.caption'), {/**/});

Since that particular GSAP instance will use the scope defined in the useGSAP hook to do the same.

 

Here is a fork of your demo:

https://stackblitz.com/edit/vercel-next-js-1kpj9n?file=pages%2Findex.jsx

 

Hopefully this helps.

Happy Tweening!

  • Like 1
Link to comment
Share on other sites

Hi Rodridgo,
Thank you so much for such a generous response.
These are some very useful learnings for me. In particular killing the Flip instance to prevent the callback from being called:

gsap.killTweensOf('.caption');

That also makes a lot of sense to use a ref to control whether the captions tween is created at all.
Also well noted  your points regarding the scope within the useGSAP hook. 

The forked demo works as intended now. 
Thank you again!

Link to comment
Share on other sites

Yeah, be careful about this:

gsap.killTweensOf('.caption');

If you run that before the Flip instance is completed it will have no effect because that particular tween hasn't been created yet. That tween is created in the onComplete callback of the Flip instance, so GSAP won't find any tween related to that selector, that's why either killing the Flip instance or using a boolean in the onComplete is the best alternative. You can use a ref to store the Flip instance in order to simplify that.

 

Hopefully this helps.

Happy Tweening!

  • Like 1
Link to comment
Share on other sites

Quote

If you run that before the Flip instance is completed it will have no effect because that particular tween hasn't been created yet. That tween is created in the onComplete callback of the Flip instance, so GSAP won't find any tween related to that selector, that's why either killing the Flip instance or using a boolean in the onComplete is the best alternative. You can use a ref to store the Flip instance in order to simplify that.

I see. Well noted!

Regarding onComplete, a related question from the demo has come up — when the Flip completes, I would like to scroll to the clicked target image. I have swapped the native browser scrollIntoView method for gsap's scrollToPlugin in the onComplete, and this works. If I move the scrollTo function into the onStart — to position the viewport over the target image during the Flip animation, this doesn't work most of the time. I think this would probably be expected behaviour though right? Because during the Flip the image target doesn't have a place in the DOM, so even if the scrollTo function gets given the target image id, the scrollTo probably gets interrupted by the animation of the layout (my guess).

My question is just: would there be another way to streamline / harmonise the Flip transition and a scrollTo animation? Or is the best way simply as I have it — 1) complete the Flip animation and then 2) scroll to the target?

As seen here in the deployed staging site (work in progress).

For some reason I can't get the scrollTo working in the Stackblitz demo.

Link to comment
Share on other sites

Hi,

 

Indeed that is not a simple thing to achieve but you should definitely create and execute any type of scroll (either direct or animated) after the Flip instance is completed. It doesn't make any sense to run that when the Flip animation has started or is still running IMHO as you have it now.

 

On a quick glance in your demo I can't see anything with an actual ID attribute in your Figure component:

<div
  ref={element}
  className="relative w-full h-full max-w-4xl"
  data-image-id={key}
>
  <Image
    src={url}
    alt={alt}
    sizes={imageSizes}
    style={{
      objectFit: 'contain',
      objectPosition: 'center',
      width: '100%',
      height: 'auto',
    }}
    onClick={clickHandler}
    onLoad={() => setIsLoaded(true)}
    className={cn(
      isLandscape ? '' : 'max-h-[90vh] object-cover',
      'h-auto w-full cursor-pointer',
    )}
  />
  <h3 style={{ opacity: 0 }} className="caption pt-2 text-xs sm:text-sm">Image caption</h3>
</div>

So I wouldn't expect this to actually work:

gsap.to(window, { duration: 2, scrollTo: { y: document.getElementById(scrollToId), offsetY: 100 } });

So you're passing an id (scrollToId) but where can that ID be found?

 

You could also explore the getBoundingClientRect method:

https://developer.mozilla.org/en-US/docs/Web/API/Element/getBoundingClientRect

 

This is beyond what we can do in these free forums since we need to focus our time in GSAP related issues and this is not a GSAP issue but mostly a logic and React setup one.

 

Hopefully this helps.

Happy Tweening!

  • Like 1
Link to comment
Share on other sites

Missed that id, between different versions of the demo, apologies and thanks for catching that.


Many thanks for your answer regarding scrolling during the Flip animation, Rodrigo. I appreciate you have been very generous with me.

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