Vue.js Integration
Complete guide to integrating Promo with Vue.js applications using Composition API, reactivity system, and modern Vue patterns.
Composition API
Modern reactive composition patterns
Components
Reusable Vue components and composables
Reactivity
Vue 3 reactivity system integration
State Management
Pinia stores and reactive data
Plugins
Vue plugins and global configuration
Performance
Optimized for Vue 3 and Vite
Installation & Setup
Install Promo Vue
Add Promo to your Vue.js project
# Install the Vue package
npm install @feedbackkit/vue
# Install peer dependencies
npm install @feedbackkit/widget vue@^3.0.0
# For TypeScript projects
npm install -D @types/node @vitejs/plugin-vue
# Optional: State management
npm install pinia
# Optional: Vue Router
npm install vue-router@4Composition API & Composables
Waitlist Composables
Reactive waitlist management with Vue Composition API
<template>
<div class="waitlist-form">
<form @submit.prevent="handleSubmit">
<input
v-model="email"
type="email"
placeholder="Enter your email"
required
:disabled="isLoading"
/>
<button
type="submit"
:disabled="isLoading || !email"
class="submit-btn"
>
{{ isLoading ? 'Joining...' : 'Join Waitlist' }}
</button>
</form>
<div v-if="error" class="error">
{{ error.message }}
</div>
<div v-if="success && position" class="success">
<h3>Welcome to the waitlist!</h3>
<p>Position: #{{ position.current }} of {{ position.total }}</p>
<p>Share your referral link: {{ position.referralLink }}</p>
</div>
<!-- Waitlist stats -->
<div v-if="stats" class="stats">
<div class="stat">
<h4>{{ stats.totalSignups }}</h4>
<p>Total Signups</p>
</div>
<div class="stat">
<h4>{{ stats.todaySignups }}</h4>
<p>Today</p>
</div>
<div class="stat">
<h4>{{ stats.referralRate }}%</h4>
<p>Referral Rate</p>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
import { useWaitlist, useWaitlistStats } from '@promokit/vue'
// Reactive state
const email = ref('')
// Waitlist composable
const {
addToWaitlist,
removeFromWaitlist,
getPosition,
isLoading,
error,
data: waitlistData
} = useWaitlist()
// Stats composable
const {
stats,
isLoading: statsLoading,
error: statsError,
refresh: refreshStats
} = useWaitlistStats({
refreshInterval: 30000 // Refresh every 30 seconds
})
// Computed properties
const success = computed(() => waitlistData.value?.success)
const position = computed(() => waitlistData.value?.position)
// Methods
async function handleSubmit() {
try {
await addToWaitlist({
email: email.value,
metadata: {
source: 'vue-app',
timestamp: new Date().toISOString()
}
})
// Clear form on success
email.value = ''
// Refresh stats
await refreshStats()
} catch (err) {
console.error('Failed to join waitlist:', err)
}
}
</script>
<style scoped>
.waitlist-form {
max-width: 400px;
margin: 0 auto;
}
.submit-btn {
background: #3b82f6;
color: white;
padding: 12px 24px;
border: none;
border-radius: 8px;
cursor: pointer;
transition: all 0.2s;
}
.submit-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.error {
color: #ef4444;
margin-top: 8px;
}
.success {
background: #10b981;
color: white;
padding: 16px;
border-radius: 8px;
margin-top: 16px;
}
.stats {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 16px;
margin-top: 24px;
}
.stat {
text-align: center;
padding: 16px;
background: #f8fafc;
border-radius: 8px;
}
</style>State Management with Pinia
Pinia Stores
Centralized state management for Promo data
// stores/waitlist.ts
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import { usePromo } from '@promokit/vue'
export const useWaitlistStore = defineStore('waitlist', () => {
// State
const signups = ref<any[]>([])
const stats = ref<any>(null)
const userPosition = ref<number | null>(null)
const isLoading = ref(false)
const error = ref<string | null>(null)
// Getters
const totalSignups = computed(() => stats.value?.totalSignups || 0)
const todaySignups = computed(() => stats.value?.todaySignups || 0)
const growthRate = computed(() => stats.value?.growthRate || 0)
const isUserOnWaitlist = computed(() => userPosition.value !== null)
// Actions
const { client } = usePromo()
async function addToWaitlist(email: string, metadata?: any) {
isLoading.value = true
error.value = null
try {
const result = await client.waitlist.add({ email, metadata })
// Update local state
signups.value.push(result)
userPosition.value = result.position
// Refresh stats
await fetchStats()
return result
} catch (err: any) {
error.value = err.message
throw err
} finally {
isLoading.value = false
}
}
async function removeFromWaitlist(email: string) {
isLoading.value = true
error.value = null
try {
await client.waitlist.remove({ email })
// Update local state
signups.value = signups.value.filter(s => s.email !== email)
userPosition.value = null
await fetchStats()
} catch (err: any) {
error.value = err.message
throw err
} finally {
isLoading.value = false
}
}
async function fetchStats() {
try {
const newStats = await client.waitlist.getStats()
stats.value = newStats
} catch (err: any) {
console.error('Failed to fetch stats:', err)
}
}
async function getPosition(email: string) {
try {
const position = await client.waitlist.getPosition({ email })
userPosition.value = position
return position
} catch (err: any) {
console.error('Failed to get position:', err)
return null
}
}
return {
// State
signups: readonly(signups),
stats: readonly(stats),
userPosition: readonly(userPosition),
isLoading: readonly(isLoading),
error: readonly(error),
// Getters
totalSignups,
todaySignups,
growthRate,
isUserOnWaitlist,
// Actions
addToWaitlist,
removeFromWaitlist,
fetchStats,
getPosition
}
})
// stores/testimonials.ts
export const useTestimonialsStore = defineStore('testimonials', () => {
const testimonials = ref<any[]>([])
const pendingTestimonials = ref<any[]>([])
const filters = ref({ approved: true })
const isLoading = ref(false)
const error = ref<string | null>(null)
const { client } = usePromo()
// Getters
const approvedTestimonials = computed(() =>
testimonials.value.filter(t => t.approved)
)
const featuredTestimonials = computed(() =>
testimonials.value.filter(t => t.featured)
)
const averageRating = computed(() => {
if (testimonials.value.length === 0) return 0
const total = testimonials.value.reduce((sum, t) => sum + t.rating, 0)
return total / testimonials.value.length
})
// Actions
async function fetchTestimonials() {
isLoading.value = true
error.value = null
try {
const result = await client.testimonials.list({
filters: filters.value,
sort: 'createdAt:desc'
})
testimonials.value = result.data
} catch (err: any) {
error.value = err.message
} finally {
isLoading.value = false
}
}
async function submitTestimonial(data: any) {
isLoading.value = true
error.value = null
try {
const result = await client.testimonials.submit(data)
// Add to pending if auto-moderation is off
if (!result.approved) {
pendingTestimonials.value.push(result)
} else {
testimonials.value.unshift(result)
}
return result
} catch (err: any) {
error.value = err.message
throw err
} finally {
isLoading.value = false
}
}
async function moderateTestimonial(id: string, action: 'approve' | 'reject') {
try {
await client.testimonials.moderate(id, { action })
// Update local state
const pending = pendingTestimonials.value.find(t => t.id === id)
if (pending) {
pendingTestimonials.value = pendingTestimonials.value.filter(t => t.id !== id)
if (action === 'approve') {
testimonials.value.unshift({ ...pending, approved: true })
}
}
} catch (err: any) {
error.value = err.message
throw err
}
}
function updateFilters(newFilters: any) {
filters.value = { ...filters.value, ...newFilters }
fetchTestimonials()
}
return {
// State
testimonials: readonly(testimonials),
pendingTestimonials: readonly(pendingTestimonials),
filters: readonly(filters),
isLoading: readonly(isLoading),
error: readonly(error),
// Getters
approvedTestimonials,
featuredTestimonials,
averageRating,
// Actions
fetchTestimonials,
submitTestimonial,
moderateTestimonial,
updateFilters
}
})Performance Optimization
Vue 3 Features
// Async components for code splitting
import { defineAsyncComponent } from 'vue'
const TestimonialWall = defineAsyncComponent({
loader: () => import('./components/TestimonialWall.vue'),
loadingComponent: () => import('./components/LoadingSkeleton.vue'),
errorComponent: () => import('./components/ErrorFallback.vue'),
delay: 200,
timeout: 3000
})
// Suspense for async data loading
<template>
<Suspense>
<template #default>
<TestimonialWall />
</template>
<template #fallback>
<LoadingSkeleton />
</template>
</Suspense>
</template>
// Teleport for modals and overlays
<template>
<div class="app">
<!-- Main content -->
<WaitlistForm />
<!-- Teleport modal to body -->
<Teleport to="body">
<Modal v-if="showModal" @close="showModal = false">
<TestimonialForm />
</Modal>
</Teleport>
</div>
</template>
// Fragment support for multiple root nodes
<template>
<WaitlistStats />
<WaitlistForm />
<RecentSignups />
</template>Reactivity Optimization
// Use shallowRef for large objects
import { shallowRef, triggerRef } from 'vue'
const testimonials = shallowRef([])
// Manually trigger reactivity when needed
function addTestimonial(testimonial) {
testimonials.value.push(testimonial)
triggerRef(testimonials)
}
// Use markRaw for non-reactive objects
import { markRaw } from 'vue'
const client = markRaw(new PromoClient())
// Computed with getter for expensive operations
const expensiveStats = computed(() => {
if (!testimonials.value.length) return null
return {
averageRating: testimonials.value.reduce((sum, t) => sum + t.rating, 0) / testimonials.value.length,
sentimentAnalysis: analyzeSentiment(testimonials.value),
wordCloud: generateWordCloud(testimonials.value)
}
})
// Use watchEffect for side effects
import { watchEffect } from 'vue'
watchEffect(() => {
// Automatically track dependencies
if (userEmail.value && autoRefresh.value) {
fetchUserData(userEmail.value)
}
})
// Debounced watchers for user input
import { debounce } from 'lodash-es'
const debouncedSearch = debounce((term) => {
searchTestimonials(term)
}, 300)
watch(searchTerm, debouncedSearch)Best Practices
Component Architecture
Single File Components
Keep template, script, and styles together. Use scoped styles to prevent CSS conflicts.
Composition over Options
Prefer Composition API for better TypeScript support and reusability.
Props Validation
Always define prop types and default values for better developer experience.
State Management
Store Modularity
Create focused stores for specific domains. Compose them when needed.
Reactive Patterns
Leverage Vue reactivity system. Use computed properties for derived state.
Error Handling
Implement error boundaries and proper error state management.