🚴‍♂️

VueのDOM操作で嵌ったパターンをまとめていく

2023/10/12に公開

概要

自分がVueを使って開発をしている時、DOM操作で嵌ったパターンをまとめてみました。
※ 随時更新予定です!(嵌ったら)

対象の読者

・基礎的なjs/vueの知識がある人
・VueのDOM操作がよくわからない人

v-ifとv-show

公式ガイドにも載っていますが、v-ifとv-showの違いは「DOM」が残るか残らないかです。
v-ifを「false」に変更した時、レンダリングされないためDOMは残りませんが
v-showを「false」に変更した時はDOMが残ります。
https://ja.vuejs.org/guide/essentials/conditional.html

下記のソースコードを例に見てみます。

test.vue
<script setup lang="ts">
// values
const isIf: Ref<Boolean> = ref(true);
const isShow: Ref<Boolean> = ref(true);

// methods
/**
 * 2つの要素にバインドしているBooleanの値を全てfalseにし、見えなくする
 */
function invisibleAllElements() {
  isIf.value = false;
  isShow.value = false;
}

/**
 * 要素が存在するか確認する
 */
function checkExistsElements() {
  const elIf: HTMLElement | null = document.getElementById("if");
  console.log("v-if Element -> " + elIf);

  const elShow: HTMLElement | null = document.getElementById("show");
  console.log("v-show Element -> " + elShow);
}
</script>

<template>
  <p v-if="isIf" id="if">v-if</p>
  <p v-show="isShow" id="show">v-show</p>

  <button @click="invisibleAllElements()">要素を消します</button>
  <button @click="checkExistsElements()">要素を確認</button>
</template>

こちらは要素を消す前の状態です。

test.vue
<p id="if" data-v-inspector="pages/test.vue:28:3">v-if</p>
<p style="display: none;" id="show" data-v-inspector="pages/test.vue:29:3">v-show</p>

「要素を消します」ボタンを押すとv-ifの要素は残っていない事が分かります。

test.vue
<!--v-if-->
<p style="display: none;" id="show" data-v-inspector="pages/test.vue:29:3">v-show</p>

要素が消える落とし穴

ここまではなんとなく分かったと思いますが、一つ落とし穴があります。
要素を消した後に「確認」ボタンを押下すると・・・

v-if Element -> null
test.vue:23 v-show Element -> [object HTMLParagraphElement]

v-ifの要素は取得できませんね。
当然ですがレンダリングされていないからです。

これは私が実務で遭遇したパターンです。

とある処理が正常に動作していないので調査した結果、
v-ifで消えたDOM要素を参照しようとしてエラーが発生していたというものがありました。

公式ドキュメントにも記載されている通り「頻繁にレンダリングするかしないか」は重要ですが
こういったパターンも存在していることは頭の片隅で覚えておきましょう。

同じ動きに見えるのに?

上記のソースコードに少し改良を加えてみました。

test.vue
<script setup lang="ts">
// values
const isIf: Ref<Boolean> = ref(true);
const isShow: Ref<Boolean> = ref(true);

// methods
/**
 * 2つの要素にバインドしているBooleanの値を全てfalseにし、見えなくする
 */
function invisibleAllElements() {
  isIf.value = false;
  isShow.value = false;
}

/**
 * 要素が存在するか確認する
 */
function checkExistsElements() {
  // 要素を見えなくする
  invisibleAllElements();

  const elIf: HTMLElement | null = document.getElementById("if");
  console.log("v-if Element -> " + elIf);

  const elShow: HTMLElement | null = document.getElementById("show");
  console.log("v-show Element -> " + elShow);
}

<template>
  <p v-if="isIf" id="if">v-if</p>
  <p v-show="isShow" id="show">v-show</p>

  <button @click="checkExistsElements()">要素を消しながら確認</button>
</template>
</script>

変更点は下記の2つになります。

それではボタンを押して確認します。一見同じ動きに見えますが・・・

v-if Element -> [object HTMLParagraphElement]
v-show Element -> [object HTMLParagraphElement]

v-ifの要素が存在していますね!!

どうして処理をまとめると動きが変わるかは、
次章の「DOM要素が見つかりません?」で説明しています。

DOM要素が見つかりません?

上記の章でリアクティブな値を更新したのに、即時反映されていないパターンが見つかりました。
こういったパターンを解決するには、「nextTick」が有効です。

これは公式ドキュメントから抜粋したnextTickの説明になります。

Vue でリアクティブな状態を変更したとき、その結果の DOM 更新は同期的に適用されません。
その代わり、Vue は「次の tick」まで更新をバッファリングし、
どれだけ状態を変更しても各コンポーネントの更新が一度だけであることを保証します。

状態を変更した直後に nextTick() を使用すると、DOM 更新が完了するのを待つことができます。
引数としてコールバックを渡すか、戻り値の Promise を使用できます。

https://ja.vuejs.org/api/general.html#version

ポイントは 「その結果の DOM 更新は同期的に適用されません。

つまり、リアクティブな値の状態を更新しても、DOMには即時反映されないということになります。

どうしてそうなるか?については「仮想DOM」を知る必要がありますが、ここでは割愛します。
(そもそも筆者も理解しきれていません・・・学習します)
https://ja.vuejs.org/guide/extras/rendering-mechanism.html

使い方は、下記のように改良すればオッケーです。

nextTick.vue
async function checkExistsElements() {
  // DOMの更新を待つ
  await nextTick(() => {
    invisibleAllElements();
  });
  
    const elIf: HTMLElement | null = document.getElementById("if");
  console.log("v-if Element -> " + elIf);

  const elShow: HTMLElement | null = document.getElementById("show");
  console.log("v-show Element -> " + elShow);
}

このパターンは非同期処理やレンダリングの仕組みを理解しないと少し難しいかもしれませんが、
DOM更新を即時反映させたい場合は、nextTickを使うことを覚えておきましょう。

ライフサイクルフック

Vueにはライフサイクルフックと呼ばれる一連の処理の流れがあり、
その処理にイベントを登録することができます。

「Vueには」と書きましたが、他のフレームワークにも似たような機構は備わってると思います。

さて、下記のように書いてみたところエラーが発生しました。
これには2つの原因があります。
https://ja.vuejs.org/guide/essentials/lifecycle.html

lifeCyclehooks.vue
<script setup lang="ts">
document.getElementById("hello");
</script>

<template>
  <p id="hello">Hello Vue!!</p>
</template>

document is not defined

こちらはSSR(サーバーサイドレンダリング)が原因になります。
documentというのはブラウザにしか存在しないグローバルオブジェクトのため、
サーバー側で処理をするとエラーが発生します。
https://developer.mozilla.org/ja/docs/Glossary/Global_object

DOM生成のタイミング

こちらはライフサイクルフックが大きく関わってきます。
DOMが生成されるのは「mounted」以降になります。

しかし、compositonsAPIでそのまま書くとタイミングは「created」になります。
生成されていないDOMを参照しようとしても取得できないため「null」になります。

これらを加味すると、マウント後のタイミングでDOM操作することが最適解と言えそうです。

lifeCycleHooks.vue
<script setup lang="ts">
// マウント後に処理を行うように変更
onMounted(() => {
  document.getElementById("hello");
});
</script>

<template>
  <p id="hello">Hello Vue!!</p>
</template>

少し早足になりましたが、ライフサイクルフックは基本ではありますが重要な処理です。
(とはいえ僕は、大体3種類ほどしか使ったことがありません笑)

まとめ

以上が今まで僕が出会ったDOM要素で嵌ったパターンになります。

実はVue.jsの日本語ドキュメントは素晴らしく正直書くことはあまりなかったのですが、
html/jsの知識が足りず嵌ったところがあったので
同じ悩みを抱える人が出てくる前に共有できたらいいなと思い書きました。

また、私はこんなところで嵌ったよ等がありましたらぜひ教えてください!
(僕のためになりますので笑)

でわでわ~。

Discussion