Hydration
Nuxt's default is universal rendering (SSR).
This means components execute twice:
- Server-side: Generate HTML
- Client-side: Add JavaScript functionality to HTML (hydration)
Understanding this "runs twice" characteristic is crucial for Nuxt application development.
What is Hydration?
Hydration is the process of attaching Vue's reactivity system and event listeners on the client side to the static HTML generated on the server side.
The process follows these steps:
Step 1 (Server)
- Vue component executes
- HTML is generated:
<p>Count: 0</p><button>Increment</button> - This HTML is sent to the browser
Step 2 (Client)
- Browser displays the HTML (at this point it's just HTML, buttons don't work)
- JavaScript loads
- Vue executes again
- Event listeners are "hydrated (attached)" to the existing HTML
- Buttons become clickable
Experience a Hydration Error
Look at the preview on the right. Open the browser's developer tools (F12 key) and check the Console tab.
You should see a warning:
Hydration completed but contains mismatches.
This is a "hydration mismatch" error. Why is this error occurring?
Let's look at the code in app.vue:
<script setup>
const timestamp = Date.now() // ❌ This is the problem!
console.log('timestamp:', timestamp)
</script>
<template>
<div>
<p>Timestamp: {{ timestamp }}</p>
</div>
</template>
What's the Problem?
- Server-side (check the Terminal tab below):
Date.now()executes and generates a timestamp at that moment- Example:
timestamp: 1729641234567
- Client-side (browser's Console tab):
Date.now()executes again a few milliseconds later- Example:
timestamp: 1729641234892
Since the execution time differs between server and client, they get different values, causing a hydration mismatch.
Why is This Serious?
Hydration errors are not just warnings:
- ⚠️ Performance degradation (entire component re-renders)
- ⚠️ Event listeners like buttons may not work
- ⚠️ Unexpected display issues
Let's learn how to fix this in the next challenge.
Hydration Mismatch
When the HTML generated on the server differs from the HTML generated on the client, a "hydration mismatch" error occurs.
Hydration mismatch is not just a warning. It causes serious problems:
- Performance: Increased time to become interactive
- User Experience: Content flickering
- Functionality: Event listeners don't work correctly
- SEO: Search engines and users may see different content
Common Causes of Mismatch
1. Using Browser-Only APIs
❌ Bad Example:
<script setup>
// window doesn't exist on the server!
const width = window.innerWidth
</script>
<template>
<p>Width: {{ width }}</p>
</template>
✅ Good Example:
<script setup>
const width = ref(0)
onMounted(() => {
// onMounted only executes on the client
width.value = window.innerWidth
})
</script>
<template>
<p>Width: {{ width }}</p>
</template>
2. Time-Based Content
❌ Bad Example:
<script setup>
// Execution time differs between server and client
const now = new Date().toISOString()
</script>
<template>
<p>{{ now }}</p>
</template>
✅ Good Example:
<script setup>
// useState transfers server value to client
const now = useState('timestamp', () => new Date().toISOString())
</script>
<template>
<p>{{ now }}</p>
</template>
3. Using Random Values
❌ Bad Example:
<script setup>
// Different values on server and client
const id = Math.random()
</script>
✅ Good Example:
<script setup>
// useState maintains consistency
const id = useState('random-id', () => Math.random())
</script>
SSR Lifecycle
Code inside <script setup> executes both on server and client,
but execution timing differs based on lifecycle hooks.
What Runs on SSR (Server-Side)
- Top-level code in
<script setup>(ref(),reactive(), etc.) useAsyncData()/useFetch()- Server-specific hooks like
onServerPrefetch()
What Runs Only on CSR (Client-Side)
onBeforeMount()/onMounted()onBeforeUpdate()/onUpdated()onBeforeUnmount()/onUnmounted()
// ⭕ Runs on both server and client
const count = ref(0)
// ⭕ Runs only on server
onServerPrefetch(async () => {
// Server-side data fetching
})
// ❌ Doesn't run on server (client only)
onMounted(() => {
console.log('mounted')
})
This means browser-specific APIs (window, document, localStorage, etc.) need to be guaranteed to execute only on the client side.
Common methods:
- Use inside
onMounted() - Wrap with
<ClientOnly>component - Guard with
import.meta.client - Use
.client.vuefiles
ClientOnly Component
For components that should only render on the client side, use the <ClientOnly> component.
<template>
<div>
<p>This displays on both server and client</p>
<ClientOnly>
<p>This only displays on client</p>
<!-- window and document can be safely used here -->
</ClientOnly>
</div>
</template>
Content inside <ClientOnly>:
- Skipped during server-side rendering
- Only hydrated on client side
- Prevents hydration mismatch
Challenge: Fix the Hydration Error
Currently, a hydration error is occurring in app.vue.
Let's implement a safe way to display the timestamp to fix this!
Task
Edit the following two files to resolve the hydration error:
1. Complete components/BrowserOnly.vue
- Fill in the TODO comments to safely retrieve and display
timestampusingonMounted - Create a
refto hold the timestamp (initial value:0) - Set
Date.now()insideonMounted
2. Fix app.vue
- Remove the problematic
timestamp-related code (both script and template parts) - Add the
<BrowserOnly />component at the TODO comment location
Hints
onMountedonly executes on the client side- Setting initial value to
0makes server and client initial state match - Executing
Date.now()insideonMountedmeans it won't run on the server
When Complete...
✅ The hydration warning will disappear from the browser's developer tools Console
✅ The timestamp will display safely along with a green success message
If you get stuck, click the button below or the button in the top-right of the editor to see the solution.
Summary
Key points about hydration:
- Components execute twice (server + client)
- Lifecycle hooks are client-only (
onMounted, etc.) - Use browser APIs inside
onMounted - Keep same values on server and client (use useState)
- Separate client-only content with
<ClientOnly>