😇

ぼくはVue3とNuxt3がまだまだわからない

2023/04/30に公開

概要

  • 最近Nuxt3を扱ったプロジェクトを担当しています
  • ただただただただハマった話をするだけ
  • 答えはこれから探す

watchwatchEffectについて

実装を進めてたらprops、provide/injectで受け取った値をwatchで検知できない場合がありました。

  • そのComponentはwatchEffectで実装されていた
  • watchEffectはComponentのリアクティブ変数を対象として変更時にトリガーされる
  • 複数リアクティブ変数があったが、propsされた変数ひとつだけ変更をwatchしたいだけだったので、watchEffect → watch へ変更した→検知されなくなった

そもそもwatchwatchEffectの違いとは・・・・?ってなって調べたのがこちら

https://ja.vuejs.org/guide/essentials/watchers.html
https://ja.vuejs.org/api/reactivity-core.html#watcheffect

なるほど(わからん)

https://qiita.com/doz13189/items/d09cfc6e1ff38621c2cc

なるほど

  • 監視対象の定義の仕方
  • watch は変更前と変更後の値が取得できる
  • 初回実行のタイミング

なぜ僕のコードは検知されないのかと頭を悩ませながら、細かくprintデバッグしていましたが、動かし続けてみるとあることがわかりました。

  • 親Componentでwatchを動かしている子Componentをラップしてv-ifを利用している
  • v-ifは子Componentに値を渡したいときにtrueになる
  • 一度目の表示時にはwatchは検知されないが、trueの状態で、再度propsを渡してあげるとwatchが動く
  • でもComponentがレンダリングされたときはpropsに値は渡っている

コードも載せていないのでなんのこっちゃみたいな話だと思いますが、左ナビゲーションにブログ記事の一覧があって、1つclickすると右コンテンツ表示域にブログが表示されるようなUIを思い浮かべてください。

  • 左メニューにブログ一覧が表示されている
  • 表示させたい記事をclickする → 1度目は表示されない
  • 表示されていないけど他記事をclickする → 2度目以降は表示される

つまりこのトリビアの種、こういうことになります。

props更新時、動かないのは1回目

ありがとうございました。

本題に戻すと、1回目動かない原因は特定することができました。

  • v-iffalse → trueになるとレンダリングされる(初期化)
  • 初期化時にwatchを定義するが、定義時は(当たり前だが)動かない

というわけでこうしてあげましょう。

  watch(props.hoge, () => { // 変更を検知したい値
      // 何らかの処理
  }, { immediate: true }) // <- こいつ

もちろんドキュメントにもありますが、watchが作成時に即時実行してくれます。これでできました。やったね。

propsemitsはComponentの機能です

Component外で定義するとComponentで定義しろ!的なワーニングが表示されます。

ので、Composablesで使いたい場合は渡してあげましょう。

※この使い方はドキュメントに載っているものではなかったので、間違ってたらマサカリ求む

const props = defineProps<{
  category: string
}>()
const emits = defineEmits<{(event: 'selectCategory', categories: OperationCategories): void }>()
useHello(props, emits)

Composablesを呼ぶ度、別のインスタンス(?)ができる

当たり前なのかも知れませんが、僕はずっと共通の何かができると信じいてた(?)

なんのこっちゃという話と思いますが、無知なaipaは 例えば下記例のようにuseHogeHoge()で呼んできたリアクティブな値などは全部同じインスタンスになると思っていました。(なぜそう思ったんでしょうか。もう僕も思い出せない)

// useHelloは hello というreactive変数を返すcomposables
const { hello } = useHello()
const { hello: hello2 } = useHello()
const { hello: hello3 } = useHello()

// 'hello! hello! hello!'
console.log(hello, hello2, hello3) // それぞれ別々のreactive変数
hello.value = 'bye!'

// 最初に用意した変数だけ変更されて、あとはかわらない
// 'bye! hello! hello!'
console.log(hello, hello2, hello3)

provide/injectとComposables

バケツリレーに疲れましたか?僕はそうです。

親子ぐらいだったら大丈夫なんですが、殆どのプロジェクトが親子どころか玄孫ぐらいまででてくるので、page componentをrootとして、最下層のcomponentからデータをやり取りするのは辛い(いわゆるバケツリレー)

provide/inject + composablesを利用すればある程度は軽減できそう

https://ja.vuejs.org/guide/components/provide-inject.html

フロントエンド界隈(?)だとprovider パターンと呼ばれているらしい
https://zenn.dev/morinokami/books/learning-patterns-1/viewer/provider-pattern

Vuexみたいなstoreと何が違うの???って思うかもですが、たぶん似ている?

https://qiita.com/karamage/items/4bc90f637487d3fcecf0

ただ、Vuexだと規律(?)があるが、Composablesだと自由に定義できる印象をもっています。逆に規律がないので、自由すぎてあとから大変ってなるかも知れない。

また、Nuxt3にはuseStateなるものがあります。余計混乱した。ただ、これはドキュメントを見る限り、SSRでサーバサイドでstoreしたデータをclientで共有できるようなメリットがあるが、clientサイドだけであればprovide/injectで事足りそうな印象を持っています。

データが複数のComponentで利用される、または経由するComponentが多いけど経由しているComponentではそのデータはいらない場合は、provide/injectを採用しても良いとするとかが良さそうだなって思いました。

どこからprovideするのか

rootにある、app.vue からが安全ですが、ごちゃごちゃ書くとfatなroot componentになるので、pluginでやりましょう。

~/plugins/globalStore.ts

import SearchKey from '~/composables/useSearchKey'

export default defineNuxtPlugin(() => {
  return {
    provide: {
      settingProvideStores: () => {
        provide(SearchKey, useSearch())
      }
    }
  }
})

~/app.vue

<template>
  <NuxtLayout>
    <NuxtPage />
  </NuxtLayout>
</template>

<script lang="ts" setup>
const { $settingProvideStores } = useNuxtApp()
$settingProvideStores()
</script>

...と思っていたんですが、middlewareでprovide/injectを使いたい場合、app.vueより先にmiddlewareが呼ばれるので、いまいちな気がしています。そもそもmiddlewareでは、provide/injectを使わなくてもいいよって話かも知れませんが。

じゃあどうしたかって?
middlewareでClassを用意して、あとからcallback的な感じで呼んで、中身を書き換えるような実装で回避しました。

例えば、ログイン後にtokenを受け取って、外部サービスとやり取りしたい場合、ログイン前はtoken持っていないからPluginがうまく動かないので、Classをpluginでprovideして、tokenがもらえていないなら、ログインページに遷移して、tokenが利用できるのならそのままやりたいことするって感じに組みました。これも正解かどうかわからない。

import { OutsideService } from '外部サービス'
import { useRuntimeConfig } from '#imports'

class Search {
  private _client: any = undefined
  private _apiToken = ''
  private localStorageKey = 'searchApiKey'

  // ログイン前にも呼ばれるけど、apikeyがないので
  // ログインページへリダイレクトする
  get client () {
    if (this._client !== undefined) {
      return this._client
    }

    const apiToken = localStorage.getItem(this.localStorageKey)
    if (apiToken === undefined || apiToken === null || apiToken === '') {
      console.error('ワーニングを表示')
      return navigateTo({
        path: '/'
      })
    }

    this.apiToken = apiToken
    this.init()
    return this._client
  }

  set apiToken (apiToken: string) {
    localStorage.setItem(this.localStorageKey, apiToken)
    this._apiToken = apiToken
  }

  public readonly init = () => {
    const config = useRuntimeConfig()
    this._client = new OutsideService({
      apiKey: this._apiToken
    })
  }
}

export default defineNuxtPlugin(() => {
  return {
    provide: {
      outsideSearch: new Search()
    }
  }
})

これで別途利用する際にapiTokenがまだ取得していない場合は、/へリダイレクトされます。またログイン時にはapiTokenをset後、外部サービスとのclientオブジェクトを生成することができます。

const { $outsideSearch } = useNuxtApp()
$outsideSearch.apiToken = response.data.apiToken
$outsideSearch.init()

なぜか知らないけど、コードの設計とか組み方の本を読みたくなりました。

navigateToがよくわからない

これはまじでよくわかっていない

  • navigateTo単体だけだとうまく動かないっぽい
  • リダイレクトで混乱した
    • よくある要件で、「ログインしていない場合、root pathへリダイレクトしてほしい」というやつを実装しようとする
    • 別の認証サービスが認証済みだったらuserの変数をうめて返してくれる。なので、user === null && route.path !== '/' みたいな条件を用意して、もしtrueであれば、 return navigateTo('/') を実行するようなコードを書く
    • 例えば、URLが/controllerの状態でnavigateTo('/')を叩くと、リダイレクトループが発生する
      • 原因はroute.pathがかわらないから
      • ちなみにnavigateToを使っていると、Infinite redirect in navigation guardでリダイレクトループをカウントして途中でabortしてくれる。優秀
    • どうやったらroute.pathが変わるのか
      • return navigateTo('/', { replace: true }) → だめ
      • return await navigateTo('/') → だめ
      • return navigateTo('/', { external: true }) → だめ
      • return navigateTo('http://localhost:3000/', { external: true }) → OK
        • なぜ??????????????

実装も確認しているがよくわからない。

我々はこの謎を解き明かすべく、アマゾンの奥地へと向かった!!!!!!

雑感想

以上です。プロジェクトは引き続き続くので、謎を解き明かすのと、もっとVueやNuxtと友達になろうと思います。

進捗がありましたらまた投稿いたします。

(というかこれ会社の技術記事として初投稿なのか。いいのか・・・?)

CBcloud Tech Blog

Discussion