Skip to content

Commit 68dc25c

Browse files
feat: add hydration content. (#281)
Co-authored-by: KotaFujishiro <>
1 parent b59a9f0 commit 68dc25c

File tree

6 files changed

+376
-0
lines changed

6 files changed

+376
-0
lines changed
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<script setup lang="ts">
2+
const timestamp = Date.now() // ❌ これが問題!サーバーとクライアントで異なる値になる
3+
4+
console.log('timestamp:', timestamp)
5+
</script>
6+
7+
<template>
8+
<div class="p-4">
9+
<h1 class="mb-4 text-2xl font-bold">
10+
ハイドレーションの確認
11+
</h1>
12+
13+
<div class="mb-4 rounded border border-red-300 bg-red-50 p-4">
14+
<p class="mb-2 font-bold text-red-700">
15+
⚠️ ハイドレーションエラーが発生しています
16+
</p>
17+
<p class="text-sm text-red-600">
18+
ブラウザの開発者ツール(F12)→ Consoleタブで警告を確認してください
19+
</p>
20+
</div>
21+
22+
<div class="mb-4 rounded border border-gray-300 bg-gray-50 p-4">
23+
<p class="mb-2 font-bold">
24+
現在のタイムスタンプ
25+
</p>
26+
<p class="text-sm text-gray-600">
27+
Timestamp: {{ timestamp }}
28+
</p>
29+
</div>
30+
31+
<!-- TODO: 上記のtimestamp表示を削除して、BrowserOnlyコンポーネントを使って安全に表示してください -->
32+
</div>
33+
</template>
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<script setup lang="ts">
2+
// TODO: タイムスタンプを保持するrefを作成(初期値は0)
3+
4+
// TODO: onMountedでDate.now()を設定
5+
// ヒント: onMounted(() => { ... })
6+
</script>
7+
8+
<template>
9+
<div class="rounded border border-green-300 bg-green-50 p-4">
10+
<p class="mb-2 font-bold text-green-700">
11+
✅ ハイドレーションエラーが修正されました!
12+
</p>
13+
<!-- TODO: タイムスタンプを表示 -->
14+
<!-- ヒント: <p class="text-sm text-green-600">Timestamp: {{ timestamp }}</p> -->
15+
</div>
16+
</template>
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import type { GuideMeta } from '~/types/guides'
2+
3+
export const meta: GuideMeta = {
4+
startingFile: 'app.vue',
5+
features: {
6+
fileTree: true,
7+
terminal: true,
8+
},
9+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<script setup lang="ts">
2+
</script>
3+
4+
<template>
5+
<div class="p-4">
6+
<h1 class="mb-4 text-2xl font-bold">
7+
ハイドレーションの確認
8+
</h1>
9+
10+
<BrowserOnly />
11+
</div>
12+
</template>
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<script setup lang="ts">
2+
// 初期値は0(サーバーサイドでは値を持たないため)
3+
const timestamp = ref(0)
4+
5+
// クライアントサイドでのみ実行される
6+
onMounted(() => {
7+
timestamp.value = Date.now()
8+
})
9+
</script>
10+
11+
<template>
12+
<div class="rounded border border-green-300 bg-green-50 p-4">
13+
<p class="mb-2 font-bold text-green-700">
14+
✅ ハイドレーションエラーが修正されました!
15+
</p>
16+
<p class="text-sm text-green-600">
17+
Timestamp: {{ timestamp }}
18+
</p>
19+
</div>
20+
</template>
Lines changed: 286 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,286 @@
1+
---
2+
ogImage: true
3+
---
4+
5+
# ハイドレーション
6+
7+
Nuxtのデフォルトはユニバーサルレンダリング(SSR)です。\
8+
これは、コンポーネントが2回実行されることを意味します。
9+
10+
1. **サーバーサイド**: HTMLを生成
11+
2. **クライアントサイド**: HTMLにJavaScriptの機能を付加(ハイドレーション)
12+
13+
この「2回実行される」という特性を理解しておくことが、Nuxtアプリケーション開発では非常に重要です。
14+
15+
## ハイドレーションとは
16+
17+
ハイドレーションとは、サーバー側で生成された静的なHTMLに、クライアント側でVueのリアクティビティシステムやイベントリスナーを付与するプロセスです。
18+
19+
プロセスは以下のステップで進行します:
20+
21+
### ステップ1(サーバー)
22+
23+
- Vueコンポーネントが実行される
24+
- HTMLが生成される: `<p>Count: 0</p><button>Increment</button>`
25+
- このHTMLがブラウザに送信される
26+
27+
### ステップ2(クライアント)
28+
29+
- ブラウザがHTMLを表示(この時点ではただのHTML、ボタンは動かない)
30+
- JavaScriptがロードされる
31+
- Vueが再度実行される
32+
- 既存のHTMLにイベントリスナーなどを「ハイドレート(付加)」する
33+
- ボタンがクリック可能になる
34+
35+
## ハイドレーションエラーを体験する
36+
37+
右のプレビューを見てください。ブラウザの開発者ツール(F12キー)を開いて、**Console**タブを確認しましょう。
38+
39+
**警告が表示されているはずです**
40+
41+
```
42+
Hydration completed but contains mismatches.
43+
```
44+
45+
これが「ハイドレーションミスマッチ」エラーです。なぜこのエラーが発生しているのでしょうか?
46+
47+
`app.vue` のコードを見てみましょう:
48+
49+
```vue
50+
<script setup>
51+
const timestamp = Date.now() // ❌ これが問題!
52+
53+
console.log('timestamp:', timestamp)
54+
</script>
55+
56+
<template>
57+
<div>
58+
<p>Timestamp: {{ timestamp }}</p>
59+
</div>
60+
</template>
61+
```
62+
63+
### 何が問題なのか?
64+
65+
1. **サーバーサイド**(下のTerminalタブを確認):
66+
67+
- `Date.now()`が実行され、その時点のタイムスタンプが生成される
68+
- 例: `timestamp: 1729641234567`
69+
70+
2. **クライアントサイド**(ブラウザのConsoleタブ):
71+
- 数ミリ秒後に`Date.now()`が再度実行される
72+
- 例: `timestamp: 1729641234892`
73+
74+
サーバーとクライアントで**実行時刻が異なる**ため、**異なる値**になり、ハイドレーションミスマッチが発生します。
75+
76+
### なぜ深刻なのか?
77+
78+
ハイドレーションエラーは単なる警告ではありません:
79+
80+
- ⚠️ パフォーマンス低下(コンポーネント全体を再レンダリング)
81+
- ⚠️ ボタンなどのイベントリスナーが動作しない可能性
82+
- ⚠️ 予期しない表示の崩れ
83+
84+
これを修正する方法を次のチャレンジで学びましょう。
85+
86+
## ハイドレーションミスマッチ
87+
88+
サーバーで生成したHTMLとクライアントで生成したHTMLが異なる場合、「ハイドレーションミスマッチ」エラーが発生します。
89+
90+
ハイドレーションミスマッチは単なる警告ではありません。以下のような深刻な問題を引き起こします:
91+
92+
- **パフォーマンス**: インタラクティブになるまでの時間が増加
93+
- **ユーザー体験**: コンテンツのちらつき
94+
- **機能**: イベントリスナーが正しく動作しない
95+
- **SEO**: 検索エンジンとユーザーが異なるコンテンツを見る可能性
96+
97+
### よくあるミスマッチの原因
98+
99+
#### 1. ブラウザ専用APIの使用
100+
101+
**悪い例**:
102+
103+
```vue
104+
<script setup>
105+
// サーバーにはwindowが存在しない!
106+
const width = window.innerWidth
107+
</script>
108+
109+
<template>
110+
<p>Width: {{ width }}</p>
111+
</template>
112+
```
113+
114+
**良い例**:
115+
116+
```vue
117+
<script setup>
118+
const width = ref(0)
119+
120+
onMounted(() => {
121+
// onMountedはクライアントでのみ実行される
122+
width.value = window.innerWidth
123+
})
124+
</script>
125+
126+
<template>
127+
<p>Width: {{ width }}</p>
128+
</template>
129+
```
130+
131+
#### 2. 時刻ベースのコンテンツ
132+
133+
**悪い例**:
134+
135+
```vue
136+
<script setup>
137+
// サーバーとクライアントで実行時刻が異なる
138+
const now = new Date().toISOString()
139+
</script>
140+
141+
<template>
142+
<p>{{ now }}</p>
143+
</template>
144+
```
145+
146+
**良い例**:
147+
148+
```vue
149+
<script setup>
150+
// useStateでサーバーの値をクライアントに転送
151+
const now = useState('timestamp', () => new Date().toISOString())
152+
</script>
153+
154+
<template>
155+
<p>{{ now }}</p>
156+
</template>
157+
```
158+
159+
#### 3. ランダム値の使用
160+
161+
**悪い例**:
162+
163+
```vue
164+
<script setup>
165+
// サーバーとクライアントで異なる値になる
166+
const id = Math.random()
167+
</script>
168+
```
169+
170+
**良い例**:
171+
172+
```vue
173+
<script setup>
174+
// useStateで一貫性を保つ
175+
const id = useState('random-id', () => Math.random())
176+
</script>
177+
```
178+
179+
## SSR時のライフサイクル
180+
181+
`<script setup>` 内のコードは**サーバーでもクライアントでも実行**されますが、\
182+
その中の**ライフサイクルフックによって実行タイミングが異なります**
183+
184+
### SSR(サーバーサイド)で実行されるもの
185+
186+
- `<script setup>` のトップレベルコード(`ref()``reactive()` など)
187+
- `useAsyncData()` / `useFetch()`
188+
- `onServerPrefetch()` などのサーバー専用フック
189+
190+
### CSR(クライアントサイド)でのみ実行されるもの
191+
192+
- `onBeforeMount()` / `onMounted()`
193+
- `onBeforeUpdate()` / `onUpdated()`
194+
- `onBeforeUnmount()` / `onUnmounted()`
195+
196+
```typescript
197+
// ⭕ サーバーでもクライアントでも実行される
198+
const count = ref(0)
199+
200+
// ⭕ サーバーでのみ実行される
201+
onServerPrefetch(async () => {
202+
// サーバーサイドでのデータ取得
203+
})
204+
205+
// ❌ サーバーでは実行されない(クライアントのみ)
206+
onMounted(() => {
207+
console.log('mounted')
208+
})
209+
```
210+
211+
つまり、**ブラウザ専用のAPI(window, document, localStorageなど)は、クライアントサイドでのみ実行されることを保証する必要があります**
212+
213+
代表的な方法:
214+
215+
- `onMounted()` 内で使用する
216+
- `<ClientOnly>` コンポーネントで囲む
217+
- `import.meta.client` でガードする
218+
- `.client.vue` ファイルを使用する
219+
220+
## ClientOnlyコンポーネント
221+
222+
クライアントサイドでのみレンダリングしたいコンポーネントには、`<ClientOnly>` コンポーネントを使用できます。
223+
224+
```vue
225+
<template>
226+
<div>
227+
<p>これはサーバーでもクライアントでも表示される</p>
228+
229+
<ClientOnly>
230+
<p>これはクライアントでのみ表示される</p>
231+
<!-- ここではwindowやdocumentを安全に使える -->
232+
</ClientOnly>
233+
</div>
234+
</template>
235+
```
236+
237+
`<ClientOnly>` 内のコンテンツは:
238+
239+
- サーバーサイドレンダリングではスキップされる
240+
- クライアントサイドでのみハイドレートされる
241+
- ハイドレーションミスマッチを防げる
242+
243+
## チャレンジ:ハイドレーションエラーを修正しよう
244+
245+
現在、`app.vue`でハイドレーションエラーが発生しています。\
246+
これを修正するために、**`timestamp`を安全に表示する方法**を実装しましょう!
247+
248+
### 課題
249+
250+
以下の2つのファイルを編集して、ハイドレーションエラーを解決してください:
251+
252+
**1. `components/BrowserOnly.vue`を完成させる**
253+
254+
- TODOコメントを埋めて、`timestamp``onMounted`を使って安全に取得・表示
255+
- タイムスタンプを保持する`ref`を用意(初期値は`0`
256+
- `onMounted`内で`Date.now()`を設定
257+
258+
**2. `app.vue`を修正する**
259+
260+
- 問題のある`timestamp`関連のコード(script部分とtemplate部分)を削除
261+
- TODOコメント部分に`<BrowserOnly />`コンポーネントを追加
262+
263+
### ヒント
264+
265+
- `onMounted`はクライアントサイドでのみ実行される
266+
- 初期値を`0`にすることで、サーバーとクライアントの初期状態を一致させる
267+
- `Date.now()``onMounted`内で実行すれば、サーバーでは実行されない
268+
269+
### 完成すると...
270+
271+
✅ ブラウザの開発者ツールのConsoleでハイドレーション警告が消えます\
272+
✅ 緑色の成功メッセージと共に、タイムスタンプが安全に表示されます
273+
274+
もし行き詰まったら、以下のボタンかエディタの右上にあるボタンをクリックして回答を見ることができます。
275+
276+
:ButtonShowSolution{.bg-faded.px4.py2.mb3.rounded.border.border-base.hover:bg-active.hover:text-primary.hover:border-primary:50}
277+
278+
## まとめ
279+
280+
ハイドレーションで重要なポイント:
281+
282+
1. **コンポーネントは2回実行される**(サーバー + クライアント)
283+
2. **ライフサイクルフックはクライアントのみ**`onMounted` など)
284+
3. **ブラウザAPIは `onMounted` 内で使用**
285+
4. **サーバーとクライアントで同じ値を保つ**(useState使用)
286+
5. **`<ClientOnly>` でクライアント専用コンテンツを分離**

0 commit comments

Comments
 (0)