Skip to content

20260708wed-02

Published: at 15:00

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. refreactive内に入れたときの二重ラップ問題(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, // 一時的に追加
});

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>