リアクションとフレームコールバック
共有値の変化に反応するuseAnimatedReactionと、毎フレーム関数を走らせるuseFrameCallback、その薄いラッパーであるuseTimestampを扱います。UIスレッド上で「値を監視する」「時間で駆動する」ための道具です。
これまでは、共有値をスタイルに結びつけて動きを作ってきました。この章で扱うのは、少し毛色の違う2つの道具です。ひとつは共有値の変化を監視して何かを起こすuseAnimatedReaction、もうひとつは毎フレーム関数を実行するuseFrameCallbackです。どちらもUIスレッド上で動き、これまで学んだしくみの延長線上にあります。
useAnimatedReaction — 値の変化に反応する
useAnimatedReactionは、共有値の変化に反応するためのフックです。特に、前回の値と今回の値を比べて「変わったときだけ何かする」といった処理に向いています。

値が変わった瞬間、各フレーム、経過時間という3つの異なるタイミングで処理を組み立てられます。
2つの関数を渡します。第1引数のprepareは、反応したい値を返す関数です。第2引数のreactは、その値が更新されたときに呼ばれ、現在の値と前回の値の2つを受け取ります。前回の値は初回はnullです。
import { useAnimatedReaction } from "react-native-reanimated";
function App() {
useAnimatedReaction(
() => {
return sv.value;
},
(currentValue, previousValue) => {
if (currentValue !== previousValue) {
// 変化したときの処理
}
}
);
}
prepareとreactはどちらも自動でworklet化され、UIスレッドで動きます。つまり、フレームごとに更新される共有値を、UIスレッド上でそのまま監視できます。JS側に結果を反映したいときは、これまでどおりrunOnJS(第6部1章で見たscheduleOnRNの従来名)で渡します。
次のデモでは、左右に往復する箱の位置を「左・中央・右」の3つのゾーンに畳み込み、ゾーンが変わった瞬間だけJS側のラベルを更新しています。prepareが返すゾーン番号が前回と変わったかをreactで比べ、変わったときだけrunOnJSする形です。位置そのものは毎フレーム変わりますが、ラベルの更新はゾーンをまたぐ数回に抑えられます。
useAnimatedReactionを使ううえで、いちばん気をつけたいのが無限ループです。prepareで読んだ共有値をreactの中で書き換えると、その書き換えがまたprepareを発火させ、止まらなくなります。
useAnimatedReaction(
() => {
return width.value;
},
(currentValue) => {
// 🚨 無限ループ
width.value += currentValue;
}
);
もうひとつ。技術的にはReactのstateなど普通の値にも反応できますが、それはuseEffectの仕事です。useAnimatedReactionは、あくまでUIスレッド上の共有値を監視するための道具だと考えてください。
なお、本書のデモではBabelプラグインがないため、prepareが読む共有値を第3引数の依存配列で明示しています。
useFrameCallback — 毎フレーム実行する
useFrameCallbackは、フレーム更新のたびに関数を実行します。コールバックにはframeInfoオブジェクトが渡され、時間に関する3つの情報が入っています。
timestamp: 直近のフレームが描画されたシステム時刻(ミリ秒)timeSincePreviousFrame: 前のフレームからの経過時間(ミリ秒)。有効化後の最初のフレームではnull。2フレーム目以降は60Hzで約16ms、120Hzで約8mstimeSinceFirstFrame: 有効化されてからの経過時間(ミリ秒)
戻り値のオブジェクトで、コールバックの開始と停止を制御できます。setActive(true / false)で切り替え、isActiveで現在動いているかを確認できます。第2引数のautostart(既定はtrue)で、最初から動かすかどうかを指定します。
import { useFrameCallback } from "react-native-reanimated";
function App() {
const frameCallback = useFrameCallback((frameInfo) => {
// 毎フレーム、共有値を1増やす
sv.value += 1;
});
return (
<Button
title="開始 / 停止"
onPress={() => frameCallback.setActive(!frameCallback.isActive)}
/>
);
}
次のデモは、毎フレーム回転を少しずつ進めながらフレーム数を数え、timeSincePreviousFrameからFPSを計算して表示します。表示は毎フレームだと重いので、15フレームごとにまとめてJS側へ渡しています。停止ボタンでsetActiveを切り替えると、回転もカウントも止まります。
コールバックはUIスレッドで動くので、毎フレームの重い計算をJSスレッドに載せずに済みます。ただし本当に毎フレーム呼ばれるので、中身は軽く保つのが鉄則です。
useTimestamp — 時間で駆動する
useTimestampは、現在のフレームのタイムスタンプを共有値として返すフックです。中身はuseFrameCallbackの薄いラッパーで、「今の時間からアニメーションを組み立てたい」というよくあるケースを手軽にするためのものです。
import { useTimestamp } from "react-native-reanimated";
function PulsingDot({ visible }) {
const timestamp = useTimestamp(visible);
const animatedStyle = useAnimatedStyle(() => ({
opacity: 0.5 + 0.5 * Math.sin(timestamp.value / 300),
}));
return <Animated.View style={[styles.dot, animatedStyle]} />;
}
timestamp.valueは最初のフレームからの経過時間(ミリ秒)で、毎フレーム更新されます。引数で更新のオン・オフを切り替えられます。上の例のように、時間を三角関数に通せば、脈打つような周期的な動きが素直に書けます。
パフォーマンスのため、複数のコンポーネントでそれぞれuseTimestampを呼ぶより、1つのインスタンスを上位で作ってpropsやコンテキストで配るほうが望ましい、という点だけ覚えておいてください。
この章のまとめ
useAnimatedReactionは共有値の変化を監視する道具、useFrameCallbackは毎フレーム実行する道具、useTimestampはその時間版の近道です。いずれもUIスレッド上で動き、共有値を軸にしている点は共通しています。宣言的なuseAnimatedStyleでは表現しづらい「変化に応じた分岐」や「時間で駆動する動き」を書きたくなったら、この章のフックを思い出してください。