【Vue3.5】watchの使い方を理解する


# 本記事の目的
本記事では、Vue.jsの`watch`の使い方についてまとめようと思います。

![giphy.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2356280/8393b987-ea98-caeb-36bf-076a3d7dbcdd.gif)


# 基礎
`watch`は簡単にいうと「コレが更新されたら、こういうことしてね」というヤツです。監視対象の値が更新された場合に特定のイベントを発火してくれます。


- 第一引数：コレが更新されたら（監視対象）
- 第二引数：こういうことしてね（コールバック）
- 第三引数：オプション（任意）

```vue
<script setup>
import { ref, watch } from "vue";

const name = ref("");

watch(name, () => {
  console.log("nameが更新されました");
});
</script>
```
# 更新前・更新後の値を参照する

コールバックの第一引数には変更後の値、第二引数には変更前の値が渡されます。
```vue
<script setup>
import { ref, watch } from "vue";

const name = ref("");

watch(name, (newVal, oldVal) => {
  console.log(`nameが${oldVal}から${newVal}に更新されました`);
});
</script>
```

# 複数の値を監視する
`watch`関数の第一引数を配列にすることで、監視対象を複数指定することができます。配列内の値のどれか一つでも変更されればコールバックが実行されます。
```vue
<script setup>
import { ref, watch } from "vue";

const firstName = ref("");
const lastName = ref("");

watch([firstName, lastName], () => {
  console.log("firstName または lastName が更新されました");
});
</script>
```

### それぞれの更新前・更新後の値を参照する
複数の監視対象の、それぞれの更新前後の値を参照する際にも、同様に配列となります。

コールバックの第一引数に**更新後の値**が配列で渡され、第二引数に**更新前の値**が配列で渡されます。それぞれ監視対象で指定している順番で渡ってきます。
```vue
<script setup>
import { ref, watch } from "vue";

const firstName = ref("");
const lastName = ref("");

watch(
  [firstName, lastName],
  ([newFirstName, newLastName], [oldFirstName, oldLastName]) => {
    console.log(`firstNameが${oldFirstName}から${newFirstName}に更新されました`);
    console.log(`lastNameが${oldLastName}から${newLastName}に更新されました`);
  }
);
</script>
```

# オブジェクト・配列を監視対象にする
オブジェクト・配列を監視対象に指定する際には少し気を付ける必要があります。

以下はオブジェクトを監視対象にしているのですが、以下の実装ではオブジェクトのプロパティに更新があった場合にもコールバックは実行されません。監視対象が配列の場合でも同様です。
```vue:うまくいかない例
<template>
  <input v-model="user.name" />
</template>

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

const user = ref({ name: "", age: 25 });

watch(user, () => {
  console.log(`ユーザー情報が変更されました`);
});
</script>
```
### 方法（1） オプション`deep`を有効にする
第三引数のオプションで`deep: true`を指定すると、監視対象のオブジェクト・配列内のプロパティや配列要素の変更も検知できるようになります。

```vue
<script setup>
import { ref, watch } from "vue";

const user = ref({ name: "", age: 25 });

watch(user, () => {
  console.log(`ユーザー情報が変更されました`);
}, { deep: true });
</script>
```

### 方法（2） 監視対象のプロパティを個別に指定する
オブジェクトのプロパティを監視対象として個別に指定する方法もあります。

プロパティを指定する場合には、監視対象をgetter関数にしてください。これにより、getter関数の実行結果として返された値が監視対象として評価されるようになります。
```vue
<script setup>
import { ref, watch } from "vue";

const user = ref({ name: "", age: 25 });

watch(() => user.value.name, () => {
  console.log(`ユーザー情報が変更されました`);
});
</script>
```


# onWatcherCleanup

バージョン3.5からリリースされた`onWatcherCleanup`を使用することで、クリーンアップイベントを指定することができるようになりました。

クリーンアップイベントとは、「特定のリソース（タイマーやイベントリスナーなど）が不要となった際に実行される処理」のことです。

ウォッチャーで指定されたコールバックが破棄される（次に同じコールバックが呼ばれた）タイミングで、`onWatcherCleanup` で指定されたクリーンアップイベントが実行されます。


```vue
<script setup>
import { onWatcherCleanup, ref, watch } from "vue";

const name = ref("");

watch(name, (newVal, oldVal) => {
  console.log(`nameが${oldVal}から${newVal}に更新されました`);

  onWatcherCleanup(() => {
    console.log(`nameが${oldVal}から${newVal}に更新された時のクリーンアップ`);
  });
});
</script>
```

### ユースケース

以下はフォームの入力内容に応じてAPIをコールして、その結果を一覧表示しています。

入力内容が更新されて新たなFetchリクエストを送信した場合、それ以前のリクエストは不要となるので破棄する必要があります。

そこで`onWatcherCleanup`を利用することで、上記の処理をクリーンアップイベントとして実行することができるようになります。

```vue
<template>
  <div>
    <input type="text" placeholder="キーワード検索" v-model="query" />
    <p v-for="product in products" :key="product.id">{{ product.title }}</p>
  </div>
</template>

<script setup>
import { ref, watch, onWatcherCleanup } from "vue";

const query = ref("");
const products = ref([]);

watch(query, async () => {
  const controller = new AbortController();

  // クリーンアップイベント
  onWatcherCleanup(() => {
    controller.abort();
  });

  const searchParams = new URLSearchParams({
    q: query.value,
  });

  try {
    const res = await fetch(
      `https://dummyjson.com/products/search?${searchParams}`,
      {
        signal: controller.signal,
      }
    );

    if (!res.ok) {
      throw new Error(res.error);
    }

    const data = await res.json();
    products.value = data.products;
  } catch (error) {
    if (error instanceof Error && error.name !== "AbortError") {
      console.error(error);
      products.value = [];
    }
  }
});
</script>
```


# オプション
### immediate
ウォッチャーで指定したコールバックは初期レンダリング時には実行されませんが、`immediate` を指定することで**初期レンダリング時にも実行されるようになります**。
```vue
<script setup>
import { ref, watch } from "vue";

const name = ref("");

watch(
  name,
  () => {
    console.log("nameが更新されました");
  },
  { immediate: true }
);
</script>
```

### once
通常、監視対象が更新される度にコールバックは実行されますが、onceオプションを指定することで**最初の更新時のみ監視してそれ以降は監視を停止することができます**。

```vue
<script setup>
import { ref, watch } from "vue";

const name = ref("");

watch(
  name,
  () => {
    console.log("nameが更新されました");
  },
  { once: true }
);
</script>
```


### deep

ウォッチャーはデフォルトでは監視対象を浅く監視するため、オブジェクトや配列の内部プロパティの変更を検知しません。しかし、`deep`オプションを有効にすることで、監視対象を再帰的に監視することが可能です。

:::note warn
`deep`オプションはオブジェクト全体を再帰的にトラバースするため、特に大規模なデータ構造を監視する際にパフォーマンスが低下する可能性があります。可能であれば、関数を使って特定のプロパティを個別に監視することを推奨します。
:::

```vue
<script setup>
import { ref, watch } from "vue";

const name = ref("");

watch(
  name,
  () => {
    console.log("nameが更新されました");
  },
  { once: true }
);
</script>
```



### flush
バージョン3.5にて新たに追加されたオプションで、**コールバックの実行タイミングを制御することができます**。

`flush`には、`pre` `sync` `post` のいずれかを指定することができ、それぞれ指定する値によってコールバックの発火タイミングが異なります。

### pre
デフォルト値。リアクティブデータの更新によって、**DOMが更新される前**（`onBeforeUpdate`の前）に実行されます。

### sync
リアクティブデータの更新によって、**DOMが更新される前**（`onBeforeUpdate`の前）に、**同期的**に実行されます。


### post
リアクティブデータの更新によって、**DOMが更新された後**（`onBeforeUpdate`の後、`onUpdated`の前）に実行されます。コールバック内で更新が反映されたDOMにアクセスしたい場合にはこちらを指定しましょう。


```vue
<script setup>
import { ref, watch } from "vue";

const name = ref("");

watch(
  name,
  () => {
    console.log("nameが更新されました");
  },
  { flush: "sync" } // "pre" または "sync" または "post"
);
</script>
```

> [こちらの記事](https://qiita.com/whopper1962/items/1a99169ba868f94e4c94#beforeupdate)では「onBeforeUpdate」「onUpdate」等のライフサイクルフックについて解説しておりますので併せてご覧ください

# watchEffectとの違い
Vue.jsには`watch`と似た機能として`watchEffect`という機能が提供されています。

どちらも「監視対象が更新されたら指定されたコールバックを実行する」という役割を持ちますが、`watchEffect`は`watch`と比較して以下のような特徴があります。

- コールバック内で使用されているリアクティブな値を自動で監視対象とする
- 初回レンダリング時に必ず実行される

`watch`は、コールバック内で使用しているリアクティブな値が更新されたとしても、監視対象として指定されていない場合にはコールバックは実行されません。

対して`watchEffect`は、コールバック内で使用されているリアクティブな値は自動的に監視対象とするため、以下ではnameかageが更新されるとコールバックが実行されます。

```vue
<script setup>
import { ref, watch, watchEffect } from "vue";

const name = ref("");
const age = ref(25);

watch(name, () => {
  // nameのみ監視しているため、ageが更新されたとしてもイベントは発火しない
  console.log(`watch: nameが${name.value}に更新され、ageが${age.value}に更新されました。`);
});

watchEffect(() => {
  // nameとageを使用しているので、nameかageが更新されたらイベントが発火する
  console.log(`watchEffect: nameが${name.value}に更新され、ageが${age.value}に更新されました。`);
});
</script>
```