Rork で作ったアプリに気の利いたアニメーションを入れたくて React Native Reanimated を追加した瞬間、赤い画面いっぱいに Reanimated 2 failed to create a worklet だの Tried to synchronously call function from a different thread だのが出てきて手が止まる、という経験はありませんか。私自身、Rork Companion のプレビューでは軽やかに動いていたトランジションが、EAS でビルドした実機でだけ worklet エラーを吐き、原因を詰めるのに半日かかりました。
Reanimated の worklet は書き味こそ通常の関数とほとんど変わりませんが、裏側では UI スレッド上で独立して実行されるよう Babel プラグインが変換をかけています。そのため「同じ書き方なのにエラーが出る/出ない」が起こりがちです。ここでは Rork 生成コードでよくぶつかる順に、6 つのチェックポイントを上から潰していく流れでまとめます。Reanimated v3 系を前提にしていますが、v2 系でもほぼ同じ考え方で切り分けられます。
まず「どの段階で落ちているか」を見分ける
worklet まわりのエラーは、大きく 3 つのレイヤーに分かれます。
- ビルド時に Babel プラグインが変換できていない(開発ビルド起動時点で落ちる)
- 初回レンダリング時に worklet が生成できない(
useAnimatedStyleなどの呼び出しで落ちる) - 実行時にスレッド越境で落ちる(ジェスチャーやアニメーションが走った瞬間に止まる)
最初に確認したいのは、Metro のコンソールに出ているメッセージがどのレイヤーのものかです。Reanimated 2 failed to create a worklet, maybe you forgot to add Reanimated's babel plugin? が出ていればレイヤー 1、ReferenceError: foo is not defined のようなものが worklet 内から出ていればレイヤー 2、Tried to synchronously call function 系であればレイヤー 3 です。ログを見ずに手を動かすと、同じ現象の別原因を踏み続けて時間を溶かします。
1. babel.config.js に react-native-reanimated/plugin が入っているか
最頻出の原因はこれです。Rork が自動生成した babel.config.js には Expo のプリセットしか入っていない場合があり、Reanimated を手動で npm install した直後に発生します。
// ❌ プラグイン未設定のまま
module.exports = function (api) {
api.cache(true);
return {
presets: ["babel-preset-expo"],
};
};// ✅ 必ず plugins 配列の最後に置く
module.exports = function (api) {
api.cache(true);
return {
presets: ["babel-preset-expo"],
plugins: ["react-native-reanimated/plugin"],
};
};ポイントは 配列の最後に置くことです。公式ドキュメントにも明記されていますが、他のプラグインより後ろにないと worklet 化の変換が正しく走りません。変更後は npx expo start --clear のように Metro のキャッシュをクリアしないと反映されないので、ここまでセットで覚えておくのがおすすめです。
もう 1 点ありがちなのが、Metro のプロセスが裏でしぶとく残っているケースです。babel.config.js を直して --clear で起動し直したのに同じ worklet エラーが出続けるときは、古い Metro インスタンスが動き続けている可能性があります。killall -9 node で一度すべての Node プロセスを落としてから起動し直すと、ほとんどの場合すんなり通ります。派手ですが、再発防止として覚えておいて損はありません。
2. ネイティブ側が古いまま残っていないか
Reanimated はネイティブモジュールを同梱しているため、JavaScript 側を更新しただけでは動きません。Rork Companion で確認できていた挙動が EAS ビルドや実機インストール済みアプリで再現しないときは、ほぼこのパターンです。
# 開発用ビルドをクリーンにやり直す
npx expo prebuild --clean
npx expo run:ios # または run:android
# EAS 側でキャッシュが残っているときは
eas build --platform ios --clear-cacheprebuild --clean は ios/ android/ ディレクトリを作り直すので、中身を手で編集している場合は差分を退避してから実行してください。Rork が生成した初期状態のまま使っているなら、そのまま実行して問題ありません。
3. worklet 内で普通の JS 関数を呼び出していないか
worklet の中で呼べるのは worklet だけ、というのが Reanimated のいちばん重要なルールです。Rork が生成したコードでは、通常のユーティリティ関数を useAnimatedStyle の中で素直に呼び出しているケースが結構あります。
// ❌ worklet の中から JS スレッドの関数を呼ぶとエラー
import { useAnimatedStyle } from "react-native-reanimated";
function clamp(value: number, min: number, max: number) {
return Math.min(Math.max(value, min), max);
}
const style = useAnimatedStyle(() => {
// 実行時に「Tried to synchronously call function clamp」などが出る
const opacity = clamp(progress.value, 0, 1);
return { opacity };
});// ✅ 関数側に "worklet"; を付けて worklet 化する
function clamp(value: number, min: number, max: number) {
"worklet";
return Math.min(Math.max(value, min), max);
}
const style = useAnimatedStyle(() => {
const opacity = clamp(progress.value, 0, 1);
return { opacity };
});関数の 1 行目に "worklet"; ディレクティブを置くと、Babel プラグインがその関数も worklet として変換してくれます。共通ユーティリティは utils/worklets.ts のようにまとめ、すべての関数に "worklet"; を宣言しておくと、使うたびに迷わなくて済みます。
4. JS スレッドの状態を更新したいのに runOnJS を忘れていないか
worklet 側から React の setState やナビゲーションを呼びたくなる場面は多いですが、直接呼ぶと 3 番と同じエラーになります。この場合は runOnJS でラップして JS スレッドに戻す必要があります。
// ❌ worklet の中で setState を直接呼んでいる
import { useAnimatedGestureHandler } from "react-native-reanimated";
const gestureHandler = useAnimatedGestureHandler({
onEnd: () => {
setSwipeCount(prev => prev + 1); // JS スレッドの関数を直接呼んでいる
},
});// ✅ runOnJS で JS スレッドに明示的に戻す
import { useAnimatedGestureHandler, runOnJS } from "react-native-reanimated";
const gestureHandler = useAnimatedGestureHandler({
onEnd: () => {
runOnJS(setSwipeCount)(swipeCount + 1);
},
});runOnJS は値しか渡せないので、setSwipeCount(prev => prev + 1) のような関数型更新を使いたい場合は、JS 側で受けるラッパー関数を用意しておくときれいに書けます。
5. worklet が最新の state を見ていない(closure 問題)
worklet は定義された時点の値をキャプチャしてしまいます。React コンポーネントの state や props を直接参照していると、古い値を掴み続けて「動いてはいるけれど最新の値にならない」という紛らわしい状態になります。
// ❌ 普通の state を worklet から参照すると古い値のままになる
const [threshold, setThreshold] = useState(100);
const style = useAnimatedStyle(() => {
return { opacity: progress.value > threshold ? 1 : 0 };
});対処は 2 つあります。ひとつは、shared value に移してしまう方法です。
// ✅ useSharedValue に移す
const threshold = useSharedValue(100);
// 変更するとき
threshold.value = 150;
const style = useAnimatedStyle(() => {
return { opacity: progress.value > threshold.value ? 1 : 0 };
});もうひとつは、useDerivedValue で JS 側の値を監視する方法です。外部ライブラリの state などを使いたいときはこちらが便利です。
// ✅ useDerivedValue で JS 側の値をブリッジ
const thresholdShared = useDerivedValue(() => {
return threshold; // JS 値の変更を検知して worklet 側に伝える
}, [threshold]);依存配列を忘れると更新されないので、useEffect と同じ感覚で揃えておくのが安全です。
6. Hermes と Expo Go のバージョン不整合
見落としがちですが、Expo Go の古いバージョンでアプリを確認すると、ネイティブ側の Reanimated バージョンが噛み合わずに worklet が動かないことがあります。Rork Companion と実機で挙動が違うときは、app.json の runtimeVersion と使っている Expo Go のバージョンを合わせているかを確認してください。
# Reanimated のバージョン確認
npx expo install --check
# 期待: react-native-reanimated が Expo SDK の推奨バージョンに揃っているExpo の案内するバージョンからずれていると、新しい API が内部で未実装だったり、worklet の変換が合わなかったりします。npx expo install react-native-reanimated で入れ直すと、SDK に対応したバージョンに寄せてくれます。実機で確認するときは、開発ビルド(development build)を作って Expo Go 依存から抜けておくと、この種のトラブルは一気に減ります。
実用上の小技として、学習用の最小再現画面をプロジェクト内に 1 枚だけ残しておくのもおすすめです。スプリングと PanGestureHandler を組み合わせただけの簡単な画面で、worklet の各パターンを切り離して試せる「サンドボックス」として使えます。本番画面でエラーが出たときは、再現コードをそのサンドボックスに持ち込んで切り出すと、他の処理に引っ張られずに本質だけを確認できます。ごちゃごちゃした画面でデバッグしていると、半日溶かす原因になりがちです。
また、Rork 公式の Companion と EAS だけで開発している方は、npx expo install react-native-reanimated を自分で叩かない限りバージョン不整合にぶつかることはほぼありません。Expo SDK を自分でアップグレードしたときは、直後に npx expo install --check を必ず回す──これを習慣にしておくと、バージョン不整合系のエラーはほぼシャットアウトできます。
次にやること
worklet 関連のエラーは「書き方の問題」に見えて、実際はビルド設定・スレッドモデル・キャッシュの問題であることがほとんどです。今日すぐにやるなら、babel.config.js を開いて react-native-reanimated/plugin が最後に入っているかを確認し、開発ビルドを --clear つきで作り直すだけでも半数以上のケースが片付きます。
より複雑なジェスチャー連携まで踏み込む場合は、関連記事の Rork で Reanimated とジェスチャーハンドラを組み合わせる高度なアニメーション設計 と Rork アプリのクラッシュ・白い画面をデバッグする完全ガイド を先に読んでおくと、今回のチェックリストが線でつながって理解が深まるはずです。
worklet の「書けるけど何が起きているか分からない」状態から抜け出したい方には、手元に置いておいて損のない一冊です。