値が変わったら再度CSSアニメーションを……という風に組みたかったけれど、CSSがもう一度読み込まれることはなかった。
なので無理やり再描写するようsetTimeoutを使用し、アニメーションのCSSがかかるimgに新規指定したdata-animateにuseStateで用意したanimateの値(boolean)が入るよう設定する。
useStateとイベントハンドラには以下のように指定。
  const [ animate, setAnimate ] = useState(false)
  const handleAnimate = () => {
    setAnimate(false)
    window.setTimeout(() => {
      setAnimate(true);
    }, 50)
  }
イベントが発火したら一旦falseに設定し、タイマー処理が終わったあとtrueに設定する。
アニメーションCSSには、data-animate=”true”で判定するよう設定すると、CSSを再描写してくれるようになる。
.image {
    animation-duration: .3s;
    animation-timing-function: ease-in-out;
    animation-fill-mode: forwards;
}
[data-animate="true"].image {
    animation-name: fade;
}
@keyframes fade {
    0% {
        opacity: 1;
    }
    100% {
        opacity: 0;
    }
}
余談
無理をせずFramer Motionを導入するのもあり。