react-spring v8では素直に無限ループをする方法が用意されていない。(たぶん)
whileで無限ループを作ってしまうしかないようだ。
Best practice for infinite loops?
v9ではloop propが用意されてるらしいのでそちらを使うと良さそう。
How can one-way animation looping be implemented? #956
ちなみにv9.0.0-rc3を使おうとしたところ、変なところでハマってうまくいかなかった。
コンポーネントの再レンダーのタイミングでしかアニメーションが更新されないような現象。
内部的にはreact-springはちゃんと動いているけど、コンポーネントの再レンダーのトリガーにできてない感じの挙動。
これが解決できればv9系を使う方が幸せっぽいのでできた人は情報求む。
閑話休題。
一応while(true)
で無限ループは作れるし動くんだけど、それを使ってるコンポーネントが再レンダーされるとブラウザがクラッシュする。
無限ループしてる関数がゾンビ的な挙動をしているのではないかと推測する。
const { opacity } = useSpring({
from: { opacity: 0 },
// フワッと現れて、その後おぼろげにフワフワする感じの不透明度アニメーション
to: async (next, cancel) => {
await next({ opacity: 1 })
while (true) {
await next({ opacity: 0.2 })
await next({ opacity: 0.5 })
}
}
})
ちなみに今回はこのアニメーションのオンオフを親コンポーネントからコントロールしたい。
例えばactive:boolean
みたいなpropで、true
ならさっきのふわふわアニメーションを頭から再生して、false
なら{opacity:0}
に落ち着くみたいな挙動をさせたい。
const { active } = props
const { opacity } = useSpring({
from: { opacity: 0 },
to: active ?
// active==trueならふわふわさせる
async (next, cancel) => {
await next({ opacity: 1 })
// ※※※ここが大事※※※
// activeの値がキャプチャされてしまうので、
// あとでfalseになってもループを抜けられない
while (active) {
await next({ opacity: 0.2 })
await next({ opacity: 0.5 })
}
} :
// active==falseなら消える
{ opacity: 0 }
})
最終的にうまくいったコードはこちら。
方針としては、
-
useSpring
に関数を渡すことで、toの再設定関数(この例ではsetAnimation
)を取得する -
active
を依存関係に持つuseEffect
を使用することで、toの再設定をactive
の変更時に限定する
(v9系ではuseSpring
に依存関係を持たせてよりスマートに書けるようになるっぽい) - whileで
activeRef=useRef(active)
を見るように変更することで、active
の値の変更をキャッチできる
-> これにより、whileループがゾンビ化するのを防げる
const { active } = props
const activeRef = useRef(active)
// useSpringは関数を渡されると、SpringValueだけじゃなく更新関数やstop関数も返す
const [{ opacity }, setAnimation] = useSpring(() => ({
// toの値はあとで設定するので初期値のみ指定
opacity: 0
}))
useEffect(() => {
// activeが変更されたことをwhileに通知する
activeRef.current = active
setAnimation({
to: active ?
// active==trueならふわふわさせる
async (next, cancel) => {
await next({ opacity: 1 })
// activeRef.current==falseならループを抜ける
while (activeRef.current) {
await next({ opacity: 0.2 })
await next({ opacity: 0.5 })
}
} :
// active==falseなら消える
{ opacity: 0 }
})
},
// useEffectはactiveが変わった時に呼ばれる
[active])
たぶん、タイミングによっては{ opacity: 0 }
にsetAnimation
した後にwhile内のawait next()
が効いて{ opacity: 0.5 }
で止まっちゃう現象が起こるような気もする。
ちゃんと対応するならPromiseをrejectさせるみたいなことをやると良いのだと思う。
今回はとりあえず動いてそうなので無指する。