Nuxt 2 → 4 移行:modal選択後にcomputed()が空になる問題
考えられる原因
1. SSRハイドレーション時のstate上書き(最有力)
Nuxt 4はデフォルトでSSR+クライアントハイドレーションが走ります。流れは:
サーバー側でsetup()実行 → HTMLレンダリング
↓
クライアント側でsetup()再実行(ハイドレーション)← ここでstateが初期値に戻る
↓
computed()が再評価 → 空になる
Nuxt 2のSSG+CSRは「ビルド時にHTMLを生成、その後はCSRのみ」なので、このダブル実行が起きていませんでした。
2. PiniaのSSR状態が引き継がれていない
Vuex(Nuxt 2)からPinia(Nuxt 4)に移行している場合、サーバー側のPiniaのstateがクライアントに引き継がれない(payloadに乗っていない)と、クライアントでstore初期値が使われます。
3. useAsyncData / useFetch の非同期タイミング
// こういった書き方の場合
const selected = computed(() =>
asyncData.value?.items.find(i => i.id === selectedId.value)
);
ハイドレーション完了前にasyncData.valueがnullになる瞬間があり、computedが空を返します。
4. refをreactive内に入れたときの二重ラップ問題(Vue 2→3の変更)
Vue 2ではthis.xxx = valueで直接代入できましたが、Vue 3でreactive({ value: ref() })のような構造になっていると.valueの扱いが変わり、意図しない空値になる場合があります。
調査方法
Step 1: SSRを無効化してエラーが消えるか確認
nuxt.config.tsに以下を追加:
export default defineNuxtConfig({
ssr: false, // 一時的に追加
});
- これで直る → SSRハイドレーションが原因
- 直らない → 純粋なVue 3リアクティビティの問題
Step 2: computedの依存を可視化
const selectedValue = computed(() => {
console.log("[computed] 再評価 deps:", someRef.value, anotherRef.value);
return someRef.value ? someRef.value.name : "";
});
console.logでいつ・どんな値で再評価されているか確認。
Step 3: watchでトリガーを特定
watch(selectedValue, (newVal, oldVal) => {
console.log("[watch] 変化:", oldVal, "→", newVal);
console.trace(); // スタックトレースで呼び出し元を確認
});
Step 4: Vue DevToolsでタイムライン確認
ブラウザのVue DevTools → Timelineタブで、どのイベントがcomputedの再評価を引き起こしているか確認できます。
対策
対策A: useState() でSSR-safeな状態管理
// ❌ Nuxt 4で問題になりやすい
const selectedItem = ref(null);
// ✅ SSR/CSR間でstateが共有される
const selectedItem = useState("selectedItem", () => null);
対策B: Piniaのstate hydration設定確認
@pinia/nuxtを使っている場合、自動的にpayloadに乗るはずですが、念のため確認:
// nuxt.config.ts
export default defineNuxtConfig({
modules: ["@pinia/nuxt"],
});
対策C: Modal選択後の処理をonMounted以降に限定
// ❌ setup直下はSSR/CSR両方で実行される
const result = someStore.selectedValue;
// ✅ クライアント限定処理
onMounted(() => {
// ここはクライアントでのみ実行
result = someStore.selectedValue;
});
対策D: モーダル部分を<ClientOnly>で囲む
<ClientOnly>
<MyModal v-model="selected" />
</ClientOnly>