DEV Community

Kaleb M
Kaleb M

Posted on

Animation Classes & useEffect Hooks - Is There a Better Way?

Hi there everyone!

Quick Context

I've recently been working on a feature that includes a bit of animation, alongside some design system classes. I wanted to introduce hooks into the code base for the first time, and decided to give it a try!

Challenge

There were two challenges I had when using animations for fading out:

  1. An element would re-appear after the fadeOut animation without applying a second class to hide it
  2. When applying a hidden class at the same time as an animation class, the animation didn't happen - just the disappearing :).

Solution

To solve this, I utilized a useEffect() hook which would set the animation class, followed by a setTimeout with a 1-second delay, to first complete the animation and then successfully hide the element we are animating.

I utilized the return function of hooks to clean up any timers on the element to prevent memory leaks.

Below you can see the code I've written (shortened version) to solve the challenge, or you can check out this Code Pen.

if you want to see the issue, comment out the useEffect hook and you will see that it fades out, then comes right back into view!

The isHidden prop is passed down from a higher component, which changes based on a tap/click.

Code

React

export const SomeNavHeader = ({
  title = 'Some Title',
  isHidden
  planId
}) => {
  const TOPBAR_VISIBILITY_CLASSES = {
    hidden: 'hide',
    visible: ''
  }
  const ANIMATION_CLASSES = {
    fadeIn: 'fade-in',
    fadeOut: 'fade-out'
  }

  // set default state to use fade in and visible class
  const [animationClass, setAnimationClass] = useState(ANIMATION_CLASSES.fadeDownAndIn)
  const [topbarNavHiddenClass, setTopbarNavHiddenClass] = useState(TOPBAR_VISIBILITY_CLASSES.visible)

  // this will run everytime isHidden changes
  useEffect(() => {
    // set timer ids to null to help with clean up - null is OK here
    let hiddenClassTimer = null


    if (isHidden) {
      // fade out then hide once animation finishes
      setAnimationClass(ANIMATION_CLASSES.fadeOut)
      hiddenClassTimer = setTimeout(() => {
        setTopbarNavHiddenClass(TOPBAR_VISIBILITY_CLASSES.hidden)
      }, DELAYS_IN_MS.oneSecond)
    } else {
      // show topbar and animate it in
      setAnimationClass(ANIMATION_CLASSES.fadeIn)
      setTopbarNavHiddenClass(TOPBAR_VISIBILITY_CLASSES.visible)
    }

    // return function helps to clean up timeouts if they are happening when component is removed from dom
    return () => {
      clearTimeout(hiddenClassTimer)
    }
  }, [
    isHidden
  ])

  return (
    <header
      className={`some-header-component ${DESIGN_SYS.topBar} ${
        DESIGN_SYS.color.neutral90
      } ${animationClass} ${topbarNavHiddenClass}`}
    >
      <p>{title}</p>
    </header>
  )
}

Scss

.some-header-component {
  &.fade-in {
    animation-duration: 1s;
    animation-name: fadeIn;
  }

  &.fade-out {
    animation-duration: 1s;
    animation-name: fadeOut;
  }

  &.hide {
    display: none;
  }
}

Is There a Better Way?

I'd absolutely love feedback or other suggestions on the best way to handle the given challenge!

Please let me know if you have any questions!

Top comments (7)

Collapse
 
kewah profile image
Antoine Lehurt

Well done for your first custom hook!

For animating components based on state using CSS, I recommend you to take a look at React Transition Group. It handles the logic for toggling the CSS classes based on the component's state (mount/unmount). If I understand your goal correctly, you could achieve something similar using enter & enter-active classes to show and then hide the component after a delay.

As @jdmg94 mentioned, you can also take a look at React Spring. It's an excellent library for animating your components, and I'm a big fan of the hooks API it provides. But, it depends on your needs and if you plan to use it in other places. It introduces a new API, so if you only want to animate this component, getting started with React Transition Group might be a first good step.

Collapse
 
avatarkaleb profile image
Kaleb M

Does react transition group work on state changes compared to just mount/unmounting?

It looks like React Spring is a good option - do you know if it's necessary for very easy animations like fading in / fading out and what has your experience been with it?

Thank you so much for commenting and reading through for some suggestions :)

Collapse
 
kewah profile image
Antoine Lehurt

Does react transition group work on state changes compared to just mount/unmounting?

Yes, you can use the in prop. For instance

<CSSTransition in={isVisible} classNames="some-header">
  <div>Foo</div>
</CSSTransition>

There is a CodeSandbox in the documentation that can help you to visualize how it works reactcommunity.org/react-transitio...

It looks like React Spring is a good option - do you know if it's necessary for very easy animations like fading in / fading out and what has your experience been with it?

I don't think it's necessary to bring React Spring for simple animations, especially if it's an isolated use case.
In my experience, managing animations beyond fade in/fade out with different states can be annoying to build with CSS classes compared to an approach like React Spring. (Outside React ecosystem, I would recommend using something like GSAP.)

Collapse
 
josemunoz profile image
José Muñoz

The principle you will be working with is called a spring, it does not have a defined curve or a set duration. In that it differs greatly from the animation you are probably used to. We think of animation in terms of time and curves, but that in itself causes most of the struggle we face when trying to make elements on the screen move naturally, because nothing in the real world moves like that.

react-spring.io

Collapse
 
avatarkaleb profile image
Kaleb M

Hi there,

Thanks for commenting!

Would React Spring be necessary for basic animations or worth including into the bundle? I've heard of it and looks great!

I created a code pen to demo the solution:

codepen.io/avatar-kaleb/pen/voBmzp -- if you want to see the issue, comment out the useEffect hook and you will see that it fades out, then comes right back into view!

Would React Spring help remove this issue without using the hook / setTimeout

Collapse
 
stereobooster profile image
stereobooster

Can you post demo of this in codesandbox for example? It would be helpful to see playable example

Collapse
 
avatarkaleb profile image
Kaleb M • Edited

Here you go!

codepen.io/avatar-kaleb/pen/voBmzp

if you want to see the issue, comment out the useEffect hook and you will see that it fades out, then comes right back into view!

Let me know if you have any questions