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:
<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
onMountedor outside of a lifecycle hook. onReActiveDatabaseReadywill 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:
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:
- Shows a warning in development mode to inform you SSR stubs are active
- Provides stub implementations that mimic the real API but don't perform actual database operations
- 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:
// 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:
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 nameonReActiveDatabaseReady(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:
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:
<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 returnsfalsemodels: Returns the configured model namesmodel(name): Returns stub model constructors- Database operations: Throw descriptive errors
// 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 configModel 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
// 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
thisfor fluent API - Terminal methods: Return safe defaults (empty arrays, undefined, 0)
- Reactive methods: Return Vue reactive objects with stub data
// 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() // undefinedType Safety in SSR
The stubs maintain full TypeScript compatibility:
// 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 stringGotchas 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.
<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
isMountedflag to ensure consistent rendering - Always use
onReActiveDatabaseReadybefore 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.
// ❌ 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:
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:
// 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:
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 modelsFramework-Specific Notes
Nuxt 3
The plugin works seamlessly with Nuxt 3's universal rendering:
// 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:
// 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:
<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:
// ✅ 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:
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:
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
onBeforeUnmountto 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.