brand logo

ドキュメント

ClientOnly 実例:SSRに弱いウィジェットを安全に描画する

Nuxt の <ClientOnly> を使って、クライアントサイドでのみレンダリングすべきコンポーネントを安全に表示する実例集。fallback/placeholder と #fallback スロットの使い分けも確認できます。

<ClientOnly> は、ブラウザ API に依存して SSR で失敗しやすいコンポーネントを「クライアントでだけ」レンダリングするためのユーティリティです。ここでは最小構成から、fallbackTag / fallback#fallback スロットの使い方、簡単なウィジェット例までをまとめます。

何を作るか

  • SSR 時はプレースホルダーだけを出し、クライアントで中身をマウント
  • fallbackTag / fallback#fallback スロットの両方を確認
  • window を参照する軽量チャート風ウィジェットを <ClientOnly> 内で安全に描画

基本:props でプレースホルダーを出す

fallbackTagfallback を指定すると、サーバーレンダリング時にその要素・テキストが表示され、クライアントで <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>

関連ドキュメント