Vue 3 TeleportとSuspense: 2026年版高度なパターンと面接質問

Vue 3のTeleportとSuspenseコンポーネントの高度な使用パターンを解説します。defer prop、ネストされたSuspense、エラーハンドリング、そして実際の面接で問われる質問まで網羅的にカバーします。

Vue 3 TeleportとSuspense高度なパターンの解説図

Vue 3のTeleportとSuspenseは、それぞれ異なるレンダリング課題を解決します。Teleportはコンポーネントを論理的なDOM階層の外に配置し、Suspenseは非同期依存関係を調整します。どちらもインポート不要のビルトインコンポーネントであり、シニアVue開発者の面接で頻繁に取り上げられるトピックです。

Vue 3.5 Teleportアップデート

Vue 3.5ではdefer propが追加され、コンポーネントツリーで後からレンダリングされるターゲットへのテレポートが可能になりました。これにより、マウント時にポータルターゲットが存在しないSSRのタイミング問題が解決されます。

Teleportの基礎とdefer Prop

Teleportはレンダリングされたコンテンツを別のDOM位置に移動しながら、Vueのコンポーネントツリーを保持します。コンポーネントは親の論理的な子として残ります。props、イベント、provide/injectはすべて通常通り動作します。Vue DevToolsでは、テレポートされたコンポーネントはターゲット要素の下ではなく、親の下にネストされて表示されます。

vue
<!-- ModalTrigger.vue -->
<script setup>
import { ref } from 'vue'

const isOpen = ref(false)
</script>

<template>
  <button @click="isOpen = true">Open Modal</button>

  <Teleport to="#modal-root">
    <div v-if="isOpen" class="modal-overlay" @click.self="isOpen = false">
      <div class="modal-content">
        <slot />
        <button @click="isOpen = false">Close</button>
      </div>
    </div>
  </Teleport>
</template>

to propはCSSセレクタ文字列またはDOM要素参照を受け取ります。マウント時にターゲットが存在しない場合、Teleportは警告を出し、何もレンダリングしません。これはSSRや動的レイアウトで問題を引き起こします。

defer Prop(Vue 3.5以降)

defer propは、アプリケーションの他の部分がマウントされるまでターゲットの解決を待つようTeleportに指示します。これにより、同じコンポーネント内や兄弟コンポーネントで後からレンダリングされる要素へのテレポートが可能になります。

vue
<!-- App.vue -->
<template>
  <Teleport defer to="#dynamic-target">
    <NotificationBanner />
  </Teleport>

  <!-- Target rendered after the Teleport -->
  <div id="dynamic-target"></div>
</template>

deferがない場合、Teleportがマウントされる時点で#dynamic-targetが存在しないため、このコードは失敗します。deferを使用すると、Vueは同じtick内のすべてのコンポーネントがマウントされた後にターゲットを解決します。ターゲットは同じマウントまたは更新サイクル内でレンダリングされる必要があります。

複数のTeleportと条件付きレンダリング

複数のTeleportコンポーネントが同じ要素をターゲットにできます。コンテンツは宣言順に追加されます。

vue
<template>
  <Teleport to="#notifications">
    <Toast message="First" />
  </Teleport>

  <Teleport to="#notifications">
    <Toast message="Second" />
  </Teleport>
</template>

<!-- Result in #notifications:
  <div>First</div>
  <div>Second</div>
-->

disabled propはテレポートを行うかどうかを制御します。無効化されると、コンテンツはインラインでレンダリングされます。このパターンは、デスクトップではモーダルがオーバーレイとして表示され、モバイルではインラインでレンダリングされるレスポンシブレイアウトを処理します。

vue
<script setup>
import { useMediaQuery } from '@vueuse/core'

const isMobile = useMediaQuery('(max-width: 768px)')
</script>

<template>
  <Teleport to="body" :disabled="isMobile">
    <MobileDrawer />
  </Teleport>
</template>

非同期コンポーネント調整のためのSuspense

Suspenseはコンポーネントツリー全体の非同期依存関係を調整します。子孫コンポーネントに未解決の非同期依存関係がある場合、Suspenseはfallbackスロットのコンテンツを表示します。すべての依存関係が解決されると、Suspenseはdefaultスロットに切り替わります。

実験的ステータス

SuspenseはVue 3.5でも実験的な状態のままです。安定版になる前にAPIが変更される可能性があります。本番環境での使用にはこのリスクを受け入れる必要があります。

vue
<!-- Dashboard.vue -->
<template>
  <Suspense>
    <template #default>
      <DashboardContent />
    </template>

    <template #fallback>
      <LoadingSpinner />
    </template>
  </Suspense>
</template>

Suspenseは2種類の非同期依存関係を追跡します。async setup()を持つコンポーネントと、defineAsyncComponentで作成された非同期コンポーネントです。

トップレベルawaitを使用した非同期Setup

Vue 3のscript setupはトップレベルawaitをサポートしています。トップレベルawaitを使用するコンポーネントは、Suspenseが追跡できる非同期依存関係になります。

vue
<!-- UserProfile.vue -->
<script setup>
const response = await fetch('/api/user/profile')
const user = await response.json()
</script>

<template>
  <div class="profile">
    <h1>{{ user.name }}</h1>
    <p>{{ user.email }}</p>
  </div>
</template>

このコンポーネントはfetchが完了するまでレンダリングできません。親のSuspense境界は解決まで fallbackを表示します。Suspense境界がない場合、コンポーネントは単にレンダリングされません。

suspensible Propを使用したネストされたSuspense

複雑なアプリケーションには複数の非同期境界があることが多いです。Vue 3.3ではsuspensible propが導入され、ネストされたSuspenseコンポーネントが親とどのように相互作用するかを制御できるようになりました。

vue
<!-- PageLayout.vue -->
<template>
  <Suspense>
    <template #default>
      <header>
        <AsyncNavigation />
      </header>

      <main>
        <!-- Inner Suspense with its own fallback -->
        <Suspense suspensible>
          <template #default>
            <AsyncContent />
          </template>
          <template #fallback>
            <ContentSkeleton />
          </template>
        </Suspense>
      </main>
    </template>

    <template #fallback>
      <PageSkeleton />
    </template>
  </Suspense>
</template>

suspensibleが設定されている場合、内側のSuspenseは親のSuspense境界に登録されます。親はAsyncNavigationAsyncContentの両方が解決されるまでfallbackを隠しません。内側のSuspenseは依然としてローカル境界として機能します。AsyncContentがより時間がかかる場合、AsyncNavigationが表示されたままContentSkeletonが表示されます。

suspensibleがない場合、内側のSuspenseは独立して動作します。親はAsyncNavigationの読み込み中のみfallbackを表示し、AsyncContentは完全に無視されます。

Vue.js / Nuxt.jsの面接対策はできていますか?

インタラクティブなシミュレーター、flashcards、技術テストで練習しましょう。

Teleport、Suspense、Transitionの組み合わせ

実際のアプリケーションではこれらのパターンを組み合わせます。非同期でコンテンツを読み込むモーダルは、3つすべての恩恵を受けます。

vue
<!-- AsyncModal.vue -->
<script setup>
import { ref, defineAsyncComponent } from 'vue'

const isOpen = ref(false)

const AsyncModalContent = defineAsyncComponent({
  loader: () => import('./ModalContent.vue'),
  loadingComponent: () => import('./ModalSkeleton.vue'),
  delay: 200,
  timeout: 10000
})
</script>

<template>
  <button @click="isOpen = true">Open</button>

  <Teleport to="body">
    <Transition name="modal">
      <div v-if="isOpen" class="modal-wrapper">
        <Suspense>
          <template #default>
            <AsyncModalContent @close="isOpen = false" />
          </template>
          <template #fallback>
            <ModalSkeleton />
          </template>
        </Suspense>
      </div>
    </Transition>
  </Teleport>
</template>

<style>
.modal-enter-active,
.modal-leave-active {
  transition: opacity 0.3s ease;
}

.modal-enter-from,
.modal-leave-to {
  opacity: 0;
}
</style>

ネスト順序が重要です。TeleportがTransitionをラップし、Transitionが条件付き要素をラップし、条件付き要素がSuspenseを含みます。これにより、トランジションはローディング状態を含むモーダル全体をアニメーションします。

Suspenseによるエラーハンドリング

Suspenseはエラーを処理しません。失敗した非同期依存関係はプロミスを拒否し、エラーをコンポーネントツリー上に伝播します。onErrorCapturedライフサイクルフックは境界でこれらのエラーをキャッチします。

vue
<!-- ErrorBoundary.vue -->
<script setup>
import { ref, onErrorCaptured } from 'vue'

const error = ref(null)

onErrorCaptured((err) => {
  error.value = err
  return false // Prevent propagation
})
</script>

<template>
  <div v-if="error" class="error-state">
    <p>Failed to load: {{ error.message }}</p>
    <button @click="error = null">Retry</button>
  </div>

  <Suspense v-else>
    <template #default>
      <slot />
    </template>
    <template #fallback>
      <slot name="loading" />
    </template>
  </Suspense>
</template>

このパターンは、ローディング状態とエラー状態の両方を持つ非同期コンポーネントツリーをラップします。リトライボタンはエラーをクリアし、Vueが再レンダリングして非同期操作を再試行します。

TeleportとSuspenseに関する面接質問

シニアVueポジションではこれらのパターンがますますテストされています。一般的な質問と、優れた回答を区別するポイントを紹介します。

Q: TeleportをCSSポジショニングの代わりに使用すべきなのはいつですか?

弱い回答:「z-indexが機能しないとき」

強い回答: TeleportはCSSでは解決できないDOM階層の問題を解決します。overflow: hiddentransformを持つコンテナ内のモーダルは新しいスタッキングコンテキストを作成し、position: fixedを壊します。Teleportはモーダルをbodyに移動させ、Vueの論理的なコンポーネントツリーを保持しながら、これらの制約から脱出します。

Q: Suspenseは各コンポーネントのローディング状態とどう違いますか?

弱い回答:「Suspenseの方がきれい」

強い回答: Suspenseは複数の非同期依存関係を調整します。個別のローディング状態を持つ10個のコンポーネントは、異なるタイミングで10個のスピナーを点滅させます。Suspenseは10個すべてが解決されるまで1つのスピナーを表示し、その後一緒に表示します。これにより、より滑らかな知覚パフォーマンスが生まれ、非同期コンポーネントが独自のローディング状態を管理する必要がなくなるため、コンポーネントコードも簡素化されます。

Q: Teleportターゲットが存在しない場合、何が起こりますか?

コンテンツはレンダリングされず、Vueは警告をログに記録します。Vue 3.5のdefer propは、他のコンポーネントが最初にマウントされるのを待つことでこれに対処します。deferなしでは、ターゲットがindex.htmlまたは子より先にマウントされる親コンポーネントに存在することを確認してください。

Q: SuspenseはOptions APIで動作しますか?

はい。async setup()を持つコンポーネントは、他の部分でComposition APIまたはOptions APIを使用しているかどうかに関係なく、Suspenseをトリガーします。非同期境界となるのはセットアップ関数であり、コンポーネントスタイルではありません。

Nuxt 3でのTeleportとSuspense

Nuxt 3はSSRの考慮事項でこれらのパターンを拡張します。NuxtのSSRドキュメントではハイドレーションのタイミングについて説明しています。

Teleportの場合、Nuxtではハイドレーション前にターゲットが存在する必要があります。ポータルターゲットをapp.vueに追加するか、defer propを使用してください。

vue
<!-- app.vue -->
<template>
  <NuxtLayout>
    <NuxtPage />
  </NuxtLayout>

  <!-- Portal targets for SSR -->
  <div id="modal-root"></div>
  <div id="toast-root"></div>
</template>

Suspenseの場合、NuxtのuseFetchuseAsyncDataは自動的に統合されます。これらのcomposableを使用するページコンポーネントは、手動設定なしでNuxtの組み込みSuspenseで動作します。関連する面接準備については、SharpSkill Vueコンポーザブルモジュールを参照してください。

Vue 3.5向け本番パターン

  • index.htmlにTeleportターゲットを配置: Vueコードが実行される前に存在する安定したポータルターゲットを作成します。これによりタイミングの問題を完全に回避できます。
  • 細かいSuspense境界: 独立したローディング単位ごとに1つのSuspenseを使用します。別々のウィジェットを持つダッシュボードは、高速なウィジェットがすぐに表示されるように、別々のSuspense境界から恩恵を受けます。
  • Suspenseの上にエラー境界を配置: 常にSuspenseをエラーハンドリングでラップしてください。ネットワーク障害やAPIエラーがツリー全体をクラッシュさせてはなりません。
  • Transitionのタイミング: SuspenseとTransitionを組み合わせる場合、状態変更中のオーバーラップを防ぐためにmode="out-in"を設定します。

今すぐ練習を始めましょう!

面接シミュレーターと技術テストで知識をテストしましょう。

今日のチャレンジ

Vue.js / Nuxt.js のバグを見つけられますか

実際のコード、隠れたバグ、1日1回。アカウントなしで試せます。

Anthony Fillion-Maillet

執筆

Anthony Fillion-Maillet

SharpSkill 創業者

10 年以上フルスタック開発に携わっています。SharpSkill を運営し、ここで公開される内容に責任を負っています。

2026年8月28日 更新

タグ

#Vue 3
#Teleport
#Suspense
#面接質問
#フロントエンド

共有

関連記事