React TSXの掛け算スライドショーをVue.jsに移植した記録
React TSXで書かれた掛け算の面積図スライドショーを、Nuxt 3のVueページとして動かすところまで持っていった。数値の入力からスライド遷移、P3ピースの回転アニメーションまで一通り移植が完了している。途中、CSSトランジションが効かない問題に2時間ほど手を止められた。
移植の方針
元のコンポーネントはReact + Tailwind CSSで構成されていた。移植先のmdx-playgroundではTailwindを使っていないため、scoped CSSで書き直す方針を採った。
Nuxt 3のカスタムVueページとして apps/web/app/pages/multiplication-slideshow.vue に配置し、definePageMeta でメタ情報を設定した。
<script setup lang="ts">
definePageMeta({
title: '十の位が1の2桁×2桁 速算スライドショー',
publishedAt: '2026-03-22',
tags: ['education', 'math'],
includeInList: true
})
</script>
includeInList: true を指定することで、トップページの記事一覧にも表示される。
CSSトランジションが動かない
移植自体はスムーズに進んだが、P3ピースの回転アニメーションで壁にぶつかった。
症状
スライドを切り替えると、P3ピースが回転アニメーションなしで一瞬で位置が変わる。他のピース(P1, P2, P4)は transition: all 0.7s ease-in-out で滑らかに移動するのに、P3だけ瞬間移動する。
原因を追う
最初はCSSの書き方を疑った。transform: rotate(90deg) をインラインスタイルで当てているので、transition プロパティに transform が含まれているか確認した。含まれている。
次に、Vue DevToolsでDOM要素の再生成が起きていないか確認した。ここで原因が見えた。
v-for のキーに currentSlide を含めていた。
<!-- NG: スライドが変わるたびにDOM要素が再生成される -->
<div
v-for="(piece, index) in pieces"
:key="`piece-${index}-${currentSlide}`"
class="piece"
>
:key に currentSlide が入っているので、スライドを切り替えるたびにVueはDOM要素を破棄して新しく作り直す。新しいDOM要素には前の状態がないから、CSSトランジションの「from」が存在しない。結果、アニメーションなしで最終状態がいきなり描画される。
解決: 安定したキーに変更
:key をピース固有のIDに変更した。
<!-- OK: DOM要素が再利用され、CSSトランジションが効く -->
<div
v-for="(meta, index) in piecesMeta"
:key="meta.id"
class="piece"
:style="getPieceStyle(index)"
>
piecesMeta はスライドによらない静的な情報(ID、色、ラベル)を持つ配列で、キーが変わらないのでDOM要素が再利用される。スタイルの変更はリアクティブな computed で計算し、CSSトランジションが補間してくれる。
この変更を入れた瞬間、P3ピースが90度回転しながら移動するアニメーションが動いた。
P3回転の実装構造
P3ピースは初期配置では a x 10 の縦長だが、並び替え後は 10 x a の横長として上部グループに組み込まれる。CSS transform: rotate(90deg) で見た目の回転を実現し、内部テキストは逆回転で正立を保つ。
// ピースのスタイル計算(P3は回転あり)
const getPieceStyle = (index: number) => {
const style: Record<string, string> = {
left: `${positions.value[index].x}px`,
top: `${positions.value[index].y}px`,
width: `${pieceWidths.value[index] * SCALE}px`,
height: `${pieceHeights.value[index] * SCALE}px`
}
if (index === 2 && currentSlide.value >= 1) {
style.transform = 'rotate(90deg)'
}
return style
}
CSSは1行だけ。
.piece {
position: absolute;
transition: all 0.7s ease-in-out;
}
transition: all が left, top, width, height, transform をすべて補間するので、位置移動と回転が同時に滑らかに動く。
worktree環境でのpnpm install問題
今回の作業はworktreeブランチで進めていた。devコンテナ内で pnpm dev を実行しようとしたところ、2つの問題が発生した。
- pnpmが見つからない: devコンテナのPATHにpnpmが含まれていなかった。
npm install -g pnpmで解決 - node_modulesの権限混在: ホスト側(vscodeユーザー)とコンテナ側(rootユーザー)で作成されたファイルが混在し、パーミッションエラーが出た。
node_modulesを削除してpnpm installをやり直して解決
worktreeで新しいブランチを切った場合、node_modules は共有されないので pnpm install が毎回必要になる。この注意事項をCLAUDE.mdにも追記した。
トップページへのリンク追加
移植が完了した後、トップページの「学習・クイズ」セクションにスライドショーへのリンクを追加した。/multiplication-slideshow で直接アクセスできる。
振り返り
:key の設計ミスでCSSトランジションが無効化されるパターンは、Reactの key propでも同じことが起きる。「アニメーションさせたいならDOM要素を再生成しない」という原則は、フレームワークを問わず共通している。
今回の移植でReactとVueの差異を改めて体感した。Reactでは useState + JSXで書いていた部分が、Vueでは ref + computed + テンプレートに分かれる。ロジックの分離という点ではVueの方が構造が見えやすいと感じた。一方でJSXの自由度が恋しくなる場面もあり、どちらが良いとは言い切れない。