How to run CSS animation again in React?

Viewed 826

There are quite a few similar questions, but I haven't found a clear answer.

Sometimes, I need to run CSS animation in React again, after it's complete, under certain condition. There's usually quite complex logic behind this case, so I simplified it to show what I mean without extra complexity.

Let's say I'm having a checkbox to decide whether I want the animation to run or not:

Spinner

When checkbox is disabled, the animation stops, but it finishes its cycle if it's already going. I was wondering what is the proper way to re-run the animation (i.e. remove the class and add it again) with React. Here's my component:

import { useState, useEffect } from 'react';

export default function Spinner() {
  const [isSpinning, setIsSpinning] = useState(true);
  const [isChecked, setIsChecked] = useState(true);

  useEffect(
    () => {
      if (isChecked) {
        setIsSpinning(true);
      }
    },
    [isChecked]
  );

  const spin = () => {
    setIsSpinning(false);

    if (isChecked) {
      // without setTimeout() it doesn't work
      window.setTimeout(() => setIsSpinning(true));
    }
  };

  return (
    <div className="container">
      <input
        type="checkbox"
        checked={isChecked}
        onChange={toggleCheckbox}
      />
      <div
        className={isSpinning && 'spinning'}
        onAnimationEnd={endSpin}
      >
        Hello!
      </div>
    </div>
  );
};

And the CSS:

@keyframes spinning {
    from {
        transform: rotateX(0);
    }

    to {
        transform: rotateX(360deg);
    }
}

.container {
    display: flex;
    gap: .5em;
}

.spinning {
    animation: spinning 1s;
}

I'm using window.setTimeout(fn) to add class name again after it's removed. Otherwise, it happens in the same render cycle and it behaves like the class was always there, so animation never re-runs. It works, but isn't there another, less hacky way to add class in next render cycle in React?

1 Answers
import { useState } from 'react'
import styled from "styled-components";

const Test = () => {

    const [isChecked, setIsChecked] = useState(false);

    return (
        <StyledWrapper>
            <div className="container">
                <input
                    type="checkbox"
                    checked={isChecked}
                    onChange={() => setIsChecked(!isChecked)}
                />
                <div
                    className={isChecked ? "spinning" : ""}
                >
                    Hello!
                </div>
            </div>
        </StyledWrapper>
    )
}

export default Test;

const StyledWrapper = styled.div`

.container {
    display: flex;
    gap: .5em;
}

.spinning {
    animation: spin 1s ease;
    -webkit-animation-name: spin;
    -webkit-animation-duration: 4000ms;
    -webkit-animation-iteration-count: infinite;
    -webkit-animation-timing-function: linear;
    -moz-animation-name: spin;
    -moz-animation-duration: 4000ms;
    -moz-animation-iteration-count: infinite;
    -moz-animation-timing-function: linear;
    -ms-animation-name: spin;
    -ms-animation-duration: 4000ms;
    -ms-animation-iteration-count: infinite;
    -ms-animation-timing-function: linear;
    
    animation-name: spin;
    animation-duration: 4000ms;
    animation-iteration-count: infinite;
    animation-timing-function: linear;
}

@-moz-keyframes spin {
    from { -moz-transform: rotate(0deg); }
    to { -moz-transform: rotate(360deg); }
}
@-webkit-keyframes spin {
    from { -webkit-transform: rotate(0deg); }
    to { -webkit-transform: rotate(360deg); }
}
@keyframes spin {
    from {transform:rotate(0deg);}
    to {transform:rotate(360deg);}
}
`;

You can use something like this. There is a property called animation-iteration-count in css, which can be given as infinite. And furthur manage a state where based on the checkbox click you toggle the boolean value. Now you can pass the className based on these boolean values.

Related