Vue3+Pinia環境でストア（Pinia）のデータ初期化とVueのライフサイクルが噛み合わなかった話

タイトルの通り、Vue3+Pinia環境でストア（Pinia）のデータ初期化と
Vueのライフサイクルが噛み合わなかった話をしようと思います。

# 前提
```:ディレクトリ・ファイル構成
/DocRoot
 |
 +- components
 |   |
 |   +- RootComponent.vue
 |   |
 |   +- NestedComponent.vue
 |   |
 |   +- (And others. many nested components...)
 |
 +- stores
     |
     + setting.js
     |
     + sample.js
```

```javascript:setting.js
import { defineStore } from 'pinia'

export default defineStore('setting', {
  state: () => ({
    data: {},
  }),

  actions: {
    async initialize() {
      this.data = await fetch('https://example.com/api/setting')
    }
  }
}
```

```javascript:sample.js
import { defineStore } from 'pinia'

export default defineStore('sample', {
  state: () => ({
    data: {}, // settingストアから取得した値を設定
  }),
}
```

```vuejs:RootComponent.vue
<template></template>

<script>
import NestedComponent from './NestedComponent.vue'
import settingStore from '../../stores/setting'

export default {
  components: {
    NestedComponent
  },

  async created() {
    const setting = settingStore()
    setting.initialize()
  },
}
</script>
```

```vuejs:NestedComponent.vue
<template></template>

<script>
import settingStore from '../../stores/setting'
import sampleStore from '../../stores/sample'

export default {
  async beforeUpdate() {
    const setting = settingStore()
    const sample = sampleStore()

    /*
     * ここでsettingストアのデータが設定されることを期待しているが
     * 実際には空オブジェクト{}が設定される
     */
    sample.data = setting.data
  }
}
</script>
```

# 何に困ったのか？
前提に記載の通り、親コンポーネントでsettingストアを初期化したつもりになっていましたが
子コンポーネントでsettingストアを参照した時には初期化が完了していない状態でした。

Vueは子コンポーネントから順番に描画していくなど、
コンポーネント間の順序も関係しているとは思います。

また、それとは別にVueの処理とPiniaのストア処理は別々に動いているものと推測されます。

では、一番ネストが深いコンポーネントでsettingストアを初期化すればいいかとも考えたのですが
一番ネストが深いコンポーネントは`v-for`で複数描画しているため、
settingストアの初期化が複数回行われることになってしまいます。

初期化は1回にしたかったのでこの案は却下しました。

次に、settingストアの初期化を`NestedComponent`で行うことを検討してみましたが
サンプルには記述がありませんが、settingストアを`RootComponent`でも使用しているため
settingストアがどこで初期化しているのか分かりづらくなると考え却下しました。

# 試したこと・その１
Vueのライフサイクル（親コンポーネントの`created`後に
子コンポーネントの`beforeUpdate`が走ることを期待していました。

この流れでいけば子コンポーネントの`beforeUpdate`が実行されるタイミングでは
settingストアが初期化されているはず！と。

そこで、以下のようにsettingストアにフラグを追加して、
`NestedComponent`ではそれを判定して処理するように書き換えてみました。

```javascript:setting.js
import { defineStore } from 'pinia'

export default defineStore('setting', {
  state: () => ({
    data: {},
    isInitialized: false,
  }),

  actions: {
    async initialize() {
      this.data = await fetch('https://example.com/api/setting')
      this.isInitialized = true
    }
  }
}
```

```vuejs:NestedComponent.vue
<template></template>

<script>
import settingStore from '../../stores/setting'
import sampleStore from '../../stores/sample'

export default {
  async beforeUpdate() {
    const setting = settingStore()
    const sample = sampleStore()

    /*
     * ここでsettingストアのデータが設定されることを期待しているが
     * 実際には空オブジェクト{}が設定される
     */
    if (setting.isInitialized) {
      sample.data = setting.data
    }
  }
}
</script>
```

結果、うまくいきませんでした。
`NestedComponent`の`beforeUpdate`が実行されるタイミングでは
まだ初期化が完了しておらず、その後、`beforeUpdate`が実行されることもなく…

# 試したこと・その２
その１がダメだったので、いっそsettingストアで初期化を行なったタイミングで
sampleストアに値を設定してしまえ、と考えてみました。

```javascript:setting.js
import { defineStore } from 'pinia'
import sampleStore from './sample'

export default defineStore('setting', {
  state: () => ({
    data: {},
    isInitialized: false,
  }),

  actions: {
    async initialize() {
      this.data = await fetch('https://example.com/api/setting')

      const sample = sampleStore()
      sample.data = this.data
    }
  }
}
```

これは期待通りに動作してくれたのですが、settingストアでsampleストアに値を設定しているので
コードの見通しが悪くなってしまいました。（実際にはsettingの一部をsampleに設定していたため）

# 試したこと・その３
`fetch`が`Promise`を返すことを利用して「その１」に近い発想で判定することを思いつき試しました。

```javascript:setting.js
import { defineStore } from 'pinia'
import sampleStore from './sample'

export default defineStore('setting', {
  state: () => ({
    data: {},
    isInitialized: new Promise(() => {}),
  }),

  actions: {
    initialize() {
      this.isInitialized = fetch('https://example.com/api/setting')
    }
  }
}
```

```vuejs:NestedComponent.vue
<template></template>

<script>
import settingStore from '../../stores/setting'
import sampleStore from '../../stores/sample'

export default {
  async beforeUpdate() {
    const setting = settingStore()
    const sample = sampleStore()

    /*
     * thenで初期化完了後に値を設定している
     * awaitでここで一旦処理が完了するのを待つようにしている
     */
    await setting.isInitialized.then(
      sample.data = setting.data
    )
  }
}
</script>
```
結果、期待通りに動作してくれました！

もしかしたら`NestedComponent`に関しては`await`だけで上手くいくのかもしれませんが
文脈的に伝わりやすいと思ったので、今回はこの書き方にしてみました。

# 結論
考えてみると当たり前のことなのですが、VueのライフサイクルとPiniaの初期化は全く別な処理なので
今回の事例のように、期待したタイミングで値が入っていないかもしれない、
ということを意識しないといけない、とまた一つ勉強になりました。

最終的に`Promise`を用いたのはちょっとトリッキーかな、と思いつつも
非同期処理をうまく活用できるのはJavaScriptのメリットの一つなのかなと感じました。

普段はバックエンド（主にPHP）を主戦場としていると処理が上から順番に流れていくのが
当たり前になってしまっていて、この辺りは今後フロントエンドに携わるときにしっかり意識しないと
ということを実感しました。

同じようなことで困る人は少ないのかもしれませんが、誰かの参考になれば幸いです！
それではまたどこかで！