Reactのアニメーションに関する困ったこと
Reactで困ったこと
Reactでアニメーションを実装しようとすると、ときどき困ることがある。
特にモーダルやポップアップなどの表示・非表示をアニメーションで表現する場合、Stateで表示・非表示を管理すると、アニメーションがうまく動かないことがある。
次のようなコードを考えてみる。
type Props = {
show: boolean;
children: React.ReactNode;
};
const Component = ({show, children}: Props) => {
return show ? (
<div
style={{
// PopUpのアニメーションを行うコード
}}
>
{children}
</div>
) : null;
}
このコードは残念ながらうまく動かない。
show = true
の時は、アニメーションが実行されるが、show = false
の時は、アニメーションが実行される前に、null
がコンポーネントから返されることになるので、アニメーションが実行されない。
解決策
1. そもそもコンポーネントを消さなければ良い。
まず、コンポーネントを消すのではなく、opacityなどを用いて透明化し非表示にすることで、アニメーションを実現することができる。ただし、この場合はそのコンポーネント自体は消えないことになるので、裏側にコンポーネントが残り続けてしまう。そのため、パフォーマンス的にあまり良くない。
2. アニメーションの変更をStateで管理するのではなく、Refで管理する。
Refで管理すれば、アニメーションが終わる前にref.current
を変更しても再レンダリングは走らないので、アニメーションは実行される。しかし今度は開始時のアニメーションが動かないことになる。そのため、ForceRender
をする必要が出てくるか、Refと別のStateの2つの値を管理することになる。これだとコードが煩雑になってしまう。
3. ライブラリやその他のものを使う。
ということで、この記事ではその他の方法を使ってみて、どのライブラリが使いやすいかなどを調べてみる。
Animation関連のライブラリ
-
- Normal CSS
-
- React Transition Group
-
- React Spring
-
- Framer Motion
の4つについて、簡単な表示/非表示を切り替えるコードを書いてみて、どれが使いやすいかを調べてみる。
ちなみに、実験環境として、Next.js + TypeScript + emotionを使用している。
1. Normal CSS
emotionCSSのみを使って、表示/非表示を切り替えるコードを書いてみたところ、思ったよりも良い実装ができた。
内部で別のStateとして、表示/非表示を管理し、表示時はすぐさま、非表示時はonAnimationEnd
で非表示にするようにしている。
<div
onAnimationEnd={(e) => onAnimationEnd(e)}
css={[animationcss(show), elementcss]}
id={animationId}
>
{children}
</div>
ここで注意しなければいけないのが、バブリングの存在である。例えば、popUpの中でonAnimationEnd
を用いたアニメーションを行った時に、バブリングが起こりここのonAnimationEnd
も実行されてしまう。そのため、ランダムなanimationId
を生成し付与することでアニメーションのターゲットが自分自身でないものは実行しないようにする必要があることに注意が必要。
const onAnimationEnd = useCallback(
(e: React.AnimationEvent<HTMLDivElement>) => {
if (!(e.target instanceof HTMLElement)) return;
if (e.target.id !== animationId) return;
//省略
}, [animationId, ...]
);
こうすることで、思ったよりも普通のCSSでの実装は簡単にできることがわかった。
上で述べた一部の注意点があることだけを心に留めておく必要がある。
と思ったが、他の人から別にe.target
とe.currenttarget
が同じかを判断すればいいのではと言われ、確かにとなったのでここに追記しておく。
React Transition Group
2.React Transition Groupは、Reactのtransition
を簡単に実装できるReactのAdd-onライブラリである。アニメーションのライブラリというわけではなく、transition
に沿ってCSSを適用しやすいようにclassName
を付与したりすることに特化したライブラリである。
emotionを使用している都合上、className
を変更するCSSTransition
が使いにくかったので、一般的なTransition
コンポーネントを使用して、実装を行った。
<Transition
in={show}
appear
unmountOnExit
timeout={2000}
onEntering={() => animationCallbackFunctions?.onStartShow?.()}
onEntered={() => animationCallbackFunctions?.onEndShow?.()}
onExiting={() => animationCallbackFunctions?.onStartHide?.()}
onExited={() => animationCallbackFunctions?.onEndHide?.()}
nodeRef={nodeRef}
>
{(state) => (
<div ref={nodeRef} style={styles[state]}>
{children}
<p>{state}</p>
</div>
)}
</Transition>
使用する際の注意点として、appear
を付与することである。これを付与しないと、初回の表示時にアニメーションが実行されない。これを付与せずに初回レンダリングのみアニメーションが実行されないという問題にかなりはまった。
そして、大事なのがunmountOnExit
を付与することで、非表示時(onExitedが呼ばれた際)にコンポーネントを消すことができる。
nodeRef
を指定してないと、findDomNode
が呼ばれてしまい、strict Mode
下ではWarningが出るので、指定することがおすすめされているらしい。
全体的にCallback関数などがまとまっており、かなり見通しのよいコードになるとは思った。
その一方で、transitionの変更に支える手法が
- timeoutを指定する
- addEndListenerを指定する
のどちらかであるらしく、任意のアニメーションに対応するためには1で述べたようにonTransitionEnd
などのイベント関数を使用する必要があり、これはバブリングなどを考慮する必要がでてくることに注意する必要がある。
また、React Transition Groupという名前だけあって、transition
に特化しているので、animation
だと使えないことに注意する必要がある。
React Spring
3.React Springは、Reactのアニメーションライブラリとして知られている。
React Spring自体の機能に非表示の際にアニメーションを走らせてからコンポーネントをunmountするような仕組みは存在しないので、自力で作る必要がある。そのため、NormalCSSと同じようにuseEffectなどを用いて自前でStateを管理することになる。つまり、React Springを用いて、このような問題に対処することはかなり難しいと感じた。
どちらかというと、「アニメーション」の複雑な動きを実装するためのライブラリとしての役割が強いと思う。
const [visible, setVisible] = useState(show);
const [springs, api] = useSpring(() => {
return {
opacity: 0,
config: {
duration: 500,
},
};
});
//useEffect等でステート管理
useEffect(() => {
if (show && !visible) {
setVisible(true);
api.start({
opacity: 1,
onRest: () => {
animationCallbackFunctions?.onEndShow?.();
},
});
animationCallbackFunctions?.onStartShow?.();
}
if (!show && visible) {
api.start({
opacity: 0,
onRest: () => {
setVisible(false);
animationCallbackFunctions?.onEndHide?.();
},
});
animationCallbackFunctions?.onStartHide?.();
}
}, [show, api, animationCallbackFunctions, visible]);
Framer Motion
4.Framer Motionも、Reactのアニメーションライブラリの1つである。
Framer Motionは今回の取り上げている問題を解決するために、AnimatePresence
というコンポーネントを用意している。
AnimatePresence
で囲んだ部分についてその内部で呼ばれるmotion
コンポーネントについて、exit
を指定することで、非表示の際にアニメーションを走らせてからコンポーネントをunmountすることができる機能がある。
これを使うと非常に楽に今回の問題を解決することができる。
<AnimatePresence>
{show && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 2 }}
onAnimationStart={(def: any) => {
if (def.opacity === 1) {
animationCallbackFunctions?.onStartShow?.();
} else {
animationCallbackFunctions?.onStartHide?.();
}
}}
onAnimationComplete={(def: any) => {
if (def.opacity === 1) {
animationCallbackFunctions?.onEndShow?.();
} else {
animationCallbackFunctions?.onEndHide?.();
}
}}
>
{children}
</motion.div>
)}
</AnimatePresence>
FramerMotionの欠点は、motion
コンポーネントの中でanimationCallbackとして、onAnimationStart
とonAnimationComplete
しかない点だ。
これだと、initial -> animateのアニメーションが使用されたか、animate -> exitのアニメーションが使用されたかを判別するためのコードを書く必要があり、それを現在の状況に応じて分けて書く必要がある。
つまり、cssのみを切り出して、アニメーションを自由に指定できるコンポーネントを作るみたいな形にすることが難しいという点が少々気になった。
そのため、アニメーションごとに別々のコンポーネントを作る必要があるとは思う。
ただ、そのようなことをせずに、アニメーション実装したいというのであれば、最も簡単に実装できるライブラリだと思う。
まとめ
わざわざライブラリを使う必要があるのかというと、そうでもないと思った。普通のCSSで実装できるのであればそれで良いような気がした。
アニメーションライブラリを使用する点として、Springアニメーションを使用したいと言った時などが挙げられると思った。
Springアニメーションを内部実装したくない場合は、React Spring
がFramer Motion
が良い選択肢だと思う。
ただ、React Springだと今回のような場合はコードが煩雑化するので、気軽に使いたいという場合はFramer Motion
という選択肢が複雑なCallbackなどを必要としない場合は、どの方向にも整っていて良いと思った。
ちなみに今回実装したコードのレポジトリはこちら
まだまだ理解できてない部分あると思うので、間違っているところやこうすれば楽にできるよなどの改善点があれば教えていただけると嬉しいです。
Discussion