ブログ

【Vue3】再描画の最適解はkey!v-ifや$forceUpdateとの使い分け

この記事をSNSでシェア!

はじめに

現在、インフラ環境をAWS上に構築し、その上で開発しているフロントアプリケーションにVue3を使用しています。
Vue3で開発を進めていく中で、「再描画されない」「状態がリセットされない」問題に遭遇することがあります。
特に本記事のきっかけとなった、「API更新後の表示が古いまま変わらない」という課題は、Vueのリアクティブの仕組みへの理解不足が原因でした。
そこで、先輩への相談や自力での調査を通じて、key、v-if、$forceUpdate()といった方法を使い分けることで解決に繋がるケースが多いと知りました。
本記事では、これらの使い分けと実装方法を実例とともに紹介します。
また、本記事で紹介するコード例は Vue SFC Playground などのオンライン環境で試すことができます。

発生した問題:API更新後の表示が古いまま変わらない

直面した課題

Vueはリアクティブなので「値を更新すればUIも更新される」と思い込んでいました。
今回作成していたのは、データベースにあるフラグをAPI経由で取得し、1なら「〇」、0なら「✕」と表示を切り替える機能です。

<!-- 子コンポーネント -->
<div>
  <div v-if="statusValue === 1">〇</div>
  <div v-if="statusValue === 0">✕</div>
</div>


このとき、「フラグ更新」ボタンを押してAPIを動かしても、データベースのフラグは更新されるのに、画面上の表示が更新されず、古い値のままでUIが更新されないという状況が発生しました。

試行錯誤

・まず「強制的に再描画できるメソッドがある」と知り、$forceUpdate()を使ってみましたが、結果は変わらず、再描画の問題だけではないと分かりました。
・原因は、Vueのリアクティブの仕組み非同期処理のタイミング にあると考えました。
 そこで keyを更新してwatchで管理すれば、更新後にコンポーネントを再描画できるのではないかと試しました。

解決策

最終的には、keyとwatchを組み合わせて再マウントすることで問題を解決しました。

<template>
  <div>
    <Child :key="childKey" :status="statusValue" />
    <button @click="updateFlag">フラグ更新</button>
  </div>
</template>

<script setup>
import { ref, watch } from 'vue'

// 子コンポーネントの key とフラグ状態
const childKey = ref(0)
const statusValue = ref(1)

// 「フラグ更新」ボタンを押したときの処理
function updateFlag() {
  // フラグ更新後、key を更新して子コンポーネントを再マウント
  childKey.value++
}

// key の変化を監視して最新フラグを取得
watch(childKey, () => {
  // sampleAPI は API からフラグを取得する関数
  statusValue.value = sampleAPI()
})
</script>


まず、「フラグ更新」ボタンの押下時にkeyの値を更新します。
watchでそのkeyの変化を監視し、keyが更新されたらAPIを再実行して最新のフラグ状態を取得。
こうすることで、Vueはkeyが変更されたことを検知してコンポーネントを再マウントし、APIから取得した新しいフラグの状態が画面に正しく反映されるようになりました。

この経験を通じて、「なぜこうなるのか?」「初期化と再描画ってどう違うのか?」を色々と調べたので、この記事にまとめておこうと思いました。

初期化と再描画の使い分けについて

Vue 3のリアクティビティの基本

Vue3では、データが変化すると自動で画面が更新される「リアクティブ」な仕組みがあります。

reactive
オブジェクト全体をリアクティブにする
ref
数値や文字列など、単一の値を扱う場合に使用する

この仕組みにより、ほとんどの場合は状態を更新するだけで再描画がされます。
しかし、複雑な構造の場合や、意図的に再描画をさせたい場合には key や v-if、$forceUpdate() を使うことで対応できます。
それぞれの使い分けを見てみましょう。

①key を使った再マウントの仕組み

Vueでは仮想DOMにより、画面全体を描画し直さず、変更があった箇所を効率的に更新します。
その際key属性は、要素を識別するために使われます
keyの値を変えることで、コンポーネントが再マウントされ、状態のリセットや初期状態に戻したい場面に利用できます。

Keyの違いによる挙動の差
・同じkey → 要素は保持され、中身もそのまま
・異なるkey → 別の要素と判断され、状態を初期化して再マウント

例:key を使ったフォームの初期化

この例では、Child コンポーネントに :key="formKey" を付けています。
ボタンを押すと formKey の値が変わり、Vue は Child を別のコンポーネントと判断して再マウントします。
その結果、子コンポーネント内の input の値などの状態が初期化され、フォームをリセットできます。

<template>
  <div>
    <Child :key="formKey" />
    <button @click="resetForm">フォームをリセット</button>
  </div>
</template>

<script setup>
import { ref } from 'vue'
import Child from './Child.vue'

// key を ref で作成
const formKey = ref(0)

// フォームリセット時に key を更新
function resetForm() {
  formKey.value++
}
</script>

<template>
  <div>
    <p>入力欄: <input v-model="input" /></p>
    <p>入力内容: {{ input }}</p>
  </div>
</template>

<script setup>
import { ref } from 'vue'

const input = ref('')
</script>
②v-if を使った再マウントの仕組み

keyの変更と同様に、v-ifを使ってコンポーネントを破棄し、その後再マウントすることで、コンポーネントの状態を初期化できます。
v-ifの条件がfalseになると、コンポーネントはDOMから完全に削除されます。
その後、条件をtrueに戻すことで、新しいコンポーネントとして作成し直されます。

特徴
・コンポーネント全体がDOMから削除されるため、ライフサイクルフックが実行される
・非同期処理($nextTick)で描画サイクルを待つ必要がある
・状態をリセットする目的で、keyの変更と並んでよく使われる

例:v-if を使ったフォームの初期化

この例では、showChildという真偽値を使い、Childコンポーネントの表示を制御しています。
ボタンを押すと、showChildを一度falseにしてコンポーネントを破棄し、$nextTickを使って次の描画サイクルでtrueに戻すことで再マウントされ、Childコンポーネントを初期化します。

<template>
  <div>
    <Child v-if="showChild" />
    <button @click="resetForm">フォームをリセット</button>
  </div>
</template>

<script setup>
import { ref, nextTick } from 'vue'
import Child from './Child.vue'

// v-if の制御用リアクティブ変数
const showChild = ref(true)

// フォームリセット関数
function resetForm() {
  // コンポーネントを一度破棄
  showChild.value = false

  // 次の描画サイクルで再表示
  nextTick(() => {
    showChild.value = true
  })
}
</script>

<template>
  <div>
    <p>入力欄: <input v-model="input" /></p>
    <p>入力内容: {{ input }}</p>
  </div>
</template>

<script setup>
import { ref } from 'vue'

const input = ref('')
</script>
③$forceUpdate() を使った再描画の仕組み

$forceUpdate() は、コンポーネントのテンプレートだけを手動で再描画するためのメソッドです。
リアクティブな状態は自動で画面に反映されますが、リアクティブ外のデータを変更した場合は Vue は検知できません。
そのような場合に $forceUpdate() を使うと、状態はそのまま保持したまま表示を更新できます。

特徴
・テンプレートを再描画する
・データの状態は保持される
・親コンポーネントで呼び出しても、子コンポーネントの再描画は起こらない

例:リアクティブでないデータの変更を表示に反映させる

この例では num はリアクティブ外のため、count を増やしても自動で画面に反映されません。
「再描画」ボタンで $forceUpdate() を呼ぶと、手動で表示を更新できます。

<template>
  <div>
    <p>カウント: {{ num.count }}</p>
    <button @click="increment">+1</button>
    <button @click="$forceUpdate()">再描画</button>
  </div>
</template>

<script>
// Vueのリアクティビティ外
let num = { count: 0 } 

export default {
  methods: {
    increment() {
      num.count++
    }
  },
  data() {
    // テンプレートで直接使えるようにする
    return { num }
  }
}
</script>

注意点
Vue の完全に自動化されたリアクティビティシステムを考えると、これが必要になることはほとんどありません。
Vue公式ドキュメント

まとめ

比較
        keyv-if$forceUpdate()
評価
目的コンポーネントのリセット
(再マウント)
コンポーネントのリセット
(再マウント)
表示だけの手動更新
(再描画)
イメージKey変更で古いコンポーネントを破棄、再作成DOMを完全に削除し、
ゼロから作り直す
データはそのまま、
テンプレート表示を更新
使いどころフォームリセット、
リストの並び替え
表示/非表示と同時に状態をリセットリアクティブではないデータの反映
ベストプラクティス

意図的にコンポーネントの状態をリセットしたい場合、最もシンプルで効率的なのは key 属性の変更です。

本記事で記述している通り、v-if の切り替えでもコンポーネントを再マウントすることは可能です。
しかし v-if は DOM からコンポーネントを完全に削除するため、再生成のタイミング制御に $nextTick などの非同期処理が必要になるケースがあります。

また、$forceUpdate() については、Vue公式ドキュメントにあるように、リアクティブなデータは本来自動で再描画されるため、Vue3ではこれが必要になることはほとんどありません。

一方で key は Vue の仮想DOMの仕組みに任せるだけで動作するため、余計な制御を加えることなく意図通りに状態をリセットできる効率的で分かりやすい方法です。
そのため、「なぜ更新されないのか?」と悩んだら、まずは key を試すことをおすすめします。

最後に

今回の調査と試行錯誤を通して、Vueでの再描画の仕組みについて理解が深まりました。
特に、「ログ上では値が更新されているのにUIは変わらない」という現象を経験したことで、データの流れやリアクティブの仕組みを意識する大切さを改めて学ぶことができました。

そして、今回得た内容はフォームのリセットだけでなく、非同期処理を伴う開発や、APIレスポンスのタイミングによる表示のずれなどで幅広く応用できる知識になりました。
今後は、「リアクティブの仕組みを理解した上でどう再描画させるか」を意識しながら開発を進めていきたいと思います。

私自身、初心者として再描画に関して迷うことがあったので、この情報が同じように悩んでいる方々の参考になれば幸いです。

KYOSOでは、こうした実務で培った知見をもとに、フロントエンド技術記事を継続発信しています。

参考

ビルトインの特別な属性(key)

条件付きレンダリング(v-if)

コンポーネントインスタンス($forceUpdate())

投稿者プロフィール

山田 翔太
山田 翔太
2024年新卒入社。
現在はJavaやvue.js、AWSを中心にWebアプリの開発に取り組んでいます。
趣味はスポーツ観戦で、野球やサッカー、バスケットボールなど名古屋のチームを応援しています。
業務で学んだことや疑問に感じた点を記事として発信し、共有していきたいと思います。
この記事をSNSでシェア!