ClientOnly 実例:SSRに弱いウィジェットを安全に描画する
Nuxt の <ClientOnly> を使って、クライアントサイドでのみレンダリングすべきコンポーネントを安全に表示する実例集。fallback/placeholder と #fallback スロットの使い分けも確認できます。
<ClientOnly> は、ブラウザ API に依存して SSR で失敗しやすいコンポーネントを「クライアントでだけ」レンダリングするためのユーティリティです。ここでは最小構成から、fallbackTag / fallback、#fallback スロットの使い方、簡単なウィジェット例までをまとめます。
何を作るか
- SSR 時はプレースホルダーだけを出し、クライアントで中身をマウント
fallbackTag/fallbackと#fallbackスロットの両方を確認windowを参照する軽量チャート風ウィジェットを<ClientOnly>内で安全に描画
基本:props でプレースホルダーを出す
fallbackTag と fallback を指定すると、サーバーレンダリング時にその要素・テキストが表示され、クライアントで <ClientOnly> がマウントされると子要素に差し替わります。
pages/example.vue
<template>
<div>
<Sidebar />
<!-- Comments はクライアントサイドでのみレンダリング -->
<ClientOnly fallbackTag="span" fallback="コメントを読み込んでいます...">
<Comments />
</ClientOnly>
</div>
</template>
応用:#fallback スロットで柔軟に
よりリッチなプレースホルダーを出したい場合は #fallback を使います。SSR 時はこのスロットが表示され、クライアントで <ClientOnly> がマウントされると子要素に切り替わります。
pages/example.vue
<template>
<div>
<Sidebar />
<ClientOnly fallbackTag="div">
<!-- これはクライアントサイドでのみレンダリング -->
<Comments />
<template #fallback>
<!-- これはサーバーサイドでレンダリング -->
<p>コメントを準備中です…</p>
</template>
</ClientOnly>
</div>
</template>
実例:ブラウザ API に依存するウィジェット
window / document を参照するようなコードは SSR 中にエラーになりがちです。以下は簡単な「幅を表示するウィジェット」を <ClientOnly> で包んで安全に描画する例です。
<template>
<div class="box">
<p>現在のビューポート幅: <strong>{{ width }}</strong> px</p>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, onBeforeUnmount } from 'vue'
const width = ref<number | null>(null)
function sync() {
// SSR 中は window が未定義なので onMounted の中でのみ参照する
width.value = window.innerWidth
}
function onResize() { sync() }
onMounted(() => {
sync()
window.addEventListener('resize', onResize, { passive: true })
})
onBeforeUnmount(() => {
window.removeEventListener('resize', onResize)
})
</script>
<style scoped>
.box {
padding: 12px; border-radius: 8px;
border: 1px solid #e5e7eb; background: #f9fafb;
}
</style>
補足:マウント後に要素へアクセスする
<ClientOnly> 内の要素はクライアントでマウントされた後にのみ存在します。テンプレートリファレンスを監視すれば、要素の用意ができたタイミングで処理を実行できます。
pages/after-mounted.vue
<script setup lang="ts">
const widgetRef = useTemplateRef('widgetRef')
watch(widgetRef, () => {
console.log('ウィジェットがマウントされました')
}, { once: true })
</script>
<template>
<ClientOnly>
<ViewportWidth ref="widgetRef" />
</ClientOnly>
</template>