Jump to content
GreenSock

omarel

Simple opacity fade doesn't work in React?

Recommended Posts

I've been running into a lot of issues using .from and .to with opacity tweens with React so I did a simple example to demonstrate. .from seems to just stop before the full tween when used in react and .to doesn't animate.

 

Opacity Tween stops in React (You can switch .from to .to and still no dice)

https://codesandbox.io/s/proud-dawn-uic35x

Screen Shot 2022-04-05 at 6.40.44 AM.png

Same tween works in Vanilla - CodePen

 

Solution:

This thread now addresses a larger issue between first render animations and React 18's useEffect empty dependency array's new "double" render in development mode - so I wanted to point to the specific solution by @Cassie  to the original question as it does solve the issue.

 

 

See the Pen WNdMbob by omarel (@omarel) on CodePen

Edited by omarel
Pointing out Solution as thread grows to the larger issue
Link to comment
Share on other sites

Hey there @omarel,

 

Oh man... Looks like React has changed up a lot in 18. So our recommended techniques may have to change.

 

Quote

React 18 has more code paths that will “try” a render and abandon the result depending on what else is going on.so imagine you have three different “render” calls, and at some point in the future it’s going to decide which one’s results to keep.one of them happens in a world where the ref isn’t populated yet, which trips GSAP up, because there isn’t a DOM node to target. this interrupts whatever render was successful and had already started animating stuff

 

This isn't a GSAP issue. It's down to how React is rendering things. I'm sure there's some logic behind it but right now it seems like it's just replacing DOM nodes for no apparent reason.

 

Here's a suggestion, maybe @OSUblake has one too.

 

*tired sigh*

https://codesandbox.io/s/thirsty-buck-zlck9y?file=/src/App.js

EDIT:
It's not replacing the DOM, it’s just that effects run twice in strict mode

  • Like 1
Link to comment
Share on other sites

I was going bananas with this yesterday reviewing my syntax over and over. That the simplest gsap tween wasn’t working. The same issue is faced with autoAlpha and I don’t know what else at this point. 
 

Right the next major version of React: 18 has been released a few days ago. Im not sure how it breaks gsap if so but that would be quite disappointing. 

Link to comment
Share on other sites

Oh wait - some good news?

This may actually be about strict mode
 

Quote

When running in “strict mode“ React will intentionally double-render components for in order to flush out unsafe side effects. With the release of React 18, StrictMode gets an additional behavior to ensure it's compatible with reusable state. When StrictMode is enabled, React intentionally double-invokes effects (mount -> unmount -> mount) for newly mounted components. Like other strict mode behaviors, React will only do this for development builds.


https://reactjs.org/docs/strict-mode.html#detecting-unsafe-effects
 

Looks like it's finding behaviors which are currently not a problem, but might be a problem when the component ships?

Link to comment
Share on other sites

Yeah, it's not a GSAP thing, it's to do with React 18 and Strict mode.

 

Advice from a React educator is...
 

Quote

'Skip Strict Mode, unless you're using concurrent features like useDeferredValue or useTransition'

 

Link to comment
Share on other sites

Ok -

With strict mode, if you do a cleanup, use useLayoutEffect and add immediateRender:false on your from tweens it works.

https://codesandbox.io/s/eloquent-cloud-56ghy5?file=/src/App.js
 

useLayoutEffect(() => {
    let from = gsap.from(elem0.current, {
      rotation: 360,
      immediateRender: false
    });

    let to = gsap.to(elem0.current, {
      opacity: 0,
      delay: 1
    });

    return () => {
      from.kill();
      to.kill();
    };
  });

 

  • Like 1
Link to comment
Share on other sites

2 hours ago, Cassie said:

Ok -

With strict mode, if you do a cleanup, use useLayoutEffect and add immediateRender:false on your from tweens it works.

https://codesandbox.io/s/eloquent-cloud-56ghy5?file=/src/App.js
 

useLayoutEffect(() => {
    let from = gsap.from(elem0.current, {
      rotation: 360,
      immediateRender: false
    });

    let to = gsap.to(elem0.current, {
      opacity: 0,
      delay: 1
    });

    return () => {
      from.kill();
      to.kill();
    };
  });

 

 

 

Ok wow! Thank you.

 

This seems like it really needs a clear write up in the Greensock docs. 

 

The gsap animation does break when reactStrictMode is on in plain React and when used with NextJS (tested)

 

Turning reactStrictMode off and keeping gsap syntax as is works or leaving reactStrictMode on with your solution which I do appreciate!! but does feel like it will become cumbersome to deal with as tweens grow but I suppose it's the only way.

 

Hoping for some more weigh-ins on potential solutions, but thank you!

 

  • Like 2
Link to comment
Share on other sites

Quote

This seems like it really needs a clear write up in the Greensock docs. 


We'll update our React guides. But they literally just released React 18 - give us a second to catch up! 😂

Link to comment
Share on other sites

Hi folks! This is Dan (from React). We would generally not recommend disabling Strict Mode. It's more of a temporary workaround while the libraries are getting updated, not a permanent strategy.

 

In general, React leans towards assuming that effects are safe to re-run extra times. Effects act like a synchronization mechanism: they let you "mirror" some state into some imperative change in the DOM. For example, an effect like this:

 

useEffect(() => {
  someRef.current.style.color = myState
})

would be safe to re-run at any point because it just "re-synchronizes" some information.

 

The issue can be reproduced in vanilla JS if you call gsap.from twice: 

See the Pen ZEvryNb by gaearon-the-encoder (@gaearon-the-encoder) on CodePen

. I don't know enough about GSAP so I can't tell whether this behavior is intentional or not. I wonder if there is some other way to call GSAP API in a way that the last call "wins" — i.e., express it as synchronization? That would resolve the issue.

 

Alternatively, you can track whether it has already ran manually:

 

const didAnimate = useRef(false);
useEffect(() => {
  if (didAnimate.current) {
    return;
  }
  didAnimate.current = true;
  gsap.from(/* ... */)
}, [])

This would also solve the issue.

 

(However, the "symmetrical" setup/cleanup approach in this post makes a lot of sense to me! And if the animations are truly interruptible — which is ideal — then there should not be any visible effect from double-calling at all.)

 

If there are some particularly common patterns for using GSAP with React, it would make sense to expose them as custom Hooks with React-leaning APIs rather than using GSAP directly. This also lets you hide any quirks behind them.

 

Please let me know if more details would be helpful! Or feel free to file an issue in the React tracker for more discussion.

  • Like 5
Link to comment
Share on other sites

Thanks so much for popping by Dan!

Link to comment
Share on other sites

Hi @gaearon

 

Thanks for taking the time to help explain what's going on. 

 

57 minutes ago, gaearon said:

The issue can be reproduced in vanilla JS if you call gsap.from twice: 

 

That behavior is intentional, and only affects from animations, which isn't the most common type, but it definitely gets used a lot. With from animations, the from value is immediately set on the target, and then it animates to what the current value was. So the first call creates an animation from an opacity of 0 to an opacity 1, and the second call is creating an animation from an opacity of 0 to an opacity of 0.

 

1 hour ago, gaearon said:

Alternatively, you can track whether it has already ran manually:

 

const didAnimate = useRef(false);
useEffect(() => {
  if (didAnimate.current) {
    return;
  }
  didAnimate.current = true;
  gsap.from(/* ... */)
}, [])

This would also solve the issue.

 

Do you see any issues with putting that inside a hook like this?

 

function useAnimationEffect(fn, deps = []) {
  const didAnimate = useRef(false);
  useEffect(() => {
    if (didAnimate.current) {
      return;
    }
    !deps.length && (didAnimate.current = true);
    return fn();
  }, deps);
}

 

We could definitely instruct our React users to do something like that.

 

Thanks

  • Like 2
Link to comment
Share on other sites

@OSUblake I don’t think the issue is happening with only .from. I believe more tweens have the issue if I play with it more. I’ve seen it with .to I can create some demos. 
 

I did think @Cassie solution was pretty darn elegant while keeping reactstrictmode on and thank you @gaearon for all the wonderful specifics of exactly what's happening!

 

It does seem like a more organic fix from Greensock is warranted in due time with some more analysis so that we don’t have to do all these conditionals. and some analysis on why some tweens get affected and others don’t with React 18. 
 

We just released a project with React 17 and gsap and didn’t see any of these issues. 
 

I’m so excited about the latest gsap 3.10 release and appreciate the hard work! I just so happened to see this while playing witb the latest 3.10 gsap release on the latest React because we always create new apps with the latest versions of react. 

  • Like 2
Link to comment
Share on other sites

Thanks for the extra info. I can see now why from behaves like this.

I think it might be helpful to take React out of the equation and think about this similarly to a from call in a button click handler:

 

button.addEventListener('click', () => {
  gsap.from(/* ... */)
})

Suppose the user clicks the button twice in quick succession — without any React. Then they will have the same problem. How would you recommend them to resolve it? Whatever the recommendation is, would likely work for React effects too. (Whether it's tracking animation state with a boolean, recommending a different API than from, or something else.)

 

28 minutes ago, omarel said:

I believe more tweens have the issue if I play with it more. I’ve seen it with .to I can create some demos. 

 

From our side, we're happy to brainstorm solutions for other cases too — but it probably wouldn't be much different from this “double click” thought experiment.

 

2 hours ago, OSUblake said:

Do you see any issues with putting that inside a hook like this?

We don't recommend people to create "overly generic" Hooks. If it has "effect" in the name or takes dependencies as arguments, it's probably not a good candidate for a custom Hook. It's hard to suggest something more specific though because I'm not familiar with common GSAP patterns.

 

The default behavior (without a ref) is usually the correct one. The reason we introduced it is we'd like to add a feature similar to KeepAlive in Vue that remounts a component with existing state. Like if you switch Feed -> Profile -> Feed, and we mount Feed with the previous state so that input aren't lost. To verify your components are resilient to this, we simulate unmounting and remounting with restored state right away. So this surfaces this issue. But this issue is what would happen if you were to quickly switch tabs yourself (when using our KeepAlive-like component). If you "solve" it with a ref, you're not gonna have an animation the second time at all. Or if you do it too quick it will similarly get stuck.

 

This is why I think the proper solution is to have the effect code be resilient to double-calling. Since double-calling is exactly what would happen if we actually remount the component with previous state and DOM before the animation has finished.

  • Like 4
Link to comment
Share on other sites

Just wanted to say "thanks" @gaearon for chiming in with your insight. I'm sure we'll have some ideas/questions to run past you. A lot of people seem to want to use GSAP in React, so smoothing out the rough spots would be very good. 

 

Thanks again!

Link to comment
Share on other sites

Thank you @GreenSock. If this does become part of the map to smooth out double calls I would love to look out for a future release or documentation. We would love to use GSAP with React 18 :) 

 

@gaearon I suppose this is more of a react question but related to the gsap implementation. Is the double invoking issue in the demo related to reactstrictmode being on or the keep alive feature. 

 

If reactstrictmode, the docs say reactstrictmode only runs in development. So can I understand if we technically ship an app to production this double invoking in the initial demo would not occur within the React app? Wont be realistic way to debug animations in development, but am curious. https://codesandbox.io/s/proud-dawn-uic35x

 

Also, can we assume in React 18 that any useeffect in our demos even when called (with an empty dependency array) can now run more than once and is this tied to using ref? 

  • Like 1
Link to comment
Share on other sites

1 hour ago, omarel said:

Thank you @GreenSock. If this does become part of the map to smooth out double calls I would love to look out for a future release or documentation. We would love to use GSAP with React 18 :) 


You can very much already use GSAP with React 18, again, this isn't a GSAP bug, this is just React calling the effect twice.

 

You just have to be aware that strict mode is calling the effects twice and adjust your code accordingly. There's inevitably going to be some wrangling involved when working with a framework and a third party library (especially React because it has very specific ways of doing things.)

If you were calling a from tween on a button that may be pressed twice in quick succession (to stick with Dan's analogy) you would handle it like this in vanilla JS - with control methods. Same applies here now that React has added strict mode.


See the Pen XWVZVPz?editors=1010 by GreenSock (@GreenSock) on CodePen




 

  • Like 2
Link to comment
Share on other sites

Here's an article/video that explains immediate render and the behaviour of from tweens - it might help you understand why they're affected, and why it's not a feature, not a 'bug'
 

 

  • Like 3
Link to comment
Share on other sites

In terms of 'to' tweens not working.

We can't replicate that at all - or see any reason at all why they wouldn't work, they don't prerecord any starting values.

This demo of yours has a 'to' tween that's not working, but that's not due to GSAP or React, you're trying to animate from an initial state of opacity:1 to opacity:1

So there's nothing to animate!

 

https://codesandbox.io/s/keen-bose-se3bi2?file=/src/App.js

  • Like 1
Link to comment
Share on other sites

@Cassie

On 4/6/2022 at 8:40 AM, Cassie said:

You can very much already use GSAP with React 18, again, this isn't a GSAP bug, this is just React calling the effect twice.

 

 

Of course, but with React 18 running into these issues threw us off. Maybe just bad timing. 3.10 and React 18 nearly on the same day! :) I realize there are all these conditionals to go around mentioned above but simply not worth it for a basic .from fade in :) I can't speak for all devs but we want it to be simple as gsap is meant to be used. I did a fade in test with react 18 using Framer Motion and no issues. (updated the demo with framer motion)  You are correct .to didn't give us an issue I updated the demo. I think the reliability of knowing either .from or .to works the same is important though.

 

Certainly gsap is amazing and a lot to offer, I think Greensock should consider evaluating mitigating some of these issues thoigh. We will still use GSAP and see if we run into too many more issues with React 18. Ive already seen a few with ScrollSmoother I'll use other threads for but still testing. 

 

Dan did mention some potential workarounds for the GSAP API such as below.

 

On 4/5/2022 at 2:58 PM, gaearon said:

I wonder if there is some other way to call GSAP API in a way that the last call "wins" — i.e., express it as synchronization?

 

Thanks again. I really do love GSAP and what it has to offer. We'll figure it all out and work around what we can.

 

 

  • Like 1
Link to comment
Share on other sites

4 hours ago, omarel said:

we want it to be simple as gsap is meant to be used

We totally agree, and we've put a lot of effort into making GSAP framework agnostic and have ZERO dependencies. It's a high-speed property manipulator for literally ANY property of ANY object that JavaScript can touch. We really wanted to avoid having a framework-specific API/toolset because:

  1. You'd get comfortable with the API in one framework and then that skillset wouldn't translate to another. Frustrating! One of things developers love about GSAP is that it works anywhere and everywhere with the same API. I've heard from plenty of people who learned some React-specific animation tool and then they had to work on a project in vanilla or another framework and were forced to switch to an entirely different library that likely didn't have the same feature set. 
  2. Frameworks are moving targets - as this thread illustrates, you may build something that works great...until the library authors totally change how things work at a low level and then you've gotta troubleshoot and adjust. No fun. It's a support/maintenance nightmare especially if you've gotta build a wrapper for React, Angular, Vue, etc. Building on open web standards the all browsers implement feels much more stable. 

In order to "fix" this issue caused by the way React handles things, we could certainly create a React-specific wrapper around GSAP with its own API and aim to support it for years, but you'd still have the tradeoff of a different API which adds to the kb and potential confusion. 

 

There are libraries out there for React with a more declarative API which is great for super simple things like your fade in example, but GSAP animators tend to be the ones who create stunning award-winning interactive sites and need crazy amounts of power and flexibility. A simplistic API falls apart very quickly when you start trying to do even moderately complex animations. Try crafting a declarative API for creating timelines with all the features GSAP has. Good luck :) Our current API is the result of years in the trenches (not that there isn't room for improvement). 

 

Rest assured that we're exploring options for smoothing over the rough spots in React. Ideally, it would allow standard GSAP code that people see in tutorials all over the web. We're not sure that's feasible given React's way of doing things with double-calls, flushed variables, etc., etc. but we'll keep exploring. If you've got any suggestions, we're all ears. 

 

4 hours ago, omarel said:

Dan did mention some potential workarounds for the GSAP API such as below.

On 4/5/2022 at 1:58 PM, gaearon said:

I wonder if there is some other way to call GSAP API in a way that the last call "wins" — i.e., express it as synchronization?

 

Yes, while that's technically feasible, I'm always concerned about performance and file size tradeoffs. This suggestion requires searching through every animation when creating a new one, checking to see if there's a matching signature of some kind and then if found, it must revert all the properties (which also means storing their pre-animated values) before instantiating the new one. That's a performance penalty that EVERYONE would be forced to pay (plus the kb cost) just to work around this odd React behavior. Animation is probably the most performance-sensitive part of UX, so I'm extremely uncomfortable implementing things like this, at least in the core.  It's not terribly uncommon for hundreds or even thousands of simultaneous animations to be running. GSAP is well-known as delivering top-notch performance and we're determined to keep it that way. 

  • Like 2
  • Thanks 1
Link to comment
Share on other sites

8 hours ago, omarel said:

I did a fade in test with react 18 using Framer Motion and no issues. (updated the demo with framer motion)

 

What Framer Motion is doing would be the same as a fromTo animation, which of will course will work as expected because we are explicitly telling the animation what values to use. And this React issue is not unique to GSAP, and will happen with any animation library that does something similar to a GSAP from animation, for example, an anime.js "reverse" animation.

 

Notice how the fromTo animation works just like Framer Motion, but the anime.js reverse animation is running into the same issue as the from animation.

 

https://codesandbox.io/s/confident-leaf-ih2zmw?file=/src/App.js

 

  • Like 3
  • Thanks 1
Link to comment
Share on other sites

  • 1 month later...

I've recently been working heavily with React and GSAP. Libraries like react-spring and framer-motion just don't provide the kind of control you need for effects with multiple stages. For example, a staggered animation in GSAP is a simple, one-line setting, and in more declarative libraries, it can be a whole mess of things.

 

However, my solution to double-rendering has been to disable strict mode. If even react devs say that doing so isn't a great idea, it's worth considering the alternatives.

  1. useRef to check if an effect has run and prevent it from running again
  2. make sure to kill all your tweens on unmount and set immediateRender to false, as shown by  @Cassie

 

In my project with strict disabled, I have been good about writing clean-up functions for everything because I was under the impression that it is a best practice. Upon reading @gaearon 's post, I decided it was worth re-enabling strict and handling everything with refs. I found that if you clean up your tweens, you cannot use the ref method since the effect will run, clean up, and then not run a second time. It seems, and correct me on this if I am wrong, that making use of the ref method and not cleaning up the effect is a bad pattern. When the component does eventually unmount, I want everything to be cleaned up. Therefore, if I wanted to run strict mode, I would have to use immediateRender: false. I have already experienced issues with doing so in staggered tweens where the first element doesn't animate correctly. I have to imagine the more I use this method, the more problems I'll find along the way.

 

So, a few questions:

  1. Is it really fine to not clean up scrollTriggers, scrollSmoothers, and tweens and use the ref method to deal with double-rendering?
  2. Is my concern with setting immediateRender in places based on anything of substance?

 

Finally, on @GreenSock's point, while it would be nice for a fix to come from GSAP's side, it's a weird ask. The benefit of GSAP is its speed and efficiency. I've tried the popular react animation libraries, and while they can be excellent, they lack the precision GSAP offers and usually have worse performance. Even vanilla JS custom solutions end up being slower. @gaearon suggests that tweens should be resilient to double-running, but should they? Does every component need the ability to remount with the existing animation state? I'm curious what the opinions are on that from the GSAP team because the react team has made their position clear.

  • Like 3
Link to comment
Share on other sites

Very fair questions, @SteveS

 

My [totally biased] opinion: this double-call behavior is problematic and I'm seeing a fair amount of other people getting bitten by it as well (outside this community). If developers need to write their code such it recovers seamlessly when the function is called multiple times quickly, that needs to be explicitly clear because chunks of logic may require special treatment, like recording initial values and re-applying them. I don't think most developers have any clue that's what's expected of them in useEffect().

 

This reminds me of the occasional "bug" people run into when they put a gsap.from() inside a "mouseenter" handler and then the user rolls over/off/over quickly and it seems to "break". It's merely a logic thing in the way they coded it, of course - not a bug in GSAP at all. Should GSAP "fix" that automatically by finding any existing/competing tweens and forcing them to revert to their original values before instantiating the new tween? I certainly don't think so because while that may seem beneficial in this one scenario, it may be quite undesirable in another. Developers should understand the logic of their code. gsap.from() clearly says that it uses the CURRENT values as the destination ones...so that's exactly what it should do (rather than assuming "oh, you must mean that all other tweens should be found and reverted before I read the values")

 

I have no doubt that the React team is doing their best to move things in the direction they deem most beneficial to the toolset and community as a whole. I can see why it'd make sense to encourage folks to make their components as bulletproof as possible, including a double-call (like if the component gets unmounted and then mounted I guess). I'm not a React user so I'm not trying to weigh in on what they should do. I'm simply saying that as a potential user, the double-call strikes me as pretty confusing and problematic. 

 

1 hour ago, SteveS said:
  • Is it really fine to not clean up scrollTriggers, scrollSmoothers, and tweens and use the ref method to deal with double-rendering?

"the ref method" - by that do you mean the didAnimate.current boolean/condition that skips all but the first render?

 

It sounds a little odd to me to not clean up stuff when your component unmounts. But I'm really not a React guy, so I'm not in a great position to offer advice. 

 

1 hour ago, SteveS said:
  1. Is my concern with setting immediateRender in places based on anything of substance?

If you've got a .from() tween that you set immediateRender: false on it, there are two down sides: 

  1. It'll stay at the original value(s) for 1 tick, then jump. Might notice it for a very brief moment. That's why immediateRender is true by default on .from() and .fromTo() tweens.
  2. It only solves this "React 18 double-render" issue if both renders occur on the same tick. That seems very likely, but I just want to be clear - if the tween renders for the first time and then the useEffect() gets called again, the .from() is gonna of course use that new value as the destination which is what you're trying to avoid. 

We've got an idea for a feature in a future release that may assist in making this situation easier to work around. I can't provide too many details publicly at the moment, but we really try to cater to the needs of our user base as best as we can even if we think the "problem" is being caused by a different technology. It's not always feasible or even wise, but we try. 

 

Like you said, it seems the React team has made their position clear so we'll just look for ways to turn lemons into lemonade. :) For now, it's mostly a matter of educating people about what's going on. Once they understand the logic, they're usually fine. But the whole double-call thing throws them for a loop. 

 

And thanks for noticing all the effort that's gone into making the platform highly performant and robust. I'm sure simplistic animations are easy with other React-specific libraries but GSAP aims to be a Swiss Army Knife for high-end animators who need insane amounts of flexibility and power. They'll feel stifled by some of the other libraries. Like a chef trying to make a gourmet meal with ingredients that are limited to chicken nuggets and ketchup. 🤔

  • Like 1
  • Thanks 1
  • Haha 1
Link to comment
Share on other sites

Well, I look forward to seeing what the GSAP team is working on. It's just a shame the react team has decided to shoot animations in react in the foot like this. React is nice because it provides a simple way to interact with data. Employing it to build websites that have complex animations is a bit of a misuse, and this is something of a reminder that react is a UI framework first and foremost.

The react team does good work, but sometimes it seems like they have spent too much time in their own ecosystem. In the interim, I'll continue to work with strict mode off, as it seems like the most straightforward way to work with the project. The other solutions feel hacky. Thanks for the information.

  • Like 3
Link to comment
Share on other sites

  • 3 weeks later...
On 6/4/2022 at 7:02 AM, GreenSock said:

We've got an idea for a feature in a future release that may assist in making this situation easier to work around.

Is there an ETA? Cause right now both sides just seem to be playing "This isn't really my issue" which is kind of true but for us developers, it would be of great help to just land on a proper solution or workaround. Where is the I'm a React guy but also I really know about GSAP guy?

I'm just leaning towards turning the strict mode off for now.

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