Vue 3 の Teleport と Suspense 活用ガイド

Vue 3 で新たに追加された Teleport と Suspense は、モダンな Web アプリケーション開発において非常に重要な機能です。これらの機能を理解し活用することで、より良いユーザー体験を提供できるようになります。
従来の Vue 2 では、モーダルやツールチップの実装で z-index の問題に悩まされたり、非同期データの読み込み状態管理が複雑になったりしていました。しかし、Vue 3 の Teleport と Suspense がこれらの課題を解決し、開発者の悩みを軽減してくれます。
本記事では、これらの新機能の基本概念から実践的な活用方法まで、体系的に学べるように構成しています。実際のコード例やエラーハンドリングも含めて、すぐに現場で使える知識をお届けします。
背景
Vue 3 における新機能の必要性
Vue 3 の設計において、開発者が長年抱えていた課題を解決するため、Teleport と Suspense という革新的な機能が追加されました。
Vue 2 までの開発では、以下のような問題に直面することがよくありました:
# | 課題 | 従来の解決策 | 問題点 |
---|---|---|---|
1 | モーダルの z-index 問題 | CSS の z-index を手動調整 | メンテナンス困難 |
2 | 非同期データ読み込み状態管理 | 各コンポーネントで loading 状態を管理 | コードの重複 |
3 | ツールチップの表示位置制御 | 絶対位置指定と JavaScript 計算 | レスポンシブ対応が困難 |
4 | ページ遷移時の非同期処理 | 手動で Promise を管理 | エラーハンドリングが複雑 |
これらの課題は、実際の開発現場で多くの開発者が体験し、時間とエネルギーを消費していた問題でした。Vue 3 の Teleport と Suspense は、これらの問題を根本的に解決するために設計されています。
Teleport とは
DOM 要素の転送機能の基本概念
Teleport は、Vue 3 で新しく追加されたビルトインコンポーネントで、コンポーネントの一部を別の場所の DOM に「転送」する機能です。
まず、Teleport の基本的な使用方法を見てみましょう:
vue<template>
<div class="container">
<h1>メインコンテンツ</h1>
<button @click="showModal = true">
モーダルを開く
</button>
<!-- Teleportを使用してモーダルをbodyに転送 -->
<Teleport to="body">
<div v-if="showModal" class="modal-overlay">
<div class="modal">
<h2>モーダルコンテンツ</h2>
<button @click="showModal = false">閉じる</button>
</div>
</div>
</Teleport>
</div>
</template>
<script setup>
import { ref } from 'vue';
const showModal = ref(false);
</script>
このコードでは、<Teleport to="body">
を使用することで、モーダルの内容が実際には<body>
要素の直下に挿入されます。これにより、CSS の z-index やレイアウトの問題を回避できます。
Teleport の仕組み
Teleport は以下のように動作します:
- 仮想 DOM 内では通常通り記述:コンポーネントの構造として自然に記述
- 実際の DOM では指定した場所に挿入:
to
属性で指定したセレクタの場所に移動 - Vue のリアクティブシステムは維持:親コンポーネントの状態変更も反映
これにより、開発者は直感的にコンポーネントを設計しながら、実際の DOM 構造は最適化できるのです。
Suspense とは
非同期コンポーネントの読み込み管理
Suspense は、非同期コンポーネントの読み込み状態を管理するビルトインコンポーネントです。React の Suspense からインスピレーションを得て、Vue 3 に実装されました。
以下は、Suspense の基本的な使用例です:
vue<template>
<div class="app">
<h1>非同期コンポーネントの例</h1>
<Suspense>
<!-- 非同期コンポーネント -->
<template #default>
<AsyncUserProfile :userId="userId" />
</template>
<!-- 読み込み中のフォールバック -->
<template #fallback>
<div class="loading">
<div class="spinner"></div>
<p>ユーザー情報を読み込み中...</p>
</div>
</template>
</Suspense>
</div>
</template>
<script setup>
import { ref } from 'vue';
import AsyncUserProfile from './AsyncUserProfile.vue';
const userId = ref(123);
</script>
非同期コンポーネントの例(AsyncUserProfile.vue):
vue<template>
<div class="user-profile">
<img :src="user.avatar" :alt="user.name" />
<h2>{{ user.name }}</h2>
<p>{{ user.email }}</p>
</div>
</template>
<script setup>
import { ref } from 'vue';
const props = defineProps(['userId']);
const user = ref(null);
// 非同期でユーザー情報を取得
const fetchUser = async (id) => {
const response = await fetch(`/api/users/${id}`);
if (!response.ok) {
throw new Error('ユーザー情報の取得に失敗しました');
}
return response.json();
};
// setup内でawaitを使用すると、自動的に非同期コンポーネントになる
user.value = await fetchUser(props.userId);
</script>
Suspense の利点
Suspense を使用することで、以下の利点が得られます:
- 統一された読み込み状態管理:複数の非同期コンポーネントを一括管理
- エラーハンドリングの簡素化:ErrorBoundary と組み合わせて使用可能
- ネストした非同期処理への対応:複雑な依存関係も管理可能
- コードの簡潔性:boilerplate コードの削減
Teleport の基本実装
モーダル・ツールチップでの活用
実際の開発で Teleport を活用する場面で最も多いのは、モーダルやツールチップの実装です。ここでは、実践的な例を見てみましょう。
モーダルコンポーネントの実装
まず、再利用可能なモーダルコンポーネントを作成します:
vue<!-- components/Modal.vue -->
<template>
<Teleport to="body">
<Transition name="modal">
<div
v-if="isVisible"
class="modal-overlay"
@click="onOverlayClick"
>
<div class="modal-container" @click.stop>
<div class="modal-header">
<slot name="header">
<h3>{{ title }}</h3>
</slot>
<button class="close-btn" @click="close">
×
</button>
</div>
<div class="modal-body">
<slot></slot>
</div>
<div class="modal-footer" v-if="$slots.footer">
<slot name="footer"></slot>
</div>
</div>
</div>
</Transition>
</Teleport>
</template>
<script setup>
import { watch } from 'vue';
const props = defineProps({
modelValue: Boolean,
title: String,
closeOnOverlay: { type: Boolean, default: true },
});
const emit = defineEmits(['update:modelValue', 'close']);
const isVisible = computed(() => props.modelValue);
const close = () => {
emit('update:modelValue', false);
emit('close');
};
const onOverlayClick = () => {
if (props.closeOnOverlay) {
close();
}
};
// モーダルが開いた時にbodyのスクロールを無効化
watch(isVisible, (newValue) => {
if (newValue) {
document.body.style.overflow = 'hidden';
} else {
document.body.style.overflow = '';
}
});
</script>
対応する CSS スタイル:
css.modal-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
}
.modal-container {
background: white;
border-radius: 8px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
max-width: 90%;
max-height: 90%;
overflow: auto;
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1rem;
border-bottom: 1px solid #e5e7eb;
}
.close-btn {
background: none;
border: none;
font-size: 1.5rem;
cursor: pointer;
padding: 0.25rem;
}
.modal-enter-active,
.modal-leave-active {
transition: opacity 0.3s ease;
}
.modal-enter-from,
.modal-leave-to {
opacity: 0;
}
ツールチップの実装
次に、ツールチップコンポーネントを作成します:
vue<!-- components/Tooltip.vue -->
<template>
<div
class="tooltip-wrapper"
@mouseenter="show"
@mouseleave="hide"
>
<slot></slot>
<Teleport to="body">
<div
v-if="isVisible"
ref="tooltipRef"
class="tooltip"
:style="tooltipStyle"
@mouseenter="show"
@mouseleave="hide"
>
<div class="tooltip-content">
<slot name="content">{{ content }}</slot>
</div>
<div
class="tooltip-arrow"
:style="arrowStyle"
></div>
</div>
</Teleport>
</div>
</template>
<script setup>
import { ref, reactive, nextTick } from 'vue';
const props = defineProps({
content: String,
position: { type: String, default: 'top' },
delay: { type: Number, default: 0 },
});
const isVisible = ref(false);
const tooltipRef = ref(null);
const tooltipStyle = reactive({});
const arrowStyle = reactive({});
let showTimer = null;
let hideTimer = null;
const show = () => {
clearTimeout(hideTimer);
if (props.delay > 0) {
showTimer = setTimeout(() => {
isVisible.value = true;
nextTick(() => updatePosition());
}, props.delay);
} else {
isVisible.value = true;
nextTick(() => updatePosition());
}
};
const hide = () => {
clearTimeout(showTimer);
hideTimer = setTimeout(() => {
isVisible.value = false;
}, 100);
};
const updatePosition = () => {
if (!tooltipRef.value) return;
const trigger = tooltipRef.value.previousElementSibling;
const triggerRect = trigger.getBoundingClientRect();
const tooltipRect =
tooltipRef.value.getBoundingClientRect();
let top, left;
switch (props.position) {
case 'top':
top = triggerRect.top - tooltipRect.height - 8;
left =
triggerRect.left +
(triggerRect.width - tooltipRect.width) / 2;
break;
case 'bottom':
top = triggerRect.bottom + 8;
left =
triggerRect.left +
(triggerRect.width - tooltipRect.width) / 2;
break;
case 'left':
top =
triggerRect.top +
(triggerRect.height - tooltipRect.height) / 2;
left = triggerRect.left - tooltipRect.width - 8;
break;
case 'right':
top =
triggerRect.top +
(triggerRect.height - tooltipRect.height) / 2;
left = triggerRect.right + 8;
break;
}
// 画面外にはみ出さないよう調整
if (left < 0) left = 8;
if (left + tooltipRect.width > window.innerWidth) {
left = window.innerWidth - tooltipRect.width - 8;
}
tooltipStyle.top = `${top}px`;
tooltipStyle.left = `${left}px`;
};
</script>
Suspense の基本実装
ローディング状態の管理
Suspense を使用した実用的なローディング状態管理の例を見てみましょう。
基本的な Suspense の使用
vue<template>
<div class="dashboard">
<h1>ダッシュボード</h1>
<Suspense>
<template #default>
<div class="dashboard-content">
<UserStats />
<RecentActivity />
<ChartComponent />
</div>
</template>
<template #fallback>
<div class="loading-container">
<div class="skeleton-loader">
<div class="skeleton-header"></div>
<div class="skeleton-stats">
<div class="skeleton-card"></div>
<div class="skeleton-card"></div>
<div class="skeleton-card"></div>
</div>
<div class="skeleton-chart"></div>
</div>
</div>
</template>
</Suspense>
</div>
</template>
<script setup>
import UserStats from './UserStats.vue';
import RecentActivity from './RecentActivity.vue';
import ChartComponent from './ChartComponent.vue';
</script>
非同期コンポーネントの作成
UserStats コンポーネントの例:
vue<!-- components/UserStats.vue -->
<template>
<div class="user-stats">
<h2>ユーザー統計</h2>
<div class="stats-grid">
<div
class="stat-card"
v-for="stat in stats"
:key="stat.label"
>
<div class="stat-value">{{ stat.value }}</div>
<div class="stat-label">{{ stat.label }}</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue';
const stats = ref([]);
// 非同期でデータを取得
const fetchStats = async () => {
try {
const response = await fetch('/api/stats');
if (!response.ok) {
throw new Error(
`HTTP error! status: ${response.status}`
);
}
return await response.json();
} catch (error) {
console.error('Stats fetch error:', error);
throw error;
}
};
// awaitを使用すると自動的に非同期コンポーネントになる
stats.value = await fetchStats();
</script>
エラーハンドリングと Suspense
Suspense と組み合わせて使用するエラーハンドリングの実装:
vue<template>
<div class="app">
<ErrorBoundary>
<template #default>
<Suspense>
<template #default>
<AsyncContent />
</template>
<template #fallback>
<LoadingSpinner />
</template>
</Suspense>
</template>
<template #fallback="{ error, retry }">
<div class="error-container">
<h2>エラーが発生しました</h2>
<p>{{ error.message }}</p>
<button @click="retry">再試行</button>
</div>
</template>
</ErrorBoundary>
</div>
</template>
<script setup>
import { defineAsyncComponent } from 'vue';
import ErrorBoundary from './ErrorBoundary.vue';
import LoadingSpinner from './LoadingSpinner.vue';
const AsyncContent = defineAsyncComponent({
loader: () => import('./AsyncContent.vue'),
errorComponent: () => import('./ErrorComponent.vue'),
delay: 200,
timeout: 5000,
});
</script>
Teleport 応用テクニック
複数ターゲット・条件付き転送
より高度な Teleport の使用方法を学びましょう。
条件付き転送の実装
画面サイズに応じて転送先を変更する例:
vue<template>
<div class="responsive-component">
<div class="main-content">
<h1>メインコンテンツ</h1>
<button @click="toggleSidebar">
サイドバーを開く
</button>
</div>
<!-- 画面サイズに応じて転送先を変更 -->
<Teleport
:to="sidebarTarget"
:disabled="!shouldTeleport"
>
<div v-if="showSidebar" class="sidebar">
<h2>サイドバー</h2>
<nav>
<ul>
<li><a href="#home">ホーム</a></li>
<li><a href="#about">について</a></li>
<li><a href="#contact">お問い合わせ</a></li>
</ul>
</nav>
<button @click="toggleSidebar">閉じる</button>
</div>
</Teleport>
</div>
</template>
<script setup>
import { ref, computed, onMounted, onUnmounted } from 'vue';
const showSidebar = ref(false);
const windowWidth = ref(window.innerWidth);
const shouldTeleport = computed(
() => windowWidth.value < 768
);
const sidebarTarget = computed(() =>
shouldTeleport.value ? 'body' : '.main-content'
);
const toggleSidebar = () => {
showSidebar.value = !showSidebar.value;
};
const handleResize = () => {
windowWidth.value = window.innerWidth;
};
onMounted(() => {
window.addEventListener('resize', handleResize);
});
onUnmounted(() => {
window.removeEventListener('resize', handleResize);
});
</script>
動的ターゲットの管理
複数のターゲットを動的に切り替える例:
vue<template>
<div class="multi-target-example">
<div class="zones">
<div id="zone-1" class="drop-zone">ゾーン1</div>
<div id="zone-2" class="drop-zone">ゾーン2</div>
<div id="zone-3" class="drop-zone">ゾーン3</div>
</div>
<div class="controls">
<button @click="moveToZone('zone-1')">
ゾーン1に移動
</button>
<button @click="moveToZone('zone-2')">
ゾーン2に移動
</button>
<button @click="moveToZone('zone-3')">
ゾーン3に移動
</button>
</div>
<Teleport :to="currentTarget">
<div class="moveable-element">
<p>移動可能な要素</p>
<p>現在の場所: {{ currentZone }}</p>
</div>
</Teleport>
</div>
</template>
<script setup>
import { ref, computed } from 'vue';
const currentZone = ref('zone-1');
const currentTarget = computed(
() => `#${currentZone.value}`
);
const moveToZone = (zoneId) => {
currentZone.value = zoneId;
};
</script>
Suspense 応用テクニック
エラーハンドリング・ネストした非同期処理
より複雑な非同期処理のパターンを学びましょう。
ネストした Suspense の実装
vue<template>
<div class="nested-suspense-example">
<h1>ネストしたSuspenseの例</h1>
<Suspense>
<template #default>
<UserProfile>
<Suspense>
<template #default>
<UserPosts />
</template>
<template #fallback>
<div class="posts-loading">
投稿を読み込み中...
</div>
</template>
</Suspense>
</UserProfile>
</template>
<template #fallback>
<div class="profile-loading">
プロフィールを読み込み中...
</div>
</template>
</Suspense>
</div>
</template>
<script setup>
import UserProfile from './UserProfile.vue';
import UserPosts from './UserPosts.vue';
</script>
複数の非同期リソースの管理
複数の API を並行して呼び出す例:
vue<!-- components/Dashboard.vue -->
<template>
<div class="dashboard">
<h1>ダッシュボード</h1>
<div class="dashboard-grid">
<div class="user-info">
<h2>{{ userInfo.name }}</h2>
<p>{{ userInfo.email }}</p>
</div>
<div class="stats">
<h3>統計情報</h3>
<div
class="stat-item"
v-for="stat in stats"
:key="stat.id"
>
<span>{{ stat.label }}</span>
<span>{{ stat.value }}</span>
</div>
</div>
<div class="recent-activity">
<h3>最近のアクティビティ</h3>
<ul>
<li
v-for="activity in activities"
:key="activity.id"
>
{{ activity.description }}
</li>
</ul>
</div>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue';
const userInfo = ref({});
const stats = ref([]);
const activities = ref([]);
// 複数のAPIを並行して呼び出し
const fetchUserInfo = async () => {
const response = await fetch('/api/user');
if (!response.ok) {
throw new Error('ユーザー情報の取得に失敗しました');
}
return response.json();
};
const fetchStats = async () => {
const response = await fetch('/api/stats');
if (!response.ok) {
throw new Error('統計情報の取得に失敗しました');
}
return response.json();
};
const fetchActivities = async () => {
const response = await fetch('/api/activities');
if (!response.ok) {
throw new Error('アクティビティの取得に失敗しました');
}
return response.json();
};
// Promise.allを使用して並行処理
const [userInfoData, statsData, activitiesData] =
await Promise.all([
fetchUserInfo(),
fetchStats(),
fetchActivities(),
]);
userInfo.value = userInfoData;
stats.value = statsData;
activities.value = activitiesData;
</script>
エラーハンドリングの実装
よくあるエラーとその対処法:
javascript// エラーハンドリングのベストプラクティス
const fetchDataWithErrorHandling = async () => {
try {
const response = await fetch('/api/data');
// HTTPエラーステータスのチェック
if (!response.ok) {
switch (response.status) {
case 404:
throw new Error('データが見つかりません');
case 401:
throw new Error('認証が必要です');
case 403:
throw new Error('アクセス権限がありません');
case 500:
throw new Error('サーバーエラーが発生しました');
default:
throw new Error(`HTTP Error: ${response.status}`);
}
}
const data = await response.json();
return data;
} catch (error) {
// ネットワークエラーの場合
if (error instanceof TypeError) {
throw new Error('ネットワークエラーが発生しました');
}
// その他のエラー
console.error('Data fetch error:', error);
throw error;
}
};
Teleport と Suspense の組み合わせ
動的コンポーネントでの連携
Teleport と Suspense を組み合わせることで、より高度な機能を実現できます。
動的モーダルの実装
vue<template>
<div class="dynamic-modal-example">
<div class="modal-triggers">
<button @click="openModal('user-profile')">
ユーザープロフィール
</button>
<button @click="openModal('settings')">設定</button>
<button @click="openModal('analytics')">分析</button>
</div>
<Teleport to="body">
<div
v-if="activeModal"
class="modal-overlay"
@click="closeModal"
>
<div class="modal-container" @click.stop>
<div class="modal-header">
<h2>{{ modalTitle }}</h2>
<button @click="closeModal">×</button>
</div>
<div class="modal-content">
<Suspense>
<template #default>
<component :is="modalComponent" />
</template>
<template #fallback>
<div class="modal-loading">
<div class="spinner"></div>
<p>コンテンツを読み込み中...</p>
</div>
</template>
</Suspense>
</div>
</div>
</div>
</Teleport>
</div>
</template>
<script setup>
import { ref, computed, defineAsyncComponent } from 'vue';
const activeModal = ref(null);
const modalComponents = {
'user-profile': defineAsyncComponent(() =>
import('./UserProfileModal.vue')
),
settings: defineAsyncComponent(() =>
import('./SettingsModal.vue')
),
analytics: defineAsyncComponent(() =>
import('./AnalyticsModal.vue')
),
};
const modalTitles = {
'user-profile': 'ユーザープロフィール',
settings: '設定',
analytics: '分析',
};
const modalComponent = computed(() => {
return activeModal.value
? modalComponents[activeModal.value]
: null;
});
const modalTitle = computed(() => {
return activeModal.value
? modalTitles[activeModal.value]
: '';
});
const openModal = (modalType) => {
activeModal.value = modalType;
};
const closeModal = () => {
activeModal.value = null;
};
</script>
非同期ツールチップの実装
vue<!-- components/AsyncTooltip.vue -->
<template>
<div
class="async-tooltip-wrapper"
@mouseenter="handleMouseEnter"
@mouseleave="handleMouseLeave"
>
<slot></slot>
<Teleport to="body">
<div
v-if="showTooltip"
ref="tooltipElement"
class="async-tooltip"
:style="tooltipStyle"
>
<Suspense>
<template #default>
<component
:is="tooltipComponent"
v-bind="componentProps"
/>
</template>
<template #fallback>
<div class="tooltip-loading">
<div class="loading-spinner"></div>
</div>
</template>
</Suspense>
</div>
</Teleport>
</div>
</template>
<script setup>
import {
ref,
computed,
defineAsyncComponent,
nextTick,
} from 'vue';
const props = defineProps({
tooltipType: String,
tooltipProps: Object,
delay: { type: Number, default: 500 },
});
const showTooltip = ref(false);
const tooltipElement = ref(null);
const tooltipStyle = ref({});
let showTimer = null;
let hideTimer = null;
const tooltipComponents = {
'user-card': defineAsyncComponent(() =>
import('./UserCardTooltip.vue')
),
'product-info': defineAsyncComponent(() =>
import('./ProductInfoTooltip.vue')
),
'quick-stats': defineAsyncComponent(() =>
import('./QuickStatsTooltip.vue')
),
};
const tooltipComponent = computed(() => {
return props.tooltipType
? tooltipComponents[props.tooltipType]
: null;
});
const componentProps = computed(
() => props.tooltipProps || {}
);
const handleMouseEnter = () => {
clearTimeout(hideTimer);
showTimer = setTimeout(() => {
showTooltip.value = true;
nextTick(() => updateTooltipPosition());
}, props.delay);
};
const handleMouseLeave = () => {
clearTimeout(showTimer);
hideTimer = setTimeout(() => {
showTooltip.value = false;
}, 100);
};
const updateTooltipPosition = () => {
if (!tooltipElement.value) return;
const trigger = tooltipElement.value.parentElement;
const triggerRect = trigger.getBoundingClientRect();
const tooltipRect =
tooltipElement.value.getBoundingClientRect();
const left =
triggerRect.left +
(triggerRect.width - tooltipRect.width) / 2;
const top = triggerRect.top - tooltipRect.height - 8;
tooltipStyle.value = {
left: `${Math.max(8, left)}px`,
top: `${Math.max(8, top)}px`,
position: 'fixed',
zIndex: 1000,
};
};
</script>
パフォーマンス最適化
効率的な使用方法
Teleport と Suspense を使用する際のパフォーマンス最適化のポイントを学びましょう。
Teleport の最適化
vue<template>
<div class="optimized-teleport">
<!-- 不要な再レンダリングを避けるための工夫 -->
<Teleport
:to="teleportTarget"
:disabled="!shouldTeleport"
>
<div v-if="showContent" class="teleported-content">
<ExpensiveComponent v-if="shouldRenderExpensive" />
<LightweightComponent v-else />
</div>
</Teleport>
</div>
</template>
<script setup>
import { ref, computed, shallowRef } from 'vue';
// shallowRefを使用して不要な深い監視を避ける
const expensiveData = shallowRef({});
const showContent = ref(false);
const shouldTeleport = computed(() => {
// 条件を明確にして不要な転送を避ける
return window.innerWidth < 768 && showContent.value;
});
const teleportTarget = computed(() => {
// 動的なターゲット選択を最適化
return shouldTeleport.value
? '#mobile-container'
: '.desktop-container';
});
const shouldRenderExpensive = computed(() => {
// 高コストなコンポーネントの条件分岐
return expensiveData.value.length > 100;
});
</script>
Suspense の最適化
vue<template>
<div class="optimized-suspense">
<Suspense>
<template #default>
<LazyComponent :key="componentKey" />
</template>
<template #fallback>
<OptimizedLoader />
</template>
</Suspense>
</div>
</template>
<script setup>
import { ref, computed, defineAsyncComponent } from 'vue';
const currentRoute = ref('home');
// キャッシュ機能付きの非同期コンポーネント
const LazyComponent = defineAsyncComponent({
loader: () => import('./LazyComponent.vue'),
loadingComponent: () => import('./LoadingComponent.vue'),
errorComponent: () => import('./ErrorComponent.vue'),
delay: 200, // 200ms後にローディングを表示
timeout: 5000, // 5秒でタイムアウト
suspensible: true, // Suspenseと連携
});
// キーを使用してコンポーネントのキャッシュを制御
const componentKey = computed(() => {
return `${currentRoute.value}-${Date.now()}`;
});
</script>
メモリリークの防止
javascript// メモリリークを防ぐためのベストプラクティス
import { ref, onUnmounted } from 'vue';
export default {
setup() {
const timers = [];
const eventListeners = [];
const addTimer = (callback, delay) => {
const timer = setTimeout(callback, delay);
timers.push(timer);
return timer;
};
const addEventListener = (element, event, handler) => {
element.addEventListener(event, handler);
eventListeners.push({ element, event, handler });
};
// コンポーネントのアンマウント時にクリーンアップ
onUnmounted(() => {
// タイマーのクリア
timers.forEach((timer) => clearTimeout(timer));
// イベントリスナーの削除
eventListeners.forEach(
({ element, event, handler }) => {
element.removeEventListener(event, handler);
}
);
// DOM要素の参照をクリア
if (document.body.style.overflow === 'hidden') {
document.body.style.overflow = '';
}
});
return {
addTimer,
addEventListener,
};
},
};
バンドルサイズの最適化
javascript// 動的インポートを使用してバンドルサイズを最適化
const optimizedComponents = {
// 必要な時だけロード
modal: () => import('./Modal.vue'),
tooltip: () => import('./Tooltip.vue'),
// 条件付きロード
heavyComponent: () => {
if (process.env.NODE_ENV === 'production') {
return import('./HeavyComponent.vue');
}
return import('./LightweightComponent.vue');
},
};
// プリロード機能
const preloadComponents = async () => {
// ユーザーの操作を予測してプリロード
if (window.innerWidth > 768) {
await import('./DesktopModal.vue');
} else {
await import('./MobileModal.vue');
}
};
まとめ
Vue 3 の Teleport と Suspense は、モダンな Web アプリケーション開発において、これまで困難だった問題を解決する強力な機能です。
重要なポイント
機能 | 主な用途 | 解決する問題 |
---|---|---|
Teleport | モーダル、ツールチップ、ポップアップ | z-index 問題、レイアウト制約 |
Suspense | 非同期コンポーネント、データ読み込み | ローディング状態管理、UX 向上 |
これらの機能を組み合わせることで、以下のような価値を提供できます:
- 開発効率の向上:boilerplate コードの削減
- ユーザー体験の改善:スムーズなローディング、直感的な UI
- 保守性の向上:統一されたパターン、エラーハンドリング
- パフォーマンスの最適化:効率的なレンダリング、メモリ使用量の削減
今後の学習のために
Vue 3 の Teleport と Suspense をマスターすることで、より高品質なアプリケーションを開発できるようになります。継続的な学習と実践を通じて、これらの機能を自在に活用できるようになることでしょう。
開発において新しい機能を学ぶ際は、まず基本的な概念を理解し、小さな例から始めて、徐々に複雑な実装に挑戦することが大切です。この記事が、あなたの Vue 3 開発の旅において、有用な道しるべとなることを願っています。
関連リンク
- review
今の自分に満足していますか?『持たざる者の逆襲 まだ何者でもない君へ』溝口勇児
- review
ついに語られた業界の裏側!『フジテレビの正体』堀江貴文が描くテレビ局の本当の姿
- review
愛する勇気を持てば人生が変わる!『幸せになる勇気』岸見一郎・古賀史健のアドラー実践編で真の幸福を手に入れる
- review
週末を変えれば年収も変わる!『世界の一流は「休日」に何をしているのか』越川慎司の一流週末メソッド
- review
新しい自分に会いに行こう!『自分の変え方』村岡大樹の認知科学コーチングで人生リセット
- review
科学革命から AI 時代へ!『サピエンス全史 下巻』ユヴァル・ノア・ハラリが予見する人類の未来