ぼくはVue3とNuxt3がまだまだわからない
概要
- 最近Nuxt3を扱ったプロジェクトを担当しています
- ただただただただハマった話をするだけ
- 答えはこれから探す
watch
とwatchEffect
について
実装を進めてたらprops、provide/injectで受け取った値をwatchで検知できない場合がありました。
- そのComponentは
watchEffect
で実装されていた - watchEffectはComponentのリアクティブ変数を対象として変更時にトリガーされる
- 複数リアクティブ変数があったが、propsされた変数ひとつだけ変更をwatchしたいだけだったので、
watchEffect
→watch
へ変更した→検知されなくなった
そもそもwatch
とwatchEffect
の違いとは・・・・?ってなって調べたのがこちら
なるほど(わからん)
なるほど
- 監視対象の定義の仕方
- 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-if
でfalse → true
になるとレンダリングされる(初期化) - 初期化時にwatchを定義するが、定義時は(当たり前だが)動かない
というわけでこうしてあげましょう。
watch(props.hoge, () => { // 変更を検知したい値
// 何らかの処理
}, { immediate: true }) // <- こいつ
もちろんドキュメントにもありますが、watchが作成時に即時実行してくれます。これでできました。やったね。
props
とemits
は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を利用すればある程度は軽減できそう
フロントエンド界隈(?)だとprovider パターンと呼ばれているらしい
Vuexみたいなstoreと何が違うの???って思うかもですが、たぶん似ている?
ただ、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
単体だけだとうまく動かないっぽい-
https://nuxt.com/docs/api/utils/navigate-to
-
Make sure to always use await or return on result of navigateTo when calling it.
-
- まぁこれはドキュメントに書いてあるからよめってなる。はい
-
https://nuxt.com/docs/api/utils/navigate-to
- リダイレクトで混乱した
- よくある要件で、「ログインしていない場合、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と友達になろうと思います。
進捗がありましたらまた投稿いたします。
(というかこれ会社の技術記事として初投稿なのか。いいのか・・・?)
Discussion