紡Book の勉強ノートに戻る
技術ブログ2026.04.09

motion + React 19 で MotionValue が style に反映されない。13.0.0-alpha.0 で逃げた話

Reactmotionframer-motionReact19

スクロール連動のアニメーションを motion(旧 framer-motion)で組んでいたら、値は正しく更新されているのに画面が動かないというバグを踏んだ。

最初は「React state にブリッジする」ワークアラウンド(=根本解決ではなく、とりあえず動かすための回避策)を試したが、動かした直後に「これだと motion を使ってる意味が消える」と気づいてやり直した。最終的には motion を 13.0.0-alpha.0(プレリリース版)に上げて動かす形に着地した。ただし「stable な v13 が出たら入れ替える」TODO が残った状態で、いまも頭の片隅に置いている。


この記事でわかること

  • motion v12 + React 19 の組み合わせで踏みうるレンダリングの噛み合わせバグ
  • 最初に試した「React state にブリッジする」逃げ道と、それを捨てた理由
  • 最終的に効いたのは情報がほぼ無いプレリリース版 13.0.0-alpha.0。stable 移行までの TODO 管理

症状

<motion.div style={{ opacity: motionValue }}> のように MotionValue を style に直接渡しても、DOM に反映されない。

debug パネルで MotionValue の中身は正しく更新されているのが見える(useMotionValueEvent で change イベントを拾うと値が流れてくる)。

値の置き場所はメモリの MotionValue。DB にもファイルにも保存しない。MotionValueメモリ・保存しない値は正しく更新されているchangedebug パネル値は見える見るだけ。置き場所ではないstyle へ直接書く経路が途切れるDOM の style(画面)ここには届かない。画面は動かない

最初は「実装のどこかがおかしいのか」と疑って、props の渡し方や型をあれこれ変えて試した。何をやっても変わらない。motion v12 の DOM 側の最適化が React 19 のレンダリング仕組みと噛み合っていない可能性 に思い至るまで、そこそこ時間を溶かした。


最初に試した逃げ道:MotionValue を React state にブリッジする

「DOM に反映されないなら、React state に値を引き写してから普通の style で当てれば動くはず」と考えて、こういうフックを AI に書いてもらった。

ts
// hooks/useMotionValueState.ts
export function useMotionValueState<T>(mv: MotionValue<T>, initial: T): T {
    const [v, setV] = useState<T>(initial);
    useMotionValueEvent(mv, 'change', (val) => setV(val));
    return v;
}

使う側はこう。

tsx
const opacity = useMotionValueState(motionValue, 1);
return <div style={{ opacity }}>...</div>;

<motion.div> を <div> に置き換えて、MotionValue ではなく state を style に渡す。useMotionValueEvent で MotionValue の変化を拾い、setState で React 側にブリッジするだけ。

これで動くは動いた。 ただ、動かしてみてから「いや、これだと motion を使ってる意味がほぼ無いな」と気づいた。


なぜこの逃げ道を捨てたか

MotionValue 本来の売りは 「再レンダーを起こさずに直接 DOM を書き換える」 ことです(冒頭の MotionValue の説明で書いた、毎フレーム再レンダーを避けるための仕組みです)。それを setState でブリッジするということは、毎変化で再レンダーを発火させていることになります。

どちらもメモリにしか無い。DB にもファイルにも保存しない。本来MotionValueメモリ・保存しないReact state通らないDOM の style再レンダーしない逃げ道MotionValueメモリ・保存しないsetStateReact stateメモリ・保存しない再レンダーdiv の style画面フックは関所で、置き場所ではない。通した値が React state に載る。motion を経由しているが、素の React state でスクロール値を持つことと同じ。

しかも <motion.div> を全部 <div> に置き換えていくと、motion の宣言的な記法(animate={{ ... }} や whileHover のような書き味)も道連れで消えます。

「動くけど、これは motion を使う意味が無くなる方向の妥協だな」と判断して、このフックは捨てた。 動くものを一度組んでから捨てることになって、地味にダメージがあった。


結果的に効いたもの:motion 13.0.0-alpha.0 に上げた

「アプリ側のコードを直すんじゃなくて、ライブラリ側の修正を取りに行くほうが筋が良さそうだ」と方針を切り替えた。

正直に書いておくと、自分でググっても辿り着けなかったので、ここは AI に頼んで Issue/PR を漁ってもらった。返ってきたのが 13.0.0-alpha.0 というプレリリース版で、ここに React 19 対応の修正が入っている、と。改めて GitHub Releases や Changelog を自分で見にいっても、このバージョンの情報は本当に少ないです。「これ AI どうやって見つけてきたんだ」と思いつつ、試しにそっちに上げてみた。

json
// package.json
"motion": "13.0.0-alpha.0"   // もとは "^12.38.0"

これで <motion.div style={{ opacity: motionValue }}> がそのまま素直に動いた。 ブリッジ用の useMotionValueState フックは丸ごと不要になった。

「最初のワークアラウンドは何だったんだ」とは思ったけど、一度動かしてみないと「motion の旨味が消える」ことには気づけなかったので、無駄ではなかった、と整理した。


TODO として残しているもの

ただ、これは alpha 版に乗っかっている状態で、本番運用としては健全じゃないです。

  • いま動いているのは 13.0.0-alpha.0(プレリリースの中でも一番上流寄り)です
  • stable な v13 が出たら、そっちに切り替えます
  • 切り替え時点で API や挙動の差分が出る可能性があるので、再確認が要ります

つまり「いまは動いているが、将来必ず手を入れる」TODO が1個残った形だ。 package.json のバージョン指定の近くにコメントを残して、「v13 stable で React 19 対応が入ったら入れ替える」と未来の自分に伝えておいた。

alpha で凌ぐのは個人開発だから許される選択で、仕事で同じことをやるなら CI でバージョンを固定する/切り戻し手順を持つ、までセットだと思う。

追記:この 13.0.0-alpha.0 は後日、別の場面で火種になった。Next.js 16 でのビルド(prerender)が落ちる調査をしたとき、この alpha 版が原因の容疑者として浮上する。顛末は別記事 ライブラリの版を変えても、ビルドが落ちる場所が移動するだけだった — motion + React 19 + Next.js 16 の SSR エラー調査 に書いた。React 19 対応のために上げた alpha が、今度は別のレイヤーで足を引っぱる ── alpha で凌ぐ選択の「つけ」の回り方として残しておく。


学んだこと

  • ワークアラウンドは「動くこと」だけで判定しません。そのライブラリを使う意味が残っているかまで見て初めて、採用か破棄かが決まります
  • ライブラリ × フレームワークの噛み合わせ問題は、アプリ側のコードを直す前に Issue/PR トラッカーまで見にいくと、「ライブラリ側ですでに直っている」が見つかることがあります
  • 情報がほぼ無いプレリリース版に辿り着くのは、自分のググり力では難しいことがあります。Issue/PR を AI に漁ってもらうのは、こういう場面で素直に効きます
  • alpha 版で凌ぐのは個人開発ではアリです。ただし「TODO として残してる」自覚と、stable 移行時の再確認をセットで持っておきます
  • debug 時は「値は正しいのに反映されない」を切り分けるために、MotionValue 側のイベントを拾って値だけ確認するパネルを一時的に置くと早いです

この記事は2026年4月時点の自分の理解で書いています。motion(旧 framer-motion)と React 19 はどちらも変化が速い領域なので、最新の公式ドキュメントも合わせて参照してください。記事内で「13.0.0-alpha.0 でしか直っていなかった」と書いた部分は、現時点ではすでに v13 stable に取り込まれている可能性が高いです。