【Vue】ライフサイクルと非同期で取得したpropsの更新タイミング

親コンポーネントで非同期に取得したデータを`props`として子に渡したところ、
- 初期値が一瞬チラつく
- `props`の値が更新されているのに画面に反映されない

という現象に遭遇しました。


この記事ではライフサイクルや`props`が渡されるタイミング・変更検知の方法について学習した内容をまとめています。

## ✅ ライフサイクルとは
Vueでは、インスタンスが生成されてから破棄されるまでの一連の流れを『ライフサイクル』と呼びます。

### 主な流れ
1. Vueインスタンスの生成
2. DOMへのマウント
3. 画面の更新・リアクティブデータの変更
4. Vueインスタンスの破棄

## ✅ ライフサイクルフックとは
ライフサイクルの各タイミングで『ライフサイクルフック』と呼ばれる関数が実行されます。
主要なライフサイクルフックが呼ばれるタイミングをまとめました。

| タイミング | OptionsAPI | CompositionAPI |
|---|---|---|
|インスタンス生成前(※)|beforeCreated|なし|
|インスタンス生成後|created|なし|`data`・`method`・`props`にアクセスできる|
|DOMマウント前|beforeMount|onBeforeMount|
|DOMマウント後|mounted|onMounted|DOM操作ができるようになる|
|画面の更新前|beforeUpdate|onBeforeUpdate|
|画面の更新後|updated|onUpdated|
|インスタンス削除前|beforeDestroy(Vue2)<br>beforeUnmount(Vue3)|onBeforeUnmount|
|インスタンス削除後|destroyed(Vue2)<br>unmounted(Vue3)|onUnmounted|
|エラー時|errorCaptured|onErrorCaptured|

> (※)インスタンス生成前と表現されることがありますが、厳密には**インスタンス生成後、リアクティブデータの初期化前**

### 💡 補足：よく使うフックの補足
**`created`**
- インスタンスの生成後、リアクティブデータが初期化された後に実行される
- `data`・`props`・`method`にアクセスできる
- DOM操作はまだできない
- CompositionAPIでは`<script setup>` `setup()`に統合された

**`mounted` / `onMounted`**
- DOMのマウントが完了した後に実行される
- DOM操作が可能になる

**`beforeUpdated` / `onBeforeUpdated`**
- リアクティブデータが更新され、画面が再描画される前に実行される
- データ更新のたびに発火するので、propsの変更検知にも使える


## ✅ 非同期に取得したpropsの扱いについて
まず、親子のライフサイクルは以下の順番で行われます。
1. 親のインスタンス生成 → `props`の初期値がセットされる & `created`の発火
2. 親のテンプレート解析 → 子コンポーネントの存在を検出
3. 子のインスタンス生成
4. 子のテンプレート解析 → この時点での`props`の値をもとに描画される
5. 子のDOMをマウント
6. 親のDOMをマウント

### 💡なぜpropsの初期値の状態で子コンポーネント描画されるのか
子コンポーネントのテンプレート解析は、その時点での`props`の値をもとに行われます。

そのため、親の`created`で非同期に`props`の値を取得した場合、 **`props`の値の更新が子のテンプレート解析に間に合わず、初期値のまま描画されてしまうという状況が発生します。** 

また、その後 props の値が更新されて再描画されたとしても、一度初期値で描画された状態がチラついて見えることがあるため、その点を考慮して実装する必要があります。

### 💡なぜpropsの変更が反映されないのか
子コンポーネントで`props`をテンプレート内で直接使っている場合は、`props`の変更は自動的に検知され、画面に反映されます。

しかし、子コンポーネントで定義したdataにpropsの値を使用している場合は、変更を検知して再計算してくれません。
そのため、`watch`などで変更を検知して再計算する必要があります。

## ✅ Propsの変更を検知して、再計算する方法
propsの変更を検知・再計算し画面に反映するためには、主に3つの方法があります。

### ①`watch`でpropsを監視し、明示的に処理を発火させる
`watch`は、監視した値が更新されるたびに、指定した処理を実行する仕組みです。

値の変更に応じて、非同期処理や副作用のある処理を行いたい場合に便利です。`props`の変更に応じて処理を走らせたい場合や、値の前後を比較して分岐処理をしたい場合にも柔軟に対応できます。

ただし、常に監視が走るため、処理が重たい場合や変更頻度が高い場合はパフォーマンスへの影響に注意する必要があります。

```vue:子コンポーネント
<template>
  <p>{{ message }}</p>
</template>

<script>
export default {
  props: {
    userId: Number
  },
  data() {
    return {
      message: ''
    };
  },
  watch: {
    userId: {
      async handler(newVal) {
        try {
          const res = await fetch(`https://api.〇〇/${newVal}`);
          const data = await res.json();
          this.message = `こんにちは、${data.name}さん！`;
        } catch (e) {
          this.message = 'ユーザー情報の取得に失敗しました。';
        }
      }
    }
  }
};
</script>
```

### ②`computed`でprops依存のリアクティブ値の変更を検知する
`computed`は、依存している`props`や`data`の値が変わったときだけ、自動で再計算される仕組みです。

DOMの操作やAPIを叩くような副作用のある処理は含めないようにしましょう。
vueが再計算するタイミングを管理しているので、意図しないタイミングで副作用が起きてしまう可能性があるからです。

そのため、`computed`はあくまで表示用の値を作るために使うのが基本です。

```vue:子コンポーネント
<template>
  <p>{{ message }}</p>
</template>

<script>
export default {
  props: {
    userName: String
  },
  computed: {
    message() {
      return `こんにちは、${this.userName}さん！`;
    }
  }
};
</script>

```

### ③ ライフサイクルを利用してPropsの変更を検知する

`beforeUpdate`・`onBeforeUpdate`は画面が再描画される直前で呼ばれるライフサイクルフックです。
このタイミングでpropsを使って再計算処理を行えば、結果的に「propsの変更を検知して再計算」しているのと同じことになります。

状態が変更されて、画面が再描画される時に再計算をすれば十分な場合は、`watch`を使って常に`props`を監視するよりはパフォーマンスの向上が見込めます。
```vue:子コンポーネント
<template>
  <p>残りの予算: {{ remaining }} 円</p>
</template>

<script>
export default {
  props: {
    budget: Number,   //予算
    expenses: Number  //支出
  },
  data() {
    return {
      remaining: 0
    };
  },
  mounted() {
    this.updateRemaining();
  },
  beforeUpdate() {
    this.updateRemaining();
  },
  methods: {
    updateRemaining() {
      this.remaining = this.budget - this.expenses;
    }
  }
};
</script>
```

# まとめ
Vueで非同期に取得したデータをpropsとして渡す場合、ライフサイクルの影響で「初期値が一瞬表示されてしまう」「propsが更新されても画面に反映されない」といった問題に直面することがあります。

このような場合、以下のポイントを押さえておくことが重要です：
- 子コンポーネントの描画はその時点での`props`の値をもとに行われるため、親での非同期処理が完了していなければ初期値が描画されてしまう
- propsをテンプレート内で直接使用している場合は自動的に検知され、画面に描画されるが、子コンポーネント内で`props`をもとに定義した変数は再計算されず、以下のような方法で変更を検知して再計算する必要がある
    - `watch`で`props`を監視し、明示的に処理を発火させる
    - `computed`で`props`依存のリアクティブ値の変更を検知する
    - `beforeUpdate`/`onBeforeUpdate` で再計算の処理を走らせる（※但し、props以外の値の更新タイミングで問題ない場合に限ります）

このようなprops周りの特性を理解しておくことで、Vueのコンポーネントを作る際に役立つのではないでしょうか。

**参考**
[【Vue.js】props で渡された値の変更を検知したい](https://zenn.dev/lovegraph/articles/4b84c9e7e4c73c)
[Vueのライフサイクルフックまとめ](https://techblog.roxx.co.jp/entry/2019/02/27/120000)
[Prop passed to child component is undefined in created method](https://stackoverflow.com/questions/54974846/prop-passed-to-child-component-is-undefined-in-created-method?utm_source=chatgpt.com)