Integrations

Svelte Integration

Complete guide to integrating Promo with Svelte applications using stores, reactivity, and modern Svelte patterns for optimal performance.

Svelte Stores

Reactive state management with built-in stores

Reactivity

Compile-time reactivity with reactive statements

Components

Lightweight components with minimal boilerplate

Actions

Reusable element actions and directives

SvelteKit

Full-stack framework with SSR and routing

Performance

Compile-time optimizations and tiny bundles

Installation & Setup

Install Promo Svelte

Add Promo to your Svelte project

# Install the Svelte package
npm install @feedbackkit/svelte

# Install peer dependencies
npm install @feedbackkit/widget svelte

# For TypeScript projects
npm install -D @tsconfig/svelte typescript

# For SvelteKit
npm install @sveltejs/kit

# Optional: Additional utilities
npm install @sveltejs/adapter-auto

Svelte Stores & Reactivity

Basic Stores

Writable and readable stores for Promo data

// src/stores/waitlist.js
import { writable, readable } from 'svelte/store';
import { promo } from '../lib/promo.js';

// Waitlist data store
export const waitlistData = writable({
  signups: [],
  stats: null,
  userPosition: null,
  isLoading: false,
  error: null
});

// Waitlist stats store (updates every 30 seconds)
export const waitlistStats = readable(null, (set) => {
  let interval;
  
  const fetchStats = async () => {
    try {
      const client = promo.getClient();
      const stats = await client.waitlist.getStats();
      set(stats);
    } catch (error) {
      console.error('Failed to fetch waitlist stats:', error);
    }
  };

  // Initial fetch
  fetchStats();
  
  // Set up interval
  interval = setInterval(fetchStats, 30000);
  
  // Cleanup function
  return () => {
    if (interval) {
      clearInterval(interval);
    }
  };
});

// Actions for waitlist store
export const waitlistActions = {
  async addToWaitlist(email, metadata = {}) {
    waitlistData.update(store => ({ ...store, isLoading: true, error: null }));
    
    try {
      const client = promo.getClient();
      const result = await client.waitlist.add({ email, metadata });
      
      waitlistData.update(store => ({
        ...store,
        signups: [...store.signups, result],
        userPosition: result.position,
        isLoading: false
      }));
      
      return result;
    } catch (error) {
      waitlistData.update(store => ({ ...store, isLoading: false, error }));
      throw error;
    }
  },

  async removeFromWaitlist(email) {
    waitlistData.update(store => ({ ...store, isLoading: true, error: null }));
    
    try {
      const client = promo.getClient();
      await client.waitlist.remove({ email });
      
      waitlistData.update(store => ({
        ...store,
        signups: store.signups.filter(s => s.email !== email),
        userPosition: null,
        isLoading: false
      }));
    } catch (error) {
      waitlistData.update(store => ({ ...store, isLoading: false, error }));
      throw error;
    }
  },

  async getPosition(email) {
    try {
      const client = promo.getClient();
      const position = await client.waitlist.getPosition({ email });
      
      waitlistData.update(store => ({ ...store, userPosition: position }));
      return position;
    } catch (error) {
      console.error('Failed to get position:', error);
      return null;
    }
  }
};

// Usage in component:
// import { waitlistData, waitlistActions } from '../stores/waitlist.js';
// $: ({ isLoading, error, userPosition } = $waitlistData);

Actions & Directives

Custom Actions

Reusable element actions for Promo integrations

// src/actions/promo.js
export function trackClick(node, { event, data = {} }) {
  function handleClick() {
    // Track click event with Promo analytics
    if (window.promo && window.promo.analytics) {
      window.promo.analytics.track(event, {
        element: node.tagName,
        text: node.textContent,
        ...data
      });
    }
  }

  node.addEventListener('click', handleClick);

  return {
    update(newParams) {
      event = newParams.event;
      data = newParams.data || {};
    },
    destroy() {
      node.removeEventListener('click', handleClick);
    }
  };
}

export function autoSubmit(node, { delay = 2000, onSubmit }) {
  let timeout;

  function handleInput() {
    clearTimeout(timeout);
    timeout = setTimeout(() => {
      if (typeof onSubmit === 'function') {
        onSubmit(new FormData(node));
      }
    }, delay);
  }

  node.addEventListener('input', handleInput);

  return {
    update(newParams) {
      delay = newParams.delay || 2000;
      onSubmit = newParams.onSubmit;
    },
    destroy() {
      clearTimeout(timeout);
      node.removeEventListener('input', handleInput);
    }
  };
}

export function waitlistPosition(node, { email }) {
  let currentEmail = email;

  async function updatePosition() {
    if (!currentEmail) return;

    try {
      const client = promo.getClient();
      const position = await client.waitlist.getPosition({ email: currentEmail });
      
      if (position) {
        node.textContent = '#' + position;
        node.setAttribute('data-position', position);
      }
    } catch (error) {
      console.error('Failed to get position:', error);
      node.textContent = '--';
    }
  }

  updatePosition();

  return {
    update(newParams) {
      currentEmail = newParams.email;
      updatePosition();
    }
  };
}

// Usage in components:
// <button use:trackClick={{ event: 'waitlist_cta_click', data: { source: 'hero' } }}>
//   Join Waitlist
// </button>
//
// <form use:autoSubmit={{ delay: 3000, onSubmit: handleFormSubmit }}>
//   <!-- form content -->
// </form>
//
// <span use:waitlistPosition={{ email: userEmail }}>Loading...</span>

SvelteKit Features

Server-Side Rendering

// src/routes/+page.server.ts
import type { PageServerLoad } from './$types';

export const load: PageServerLoad = async ({ locals, url }) => {
  const { promo } = locals;
  
  if (!promo) {
    return {
      waitlistStats: null,
      testimonials: [],
      error: 'Promo not configured'
    };
  }

  try {
    // Fetch data server-side for SEO and performance
    const [waitlistStats, testimonials] = await Promise.all([
      promo.client.waitlist.getStats(),
      promo.client.testimonials.list({ 
        filters: { approved: true, featured: true },
        limit: 6 
      })
    ]);

    return {
      waitlistStats,
      testimonials: testimonials.data,
      meta: {
        title: `Join ${waitlistStats.totalSignups} others on our waitlist`,
        description: 'Be the first to know when we launch',
        ogImage: url.origin + '/og-waitlist.png'
      }
    };
  } catch (error) {
    console.error('Failed to load page data:', error);
    
    return {
      waitlistStats: null,
      testimonials: [],
      error: error.message
    };
  }
};

API Routes

// src/routes/api/waitlist/+server.ts
import { json } from '@sveltejs/kit';
import type { RequestHandler } from './$types';

export const POST: RequestHandler = async ({ request, locals }) => {
  const { promo } = locals;
  
  if (!promo) {
    return json({ error: 'Service unavailable' }, { status: 503 });
  }

  try {
    const { email, metadata = {} } = await request.json();
    
    if (!email || !email.includes('@')) {
      return json({ error: 'Valid email required' }, { status: 400 });
    }

    const result = await promo.client.waitlist.add({
      email,
      metadata: {
        ...metadata,
        source: 'sveltekit',
        timestamp: new Date().toISOString()
      }
    });

    return json({
      success: true,
      position: result.position,
      referralLink: result.referralLink
    });
  } catch (error) {
    console.error('Waitlist signup error:', error);
    return json({ error: error.message }, { status: 500 });
  }
};

export const GET: RequestHandler = async ({ url, locals }) => {
  const { promo } = locals;
  const email = url.searchParams.get('email');
  
  if (!email) {
    return json({ error: 'Email parameter required' }, { status: 400 });
  }

  try {
    const position = await promo.client.waitlist.getPosition({ email });
    return json({ position });
  } catch (error) {
    return json({ error: error.message }, { status: 500 });
  }
};

Performance Optimization

Bundle Optimization

// vite.config.js
import { sveltekit } from '@sveltejs/kit/vite';
import { defineConfig } from 'vite';

export default defineConfig({
  plugins: [sveltekit()],
  
  build: {
    rollupOptions: {
      output: {
        manualChunks: {
          promo: ['@promokit/js', '@promokit/svelte'],
          vendor: ['svelte', '@sveltejs/kit']
        }
      }
    }
  },
  
  optimizeDeps: {
    include: ['@promokit/js']
  },
  
  // Tree-shaking configuration
  ssr: {
    noExternal: ['@promokit/svelte']
  }
});

// Dynamic imports for code splitting
// src/components/TestimonialWall.svelte
<script>
  import { onMount } from 'svelte';
  
  let TestimonialCard;
  let testimonials = [];
  
  onMount(async () => {
    // Lazy load component
    const module = await import('./TestimonialCard.svelte');
    TestimonialCard = module.default;
    
    // Fetch data after component mount
    await loadTestimonials();
  });
  
  async function loadTestimonials() {
    try {
      const response = await fetch('/api/testimonials');
      testimonials = await response.json();
    } catch (error) {
      console.error('Failed to load testimonials:', error);
    }
  }
</script>

{#if TestimonialCard}
  <div class="testimonials">
    {#each testimonials as testimonial}
      <svelte:component this={TestimonialCard} {testimonial} />
    {/each}
  </div>
{:else}
  <div class="loading">Loading testimonials...</div>
{/if}

Reactive Optimization

// Optimize reactive statements
<script>
  import { waitlistStats } from '../stores/waitlist.js';
  
  // ❌ Don't do this - runs on every update
  $: expensiveCalculation = $waitlistStats ? 
    complexAnalysis($waitlistStats) : null;
  
  // ✅ Do this - only run when specific values change
  $: totalSignups = $waitlistStats?.totalSignups || 0;
  $: growthRate = $waitlistStats?.growthRate || 0;
  $: insights = calculateInsights(totalSignups, growthRate);
  
  function calculateInsights(signups, growth) {
    // Only runs when signups or growth changes
    return {
      momentum: growth > 0.1 ? 'high' : 'moderate',
      milestone: getNextMilestone(signups)
    };
  }
  
  // Use tick() for DOM updates
  import { tick } from 'svelte';
  
  let scrollContainer;
  
  async function addTestimonial(testimonial) {
    testimonials = [...testimonials, testimonial];
    
    // Wait for DOM update
    await tick();
    
    // Scroll to new testimonial
    scrollContainer.scrollTop = scrollContainer.scrollHeight;
  }
  
  // Debounce expensive operations
  import { debounce } from 'lodash-es';
  
  let searchTerm = '';
  let filteredTestimonials = [];
  
  const debouncedSearch = debounce((term) => {
    filteredTestimonials = testimonials.filter(t =>
      t.testimonial.toLowerCase().includes(term.toLowerCase())
    );
  }, 300);
  
  $: debouncedSearch(searchTerm);
</script>

<!-- Use keyed each blocks for better performance -->
{#each testimonials as testimonial (testimonial.id)}
  <TestimonialCard {testimonial} />
{/each}

<!-- Virtualization for large lists -->
<script>
  import { createVirtualList } from '@tanstack/svelte-virtual';
  
  const virtualList = createVirtualList({
    count: testimonials.length,
    estimateSize: () => 200,
    getScrollElement: () => scrollContainer
  });
</script>

<div bind:this={scrollContainer} class="scroll-container">
  <div style="height: {$virtualList.getTotalSize()}px; position: relative;">
    {#each $virtualList.getVirtualItems() as item}
      <div
        style="position: absolute; top: 0; left: 0; width: 100%; transform: translateY({item.start}px);"
      >
        <TestimonialCard testimonial={testimonials[item.index]} />
      </div>
    {/each}
  </div>
</div>

Best Practices

Component Design

Reactive Patterns

Use reactive statements ($:) for derived state. Keep them simple and focused.

Store Design

Create focused stores for specific domains. Use derived stores for computed state.

Component Communication

Use events for child-to-parent communication and stores for global state.

Development Workflow

TypeScript Integration

Use TypeScript for better developer experience and type safety with Promo APIs.

Testing Strategy

Test stores separately from components. Mock Promo client for unit tests.

Error Handling

Implement global error boundaries and consistent error states across stores.

Next Steps