12
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

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

Last updated at Posted at 2023-05-25

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

前提

ディレクトリ・ファイル構成
/DocRoot
 |
 +- components
 |   |
 |   +- RootComponent.vue
 |   |
 |   +- NestedComponent.vue
 |   |
 |   +- (And others. many nested components...)
 |
 +- stores
     |
     + setting.js
     |
     + sample.js
setting.js
import { defineStore } from 'pinia'

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

  actions: {
    async initialize() {
      this.data = await fetch('https://example.com/api/setting')
    }
  }
}
sample.js
import { defineStore } from 'pinia'

export default defineStore('sample', {
  state: () => ({
    data: {}, // settingストアから取得した値を設定
  }),
}
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>
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ストアがどこで初期化しているのか分かりづらくなると考え却下しました。

試したこと・その1

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

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

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

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
    }
  }
}
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>

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

試したこと・その2

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

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に設定していたため)

試したこと・その3

fetchPromiseを返すことを利用して「その1」に近い発想で判定することを思いつき試しました。

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')
    }
  }
}
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)を主戦場としていると処理が上から順番に流れていくのが
当たり前になってしまっていて、この辺りは今後フロントエンドに携わるときにしっかり意識しないと
ということを実感しました。

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

12
2
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
12
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?