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