Integrations

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@4

Composition 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.

Next Steps