Slide 1

Slide 1 text

Nuxt 4 の Singleton Data Fetching Layer で 何が変わるのか 2025.10.25 Vue Fes Japan 2025 View slides: naokihaba/talks

Slide 2

Slide 2 text

Naoki Haba naokihaba @naokihaba naohaba.bsky.social Software Engineer. Vue Fes Japan 2025 Core Staff

Slide 3

Slide 3 text

Nuxt 4 リリースから3 ヶ月 🙋 皆さんはもう Nuxt 4 を使い始めていますか? 今日は、Nuxt 4 で改善されたデータフェッチの仕組みについてお話しします

Slide 4

Slide 4 text

その前に、こんな経験ありませんか? ✗ 同じキーで呼び出しても複数回リクエストが発生 ✗ key が変わっても手動でwatch を書く必要がある ✗ キャッシュを使うタイミングを細かく制御できない ✗ 不要なデータが残り続けてメモリリークのリスク ✅ Singleton Data Fetching Layer で解決

Slide 5

Slide 5 text

問題の詳細 Problem Details

Slide 6

Slide 6 text

Nuxt 3 までの挙動を見てみよう ComponentA.vue ComponentB.vue 同じキー 'user' を使っているが… const { data } = useAsyncData('user', async () => { console.log(' 🔵 ComponentA: リクエスト開始') const result = await $fetch('/api/users') return result }, { server: false }) const { data } = useAsyncData('user', async () => { console.log(' 🟢 ComponentB: リクエスト開始') const result = await $fetch('/api/users') return result }, { server: false }) どうなると思いますか?

Slide 7

Slide 7 text

結果:2 回リクエストが実行される 😱 Console 🔵 ComponentA と 🟢 ComponentB の両方でログ が出力される Network 同じ key: 'user' なのに 2 回のリクエストが発生

Slide 8

Slide 8 text

なぜ2 回実行される? 課題: 各コンポーネントで asyncData をコピーし、 refresh も新規作成 → ComponentA と ComponentB で それぞれ handler が実行される Nuxt 3 の実装 const asyncData = { ...nuxtApp._asyncData[key] } // オブジェクトをコピー asyncData.refresh = () => handler(nuxtApp) // refresh 関数を新規作成 if (options.immediate) { initialFetch() // ← ここで各コンポーネントごとにフェッチが実行される // 各コンポーネントでの処理 // immediate: true (デフォルト)なら即実行 }

Slide 9

Slide 9 text

解決策 Solution

Slide 10

Slide 10 text

改善1: 同じkey でref インスタンスを共有 Before (Nuxt 3) After (Nuxt 4) users1 !== users2 // 別々の ref // handler が 2 回実行 😢 const { data: users1 } = useAsyncData( 'users', () => $fetch('/api/users') ) const { data: users2 } = useAsyncData( 'users', () => $fetch('/api/users') ) users1 === users2 // 同じ ref // handler は 1 回だけ ✨ const { data: users1 } = useAsyncData( 'users', () => $fetch('/api/users') ) const { data: users2 } = useAsyncData( 'users', () => $fetch('/api/users') )

Slide 11

Slide 11 text

実装: _init フラグによる厳密な管理 ポイント _init フラグで 一度だけ初期化 を保証 既に初期化済みの場合は既存のインスタンスを使用 → 同じ key なら 同じ ref インスタンス を共有 if (!nuxtApp._asyncData[key.value]?._init) { initialFetchOptions.cachedData = options.getCachedData(key.value, nuxtApp, ...); nuxtApp._asyncData[key.value] = createAsyncData(nuxtApp, key.value, _handler, options, ...); } const initialFetch = () => nuxtApp._asyncData[key.value].execute(initialFetchOptions);

Slide 12

Slide 12 text

補足 Nuxt 3.17.2 から徐々に実装が進行 Singleton Data Fetching Layer の機能は一度にすべてが導入されたわけで はありません Nuxt 3.17.2 から段階的に実装が開始され、Nuxt 4 で完成形となりました この期間に、ref の共有、reactive key のサポート、自動クリーンアップ などが順次追加されました 段階的な実装について

Slide 13

Slide 13 text

その他の改善点 Other Improvements

Slide 14

Slide 14 text

その他の改善点 1. getCachedData の拡張制御 すべてのデータ取得時に呼び出される 初回だけでなく、watch や refresh 時も キャッシュ戦略を柔軟に制御可能 2. Reactive key のサポート computed や ref を key に使用できる。 Reactive key が変わると自動的にデータを再取得 getCachedData: (key, nuxtApp, ctx) => { // ctx.cause で呼び出し元を判定 // 'initial' | 'watch' | 'refresh:manual' if (ctx.cause === 'initial') { return nuxtApp.payload.data[key] const { data } = useAsyncData('user', () => $fetch('/api/user'), { } return nuxtApp.static.data[key] } }) const userId = ref('1') const key = computed(() => `user-${userId.value}`) const { data } = useAsyncData(key, () => $fetch(`/api/users/${userId.value}`) userId.value = '2' // watch を書く必要がない! // key が変わると自動的にデータを再取得 ) // userId を変更すると自動的に再フェッチ

Slide 15

Slide 15 text

1. 自動データクリーンアップ 使われなくなったデータは自動的に削除される asyncData._deps++ if (data?._deps) { data._deps-- // 誰も参照していなければクリーンアップ if (data._deps === 0) { data._off() // データを自動削除 } // 参照カウントで追跡 // コンポーネントがアンマウントされたら const unregister = (key) => { const data = nuxtApp._asyncData[key] } } onScopeDispose(() => { unregister(key.value) })

Slide 16

Slide 16 text

まとめ ✅ 同じ key で ref インスタンスを共有 ✅ Reactive key のサポート ✅ getCachedData の拡張制御 ✅ 自動データクリーンアップ ✨ パフォーマンス向上 & メモリ管理の改善 Nuxt 4 Singleton Data Fetching Layer

Slide 17

Slide 17 text

ご清聴ありがとう ございました Slides: naokihaba/talks