Search code examples
cssreactjscss-animations

re-triggering css animations with react


I have a list of items in a table. I would like an item's row to briefly flash with a highlight due to some external events.

I have a CSS animation fadingHighlight to accomplish the desired visual effect, and when an event comes in I set a class last-updated on the desired row to trigger the animation.

However, when an update comes in for the same row multiple times in a row, only the first update causes a flash. (I believe this is because react persists the last-updated class on the row rather than re-rendering it, and thus the css doesn't re-start the animation.)

How can I re-trigger the animation if the same item gets updates multiple times in a row?

Demo and code: https://codesandbox.io/s/pensive-lamarr-d71zh?file=/src/App.js

Relevant portions of the react:

const Item = ({ id, isLastUpdated }) => (
  <div className={isLastUpdated ? "last-updated" : ""}>Item {id}</div>
);

const App = () => {
  const [lastUpdatedId, setLastUpdatedId] = React.useState(undefined);

  const itemIds = ["1", "2", "3"];
  return (
    <div className="App">
      <h2>The list of items</h2>
      {itemIds.map((id) => (
        <Item key={id} id={id} isLastUpdated={lastUpdatedId === id} />
      ))}

      <h2>Trigger updates</h2>
      {itemIds.map((id) => (
        <button onClick={() => setLastUpdatedId(id)}>Update item {id}</button>
      ))}
    </div>
  );
}

Styles:


@keyframes fadingHighlight {
  0% {
    background-color: #ff0;
  }
  100% {
    background-color: #fff;
  }
}

.last-updated {
  animation: fadingHighlight 2s;
}

Solution

  • (Thanks again to @leo for this answer which helped me get to this one!)

    I got the desired behavior (forcing the animation to restart) by

    1. Immediately removing the highlight class, and
    2. Setting a timeout (10msec seems to be fine) for adding back the highlight class.

    The timeout seems to force react to separately render the component without the highlight class and then again with the highlight class, causing the animation to restart. (Without the timeout, react may collapse these changes into one render step, causing the DOM to treat the whole thing as a no-op.)

    A nice result of this approach where each Item manages its own highlight state is that multiple items can be highlighted at the same time (e.g. if updates for one item come in before the highlight fades on another).

    Demo here

    const Item = ({ id, updateTime }) => {
      const [showHighlight, setShowHighlight] = React.useState(false);
    
      // By putting `updateTime` in the dependency array of `useEffect,
      // we re-trigger the highlight every time `updateTime` changes.
      useEffect(() => {
        if (updateTime) {
          setShowHighlight(false);
          setTimeout(() => {
            setShowHighlight(true);
          }, 10);
        }
      }, [updateTime]);
    
      return <div className={showHighlight ? "updated" : ""}>Item {id}</div>;
    };
    
    const App = () => {
      // tracking the update times at the top level
      const [updateTimes, setUpdateTimes] = React.useState({});
    
      // ...
            <Item key={id} id={id} updateTime={updateTimes[id]} />
    
      // ...
            <button
              onClick={() => {
                setUpdateTimes({ ...updateTimes, [id]: Date.now() });
              }}
            >
       // ...
    }