Skip to content

Server Side Rendering (SSR)

The Vue ReActiveRecord plugin is designed to work seamlessly in Server-Side Rendering (SSR) environments like Nuxt, Quasar SSR, or custom Vue SSR applications. The plugin automatically detects when it's running in an SSR context and provides safe stub implementations that prevent errors while maintaining API compatibility.

Hydration and SSR Safety: Use onReActiveDatabaseReady

To ensure hydration safety and prevent SSR/client mismatches, always use the onReActiveDatabaseReady composable for any logic that queries or mutates the database in a Vue component. This guarantees that your code only runs after the real database is ready in the browser, and is a no-op in SSR (where stub data is used).

Recommended pattern:

vue
<script setup lang="ts">
import { ref } from 'vue'
import { useReActiveModel, onReActiveDatabaseReady } from '@nhtio/vue-re-active-record'

const User = useReActiveModel('user')
const users = ref([])

onReActiveDatabaseReady(async () => {
  users.value = await User.all()
})
</script>
  • This pattern is hydration-safe and SSR-safe.
  • Do not run queries directly in onMounted or outside of a lifecycle hook.
  • onReActiveDatabaseReady will await the real database in the browser, and is a no-op in SSR.

How SSR Detection Works

The plugin uses internal environment detection to determine whether it's running in a browser or server environment:

typescript
import { isWebContext } from '@nhtio/vue-re-active-record'

// Internal plugin logic
if (isWebContext()) {
  // Browser mode - use real ReactiveDatabase with IndexedDB
  // ...
} else {
  // SSR mode - use safe stub implementations
  // ...
}

When the plugin detects it's running in an SSR environment, it automatically:

  1. Shows a warning in development mode to inform you SSR stubs are active
  2. Provides stub implementations that mimic the real API but don't perform actual database operations
  3. Maintains type safety - your TypeScript code works the same in both environments

Safe Integration in SSR

Basic Setup

The plugin integrates the same way in SSR as it does in the browser. Use the default export for plugin installation:

typescript
// plugins/vue-re-active-record.ts (Nuxt example)
import VueReActiveRecord from '@nhtio/vue-re-active-record'

export default defineNuxtPlugin((nuxtApp) => {
  nuxtApp.vueApp.use(VueReActiveRecord, {
    models: {
      user: {
        schema: '++id, name, email, created_at',
        primaryKey: 'id',
        relationships: {},
      },
      post: {
        schema: '++id, title, content, user_id, created_at',
        primaryKey: 'id',
        relationships: {
          user: { type: 'belongsTo', model: 'user', foreignKey: 'user_id' }
        },
      }
    }
  })
})

Options API Support

You can enable Options API support (for Vue 2/3) by passing { useOptionsAPI: true } as a third argument:

typescript
app.use(VueReActiveRecord, { /* config */ }, { useOptionsAPI: true })

This adds $reactiveDatabase and $useReActiveModel to your Options API components.

Initialization and Composables

The plugin provides composables for accessing the database and models, and for controlling initialization:

  • useReActiveDatabase() — Get the current database instance (SSR stub or real)
  • useReActiveModel(modelName) — Get a model constructor for the given model name
  • onReActiveDatabaseReady(callback) — Run logic after the real database is ready (hydration-safe, SSR-safe)
  • awaitPluginInit() — Wait for the real database to be ready (resolves immediately in SSR)
  • initializeReActiveDatabase(config) — Programmatically initialize the plugin and wait for readiness

Example:

typescript
import { useReActiveModel, onReActiveDatabaseReady } from '@nhtio/vue-re-active-record'

onReActiveDatabaseReady(async () => {
  const users = await useReActiveModel('user').all()
  // ...
})

Component Usage

Your Vue components work identically in both SSR and client environments:

vue
<template>
  <div>
    <div v-if="users.length">
      <div v-for="user in users" :key="user.id">
        {{ user.name }} ({{ user.email }})
      </div>
    </div>
    <div v-else>No users found</div>
  </div>
</template>

<script setup lang="ts">
import { ref } from 'vue'
import { useReActiveModel, onReActiveDatabaseReady } from '@nhtio/vue-re-active-record'

const User = useReActiveModel('user')
const users = ref([])

onReActiveDatabaseReady(async () => {
  users.value = await User.all()
})
</script>

How the Plugin Handles SSR

Automatic Stub Creation

When in SSR mode, the plugin creates stub implementations that:

Database Stubs

  • ready: Always returns false
  • models: Returns the configured model names
  • model(name): Returns stub model constructors
  • Database operations: Throw descriptive errors
typescript
// SSR behavior
import { useReActiveDatabase } from '@nhtio/vue-re-active-record'
const db = useReActiveDatabase()
console.log(db.ready) // false
console.log(db.models) // ['user', 'post'] - from your config

Model Stubs

  • Constructor: Accepts initial data safely
  • Properties: Reactive getters/setters that work with Vue
  • Query methods: Return empty results or safe defaults
  • Modification methods: Throw descriptive errors
typescript
// SSR behavior
import { useReActiveModel } from '@nhtio/vue-re-active-record'
const User = useReActiveModel('user')

// Safe operations (return empty/default values)
const users = await User.all()        // []
const user = await User.first()       // undefined
const count = await User.query().count() // 0

// Operations that would modify data (throw errors)
await User.create({ name: 'John' })   // Throws: "Cannot create records in SSR mode"

Query Builder Stubs

  • Chainable methods: Return this for fluent API
  • Terminal methods: Return safe defaults (empty arrays, undefined, 0)
  • Reactive methods: Return Vue reactive objects with stub data
typescript
// SSR behavior - all chainable
const query = User.query()
  .where('active', true)
  .orderBy('created_at', 'desc')
  .limit(10)

const results = await query.fetch() // []
const first = await query.first()   // undefined

Type Safety in SSR

The stubs maintain full TypeScript compatibility:

typescript
// This code works identically in SSR and browser
const User = useReActiveModel('user')

// TypeScript knows this returns User[] | undefined
const users: User[] = await User.all()

// TypeScript knows this returns a query builder
const query = User.query().where('name', 'John')

// All your model properties are properly typed
const user = new User({ name: 'Jane', email: 'jane@example.com' })
console.log(user.name)  // TypeScript knows this is a string

Gotchas and Potential Issues

1. Data Hydration Mismatches

Problem: SSR renders with empty data, client loads real data, causing hydration mismatches. This is especially likely if you run queries before the plugin is fully initialized in the browser.

Solution: Always use loading states and onReActiveDatabaseReady to prevent hydration mismatches, and always run queries inside onReActiveDatabaseReady (or use awaitPluginInit/initializeReActiveDatabase if you need lower-level control). This ensures the real database is ready in the browser and is a no-op in SSR.

vue
<template>
  <div>
    <!-- Consistent loading state prevents hydration mismatches -->
    <div v-if="!isMounted">Loading tasks...</div>
    <div v-else-if="tasks.length">
      <div v-for="task in tasks" :key="task.id">
        <!-- ... -->
      </div>
    </div>
    <div v-else>No tasks found</div>
  </div>
</template>

<script setup lang="ts">
import { ref, onBeforeUnmount, computed } from 'vue'
import { useReActiveModel, onReActiveDatabaseReady } from '@nhtio/vue-re-active-record'
import type { VueReactiveQueryCollection } from '@nhtio/vue-re-active-record'

const Task = useReActiveModel('task')
const collection = ref<VueReactiveQueryCollection<'task'> | undefined>(undefined)
const processing = ref<string[]>([])
const isMounted = ref(false)

const tasks = computed(() => collection.value?.$value || [])

onReActiveDatabaseReady(async () => {
  isMounted.value = true
  const col = await Task
    .query()
    .orderBy('completed', 'asc')
    .orderBy('priority', 'desc')
    .reactive()
    .fetch()
  collection.value = col as VueReactiveQueryCollection<'task'>
})

onBeforeUnmount(() => {
  if (collection.value) {
    collection.value.unmount()
  }
})
// ...
</script>

Key Points:

  • Always use isMounted flag to ensure consistent rendering
  • Always use onReActiveDatabaseReady before running queries for hydration safety
  • Load data only in the callback to guarantee client-side execution
  • Clean up reactive collections in onBeforeUnmount
  • Use safe getters for reactive collections that might be undefined

2. Assuming Database Operations Work

Problem: Code that assumes database operations succeed in SSR, or that queries can run before the plugin is initialized in the browser.

typescript
// ❌ Problematic - will throw in SSR or may fail if plugin is not ready
const createUser = async () => {
  const user = await User.create({ name: 'John' })
  router.push(`/users/${user.id}`)
}

// ✅ Better - handle SSR gracefully and ensure plugin is ready
import { onReActiveDatabaseReady, useReActiveModel } from '@nhtio/vue-re-active-record'
onReActiveDatabaseReady(async () => {
  const User = useReActiveModel('user')
  const user = await User.create({ name: 'John' })
  router.push(`/users/${user.id}`)
})

3. Environment-Specific Code

Problem: Code that should only run in browser being executed in SSR.

Solution: Use proper guards:

typescript
import { isWebContext, onReActiveDatabaseReady } from '@nhtio/vue-re-active-record'

// ✅ Safe approach
if (isWebContext()) {
  onReActiveDatabaseReady(async () => {
    // Browser-only code
    const users = await User.all()
    setupRealTimeSync()
  })
}

// Or in components
onReActiveDatabaseReady(async () => {
  // This only runs on client after plugin is ready
  loadInitialData()
})

4. Reactive Queries in SSR

Problem: Reactive queries might behave unexpectedly in SSR.

Solution: Understand that reactive queries return Vue reactive objects with stub data:

typescript
// SSR behavior
const reactiveUsers = await User.query().reactive().fetch()
console.log(reactiveUsers.value) // [] (empty array)
console.log(reactiveUsers.$value) // [] (empty array)

// The reactive object exists but contains no real data
reactiveUsers.on('next', () => {
  // This won't fire in SSR
})

5. DevTools Integration

The plugin's DevTools integration is automatically disabled in SSR environments. If you need to debug SSR behavior, use console logging:

typescript
import { useReActiveDatabase } from '@nhtio/vue-re-active-record'

const db = useReActiveDatabase()
console.log('Database ready:', db.ready) // false in SSR
console.log('Available models:', db.models) // Your configured models

Framework-Specific Notes

Nuxt 3

The plugin works seamlessly with Nuxt 3's universal rendering:

typescript
// plugins/vue-re-active-record.ts - Universal (recommended)
import VueReActiveRecord from '@nhtio/vue-re-active-record'

export default defineNuxtPlugin((nuxtApp) => {
  nuxtApp.vueApp.use(VueReActiveRecord, {
    models: {
      // Your model configurations
    }
  })
})

For client-only plugin registration:

typescript
// plugins/vue-re-active-record.client.ts - Client only
import VueReActiveRecord from '@nhtio/vue-re-active-record'

export default defineNuxtPlugin((nuxtApp) => {
  nuxtApp.vueApp.use(VueReActiveRecord, {
    models: {
      // Your model configurations  
    }
  })
})

Best Practices

1. Hydration Safety

Always prevent hydration mismatches by using consistent rendering patterns:

vue
<template>
  <div>
    <!-- Always show loading state initially -->
    <div v-if="!isMounted">Loading...</div>
    <div v-else-if="hasData">
      <!-- Your data-dependent UI -->
    </div>
    <div v-else>
      <!-- Empty state -->
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref, onMounted } from 'vue'

const isMounted = ref(false)
const hasData = ref(false)

onMounted(async () => {
  isMounted.value = true
  // Load data and update hasData accordingly
})
</script>

2. Client-Side Data Loading

Use onReActiveDatabaseReady for all database operations to ensure client-side execution:

typescript
// ✅ Safe - runs only on client
onReActiveDatabaseReady(async () => {
  const users = await User.all()
  // Process users...
})

// ❌ Dangerous - might run during SSR
const users = await User.all()

3. Error Handling

Gracefully handle SSR errors when database operations are attempted:

typescript
const saveData = async () => {
  try {
    await User.create(userData.value)
    // Success handling
  } catch (error) {
    if (error.message.includes('SSR mode')) {
      // Queue for client-side or show appropriate message
      console.warn('Database operations not available during SSR')
    } else {
      // Handle actual errors
      throw error
    }
  }
}

4. Environment Detection

Use proper guards for browser-specific code:

typescript
import { isWebContext } from '@nhtio/vue-re-active-record'

// ✅ Safe approach
if (isWebContext()) {
  onReActiveDatabaseReady(async () => {
    // Browser-only code
    setupWebSocketConnection()
    initializeServiceWorker()
  })
}

5. Development and Testing

  • Monitor console warnings during development to identify SSR issues
  • Test both SSR and client builds to ensure compatibility
  • Use the plugin's DevTools in browser environments for debugging
  • Verify hydration behavior in different frameworks (Nuxt, Quasar, etc.)

6. Performance Considerations

  • Clean up reactive collections in onBeforeUnmount to prevent memory leaks
  • Use computed properties for derived data instead of watchers when possible
  • Leverage the stub implementations for server-side performance benefits

The Vue ReActiveRecord plugin's SSR compatibility ensures your application works seamlessly across environments while maintaining type safety and API consistency.