Nuxt3もRC版になったということで、勉強も兼ねて自分のWebサイトを作っている途中に遭遇した問題について。
環境
- node.js v18.13.0
- Typescript v4.7.4
- Nuxt v3.0.0-rc.14
- TailwindCSS v3.2.4
- vue-gtag-next v1.14.0
※Vueは当然3.0系
開発環境はWSL(Debian)だったりChromeOS(Crostini)だったり色々。
本番の動作環境
- Google Cloudrun
- Firebase Hosting
Cloudrun上でNodejs+NuxtjsUniversalモード(SSR)で動作させて、静的ファイルについてはFirebase Hostingで配信するという形式。
発端
個人で制作しているWebサイトにオプトインの確認表示を実装した
WebサイトにGoogle Analytics4を導入したので、初回表示時に使用を承諾するかどうか確認表示を追加することにした。
常に画面下側に表示したいので、ルートにあたるapp.vueにoptin.vueを作成して追加。
<template>
<div id="app" ref="app" class="w-full">
<NuxtLayout>
<NuxtPage />
</NuxtLayout>
<OptIn />
</div>
</template>
optin.vueでは"optin"というcookieが無ければコンポーネントを表示するよう実装した。
<template>
<Transition name="optin">
<div v-if="isShow" class="optin fixed bottom-0 z-50">
<div class="w-full">
<AppHeading2>Cookie/解析ツール等の使用について</AppHeading2>
</div>
<div class="w-full flex justify-around">
<CommonAppBtn @click="setOptIn(false)">
DENIED/拒否
</CommonAppBtn>
<CommonAppBtn variant="denied" @click="setOptIn(true)">
ACCEPT/許可
</CommonAppBtn>
</div>
</div>
</Transition>
</template>
<script lang="ts" setup>
import { useState } from 'vue-gtag-next'
const config = useRuntimeConfig()
const gaMeasurementID = config.public.gaMeasurementId
const gtag = useState()
const optIn = useCookie<'ACCEPT'|'DENIED'|undefined>('optin')
const isShow = ref<boolean>(false)
const start = () => {
if (gtag.isEnabled === undefined) {
return
}
gtag.isEnabled.value = true
}
const checkCookie = () => {
if (!optIn.value) {
isShow.value = true
return
}
if (optIn.value !== 'ACCEPT') {
return
}
start()
}
checkCookie()
</script>
実装してみると問題なく動いた()のと、個人で作ってるお気楽Webサイトにはステージング環境なんてものはないので本番環境にデプロイして動作確認してみることに。
すると不具合が発生。
症状
デプロイすると必ず確認表示が表示される。
デプロイしてサイトを表示してみると、常に一瞬だけ確認表示コンポーネントが表示されてしまう。Cookieが空だろうがなんだろうがお構いなし。
ハイドレーションエラー
ブラウザのConsoleにはハイドレーションエラーが表示される。optin.vue内の要素になにか問題があるらしい。
(当時のスクリーンショットが無いので文章のみ。)
ローカルでは再現しない。
問題究明のためにまずローカルで動作を確認してみるが、まったく再現しない。
=>ローカルで再現しないという事はCloudrunかFirebase Hostingの問題では?
原因
今の構成では特定のcookie以外が無視される。
公式ドキュメント曰く、Firebase HostingとCloudrunを組み合わせて使う場合"__session"というkey以外のcookieを自動的に削除する。つまりSSR時の挙動としては
- サーバーサイドではcookieがundefinedになる。
- なのでオプトイン確認を表示した状態でレンダリング
- クライアントサイドではCookieがundefinedではない。
- なのでオプトイン確認を非表示に変更(この処理が完了するまでの間だけ表示される。)
- サーバーサイドとクライアントサイドでHTMLが食い違っているためハイドレーションエラーになる。
というオチだった。
解決策
要するにサーバーサイドでオプトイン確認表示に関する処理を行わなければよいのでやることは簡単。
<ClientOnly> を使う。
Nuxtの場合は<ClientOnly>でコンポーネントを囲むと常にクライアントサイドでのレンダリングに変わる。
なのでoptin.vueをこんな感じで書き換えた。
ClientOnlyについてはこちら
<template>
<ClientOnly>
<Transition name="optin">
<div v-if="isShow">
// 省略
</div>
</Transition>
</ClientOnly>
</template>
あるいは表示フラグを初期値falseにしておいて、クライアントサイドでCookieの確認を行うようにonMounted内で処理を書く。これでハイドレーションエラーも消えるはず。
<script lang="ts" setup>
const isShow = ref<boolean>(false)
// 省略
onMounted(() => {
nextTick(() => {
if (!optIn.value) {
isShow.value = true
return
}
if (optIn.value !== 'ACCEPT') {
removeGaCookie()
return
}
start()
})
})
</script>
感想
Nuxt3楽しい。