DEV Community

Cover image for Mastering SSR, Data Fetching, and No-JavaScript Rendering in Nuxt 4
Ahmed Niazy
Ahmed Niazy

Posted on

Mastering SSR, Data Fetching, and No-JavaScript Rendering in Nuxt 4

1. Introduction

Nuxt 4 is an incredibly powerful framework, especially with its SSR-first architecture powered by Nitro 4. However, many developers run into a frustrating issue:

Skeleton loaders remain visible forever, and real data never appears—especially when JavaScript is disabled.

This article is a complete deep-dive into:

  • How Nuxt 4 handles SSR
  • Why some pages fail when JavaScript is disabled
  • Why skeletons get stuck
  • How to design fully SSR-compatible pages
  • Debugging strategies
  • Correct use of useFetch()
  • Architecture best practices
  • Avoiding hydration pitfalls

This guide is based on real production problems and real solutions.


2. Understanding SSR in Nuxt 4

Nuxt 4 uses Nitro 4 for rendering.
SSR means:

  1. The server renders the page.
  2. useFetch() runs on the server.
  3. HTML is generated with data already included.
  4. Browser receives a fully rendered page.
  5. If JS is enabled, hydration happens.

If JavaScript is disabled, the page should still display complete content — if SSR is implemented correctly.


3. The Root Cause of Skeleton Loaders Never Disappearing

If your data fetching happens only on the client, you will see:

  • Skeleton forever
  • No real content
  • Empty state
  • Hydration errors

Most commonly caused by:

onMounted(async () => {
  data.value = await $fetch('/api/data')
})
Enter fullscreen mode Exit fullscreen mode

onMounted() never runs on the server, so SSR produces no content.


4. Client-Side Fetching vs Server-Side Fetching

❌ Client-side only:

onMounted(async () => {
  data.value = await $fetch('/api/data')
})
Enter fullscreen mode Exit fullscreen mode

✔ Server-side rendering with useFetch():

const { data, pending } = await useFetch('/api/data', {
  server: true
})
Enter fullscreen mode Exit fullscreen mode

Server fetching ensures:

  • Data loads before HTML is delivered
  • Skeleton disappears automatically
  • Page works without JavaScript

5. How Skeleton Loaders Work Internally

Nuxt skeleton loaders depend on pending.

<template>
  <Skeleton v-if="pending" />
  <Content v-else />
</template>
Enter fullscreen mode Exit fullscreen mode

If pending stays true, skeleton never leaves.

Reasons:

  • useFetch executes on the client
  • Hydration mismatch
  • Data not fetched before SSR
  • Wrong composable structure

6. Fixing Skeleton Issues With SSR-First Fetching

Correct SSR-fetching:

await useFetch('/api/data', {
  server: true,
  lazy: false
})
Enter fullscreen mode Exit fullscreen mode

Why these options matter:

  • server: true: forces SSR execution
  • lazy: false: ensures fetch runs immediately

7. The Architecture Mistake: Fetching Data Inside Components

Avoid this:

<!-- inside a component -->
<script setup>
const { data } = useFetch('/api/data')
</script>
Enter fullscreen mode Exit fullscreen mode

This breaks SSR because:

  • Each component re-fetches
  • Execution happens after hydration
  • SSR doesn’t know about the data

Correct pattern:

  • Fetch at the page level
  • Pass data as props

8. Building SSR-Compatible Composables

Bad composable:

export const useItems = () => useFetch('/api/items')
Enter fullscreen mode Exit fullscreen mode

Correct composable:

export const useItems = () => {
  return useFetch('/api/items', {
    server: true,
    lazy: false
  })
}
Enter fullscreen mode Exit fullscreen mode

9. Full SSR-Ready Page Example (Works Without JavaScript)

<script setup>
const { data: items, pending } = await useFetch('/api/items', {
  server: true,
  lazy: false
})
</script>

<template>
  <ItemSkeleton v-if="pending" />

  <div v-else>
    <ItemCard
      v-for="item in items"
      :key="item.id"
      :item="item"
    />
  </div>
</template>
Enter fullscreen mode Exit fullscreen mode

This page:

  • Renders instantly
  • Works without JavaScript
  • Avoids hydration mismatch
  • Displays content on first paint

10. Understanding Hydration and Why It Breaks UI State

Hydration attaches Vue to SSR-rendered HTML.

Hydration fails when:

  • SSR and client outputs don’t match
  • State differs between server and client
  • Data shape changes
  • Skeleton conditions differ

When hydration breaks → UI becomes stuck in loading state.


11. Debugging SSR Problems Like a Pro

Technique 1: Disable JavaScript

If the page breaks → your SSR is not fully implemented.

Technique 2: Verify server execution

console.log("Is server?", process.server)
Enter fullscreen mode Exit fullscreen mode

Technique 3: Check Nuxt DevTools

View SSR state flow.

Technique 4: View page source

If HTML doesn’t contain data → SSR didn’t run fetch.


12. Why useFetch() Sometimes Runs on the Client Only

Common reasons:

  • Key function depends on client-side values
  • Conditional rendering prevents SSR execution
  • Missing options: server: true
  • Using watchers to trigger fetch

Wrong pattern:

const id = ref(null)
useFetch(() => `/api/item/${id.value}`)
Enter fullscreen mode Exit fullscreen mode

SSR does not know id.


13. Nitro’s Role in SSR Data Handling

Nitro handles:

  • Server file execution
  • API routing
  • Caching
  • Lazy loading
  • Edge deployments

Understanding Nitro helps prevent:

  • Unnecessary client-fetches
  • Wrong caching behavior
  • Inconsistent SSR render

14. API Response Design for SSR Stability

Good SSR-friendly API example:

export default defineEventHandler(() => {
  return {
    success: true,
    data: [
      { id: 1, title: "Example A" },
      { id: 2, title: "Example B" }
    ]
  }
})
Enter fullscreen mode Exit fullscreen mode

Guidelines:

  • Consistent keys
  • Predictable object structures
  • Avoid deeply nested unpredictability

15. Avoid Suspense Pitfalls

Suspense can break SSR.

❌ Wrong:

<Suspense>
  <ChildComponent />
</Suspense>
Enter fullscreen mode Exit fullscreen mode

If child fetches → fallback shows forever when JS is disabled.

✔ Correct:

<Suspense>
  <ServerRenderedBlock />
  <template #fallback>
    <Skeleton />
  </template>
</Suspense>
Enter fullscreen mode Exit fullscreen mode

16. Managing Large Component Trees in SSR

Rules:

  • Fetch data only once (at page level)
  • Pass ready data downward
  • Avoid client-side lifecycle hooks for initial load
  • Make components stateless regarding initial data

17. Performance Benefits of SSR-First Design

SSR-first yields:

  • Faster LCP
  • Better SEO
  • Fully visible first paint
  • No empty content
  • Smooth hydration
  • Better performance on slow devices
  • Works without JavaScript entirely

18. SSR Testing Checklist

✔ Disable JavaScript
✔ Check HTML source for real data
✔ Ensure skeleton disappears
✔ Ensure no hydration warnings
✔ Confirm fetch runs on server
✔ Validate composables return SSR data


19. Best Practices Summary

  • Prefer SSR fetching always
  • Avoid data fetching inside components
  • Avoid onMounted for initial data
  • Use composables sensibly
  • Ensure skeleton logic depends on SSR pending
  • Test pages with JS disabled
  • Ensure API responses are predictable
  • Avoid hydration mismatches

20. Final Exam (Great for Dev.to Readers)

Q1: Why do skeleton loaders remain visible when JavaScript is disabled?

Q2: Convert this into SSR-compatible code:

onMounted(async () => {
  list.value = await $fetch('/api/list')
})
Enter fullscreen mode Exit fullscreen mode

Q3: Write an SSR-first composable using useFetch.

Q4: Describe what causes hydration mismatch errors.

Q5: Build a skeleton fallback pattern that works without JavaScript.


Conclusion

SSR in Nuxt 4 is powerful — but only when you understand how to use it correctly.
By adopting an SSR-first architecture, eliminating client-only data fetching, designing stable composables, and structuring your UI for hydration safety, your application becomes:

  • Faster
  • More stable
  • SEO-optimized
  • Accessible
  • Fully functional with JavaScript disabled
  • Production-ready

This guide provides the deep technical understanding needed to achieve that level of reliability in Nuxt 4.

Top comments (0)