Svelte Framework Guide
Complete guide to using DuckDB WASM Adapter with Svelte applications, including Svelte 5's new runes API.
Quick Start
Installation
bash
npm install @northprint/duckdb-wasm-adapter-svelteBasic Setup (Svelte 5 with Runes)
javascript
// src/App.svelte
<script>
import { createDuckDBRunes } from '@northprint/duckdb-wasm-adapter-svelte';
// Create DuckDB instance with Svelte 5 runes
const db = createDuckDBRunes({
autoConnect: true,
config: {
worker: true,
cache: { enabled: true }
}
});
// Reactive state with $state and $derived
const stats = $derived({
connected: db.isConnected,
loading: db.isLoading,
status: db.status
});
</script>
<main>
<h1>My DuckDB App</h1>
<p>Status: {db.status}</p>
<!-- Your components -->
</main>Basic Setup (Traditional Stores)
javascript
// src/App.svelte
<script>
import { createDuckDB } from '@northprint/duckdb-wasm-adapter-svelte';
const db = createDuckDB({
autoConnect: true,
config: {
worker: true,
cache: { enabled: true }
}
});
</script>
<main>
<h1>My DuckDB App</h1>
<p>Status: {$db.status}</p>
<!-- Your components -->
</main>First Query (Svelte 5 Runes)
svelte
<script>
import { createDuckDBRunes, createQueryRune } from '@northprint/duckdb-wasm-adapter-svelte';
const db = createDuckDBRunes({ autoConnect: true });
const queryRune = createQueryRune(db, 'SELECT 42 as answer');
</script>
<h1>Dashboard</h1>
{#if queryRune.loading}
<div>Loading...</div>
{:else if queryRune.error}
<div>Error: {queryRune.error.message}</div>
{:else if queryRune.hasData}
<p>The answer is: {queryRune.data[0].answer}</p>
{/if}Core Concepts
Store-Based Architecture
Svelte adapter uses reactive stores for state management:
javascript
// All state is managed through Svelte stores
const db = duckdb({ autoConnect: true });
// Reactive stores
$: status = $db.status; // Connection status
$: isConnected = $db.isConnected; // Connection state
$: error = $db.error; // Error stateReactive Queries
Queries automatically update when dependencies change:
svelte
<script>
import { duckdb } from '@northprint/duckdb-wasm-adapter-svelte';
const db = duckdb({ autoConnect: true });
let department = '';
let searchTerm = '';
// Reactive query - re-runs when department or searchTerm changes
$: users = db.query(
`SELECT * FROM users
WHERE ($1 = '' OR department = $1)
AND ($2 = '' OR name ILIKE '%' || $2 || '%')
ORDER BY name`,
[department, searchTerm]
);
</script>
<select bind:value={department}>
<option value="">All Departments</option>
<option value="Engineering">Engineering</option>
<option value="Sales">Sales</option>
</select>
<input bind:value={searchTerm} placeholder="Search users..." />
{#if $users.loading}
<div>Loading...</div>
{:else if $users.error}
<div>Error: {$users.error.message}</div>
{:else if $users.data}
<p>Found {$users.data.length} users</p>
{#each $users.data as user}
<div class="user-card">
<h3>{user.name}</h3>
<p>{user.email}</p>
</div>
{/each}
{/if}Data Fetching Patterns
Simple Queries
svelte
<script>
import { duckdb } from '@northprint/duckdb-wasm-adapter-svelte';
const db = duckdb({ autoConnect: true });
const users = db.query(`
SELECT id, name, email, department
FROM users
WHERE active = true
ORDER BY name
`);
</script>
<div class="user-list">
<h2>Active Users</h2>
{#if $users.loading}
<div class="loading">Loading users...</div>
{:else if $users.error}
<div class="error">Failed to load users: {$users.error.message}</div>
{:else if $users.data}
<p>{$users.data.length} users found</p>
<div class="user-grid">
{#each $users.data as user (user.id)}
<div class="user-card">
<h3>{user.name}</h3>
<p>{user.email}</p>
<span class="department">{user.department}</span>
</div>
{/each}
</div>
{/if}
</div>
<style>
.user-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 1rem;
}
.user-card {
border: 1px solid #ddd;
border-radius: 8px;
padding: 1rem;
}
.loading, .error {
text-align: center;
padding: 2rem;
}
.error {
color: #d32f2f;
}
</style>Conditional Queries
svelte
<script>
import { duckdb } from '@northprint/duckdb-wasm-adapter-svelte';
const db = duckdb({ autoConnect: true });
let selectedUserId = null;
// Only run when user is selected
$: user = selectedUserId ?
db.query('SELECT * FROM users WHERE id = ?', [selectedUserId]) :
{ data: null, loading: false, error: null };
// Only run when user is loaded
$: orders = $user.data ?
db.query('SELECT * FROM orders WHERE user_id = ? ORDER BY created_at DESC', [selectedUserId]) :
{ data: null, loading: false, error: null };
</script>
<UserSelector bind:selectedUserId />
{#if !selectedUserId}
<div class="no-selection">
Please select a user to view details
</div>
{:else if $user.loading}
<div class="loading">Loading user details...</div>
{:else if $user.error}
<div class="error">Error loading user: {$user.error.message}</div>
{:else if $user.data}
<div class="user-details">
<h2>{$user.data.name}</h2>
<p>Email: {$user.data.email}</p>
<p>Department: {$user.data.department}</p>
{#if $orders.loading}
<div class="loading">Loading orders...</div>
{:else if $orders.data && $orders.data.length > 0}
<h3>Recent Orders</h3>
<OrdersList orders={$orders.data} />
{:else}
<p>No orders found</p>
{/if}
</div>
{/if}Data Mutations
Creating Records
svelte
<script>
import { duckdb, mutation } from '@northprint/duckdb-wasm-adapter-svelte';
const db = duckdb({ autoConnect: true });
const createUser = mutation({
onSuccess: (result) => {
console.log('User created:', result);
// Reset form
form = { name: '', email: '', department: '' };
// Refresh users list if needed
users.refresh();
},
onError: (error) => {
console.error('Failed to create user:', error);
}
});
// Refresh users when needed
const users = db.query('SELECT * FROM users ORDER BY name');
let form = {
name: '',
email: '',
department: ''
};
async function handleSubmit() {
await createUser.execute(
'INSERT INTO users (name, email, department) VALUES (?, ?, ?) RETURNING *',
[form.name, form.email, form.department]
);
}
</script>
<form on:submit|preventDefault={handleSubmit}>
<h2>Create New User</h2>
<div class="form-group">
<label for="name">Name:</label>
<input
id="name"
bind:value={form.name}
type="text"
required
/>
</div>
<div class="form-group">
<label for="email">Email:</label>
<input
id="email"
bind:value={form.email}
type="email"
required
/>
</div>
<div class="form-group">
<label for="department">Department:</label>
<select id="department" bind:value={form.department} required>
<option value="">Select Department</option>
<option value="Engineering">Engineering</option>
<option value="Sales">Sales</option>
<option value="Marketing">Marketing</option>
</select>
</div>
<button type="submit" disabled={$createUser.loading}>
{$createUser.loading ? 'Creating...' : 'Create User'}
</button>
{#if $createUser.error}
<div class="error">
Error: {$createUser.error.message}
</div>
{/if}
{#if $createUser.data}
<div class="success">
User created successfully!
</div>
{/if}
</form>
<style>
.form-group {
margin-bottom: 1rem;
}
label {
display: block;
margin-bottom: 0.5rem;
font-weight: bold;
}
input, select {
width: 100%;
padding: 0.5rem;
border: 1px solid #ddd;
border-radius: 4px;
}
.error {
color: #d32f2f;
margin-top: 1rem;
}
.success {
color: #2e7d32;
margin-top: 1rem;
}
</style>Updating Records
svelte
<script>
import { mutation } from '@northprint/duckdb-wasm-adapter-svelte';
export let user;
const updateUser = mutation({
onSuccess: (result) => {
dispatch('updated', result[0]);
}
});
let form = { ...user };
async function handleSubmit() {
await updateUser.execute(
'UPDATE users SET name = ?, email = ?, department = ? WHERE id = ? RETURNING *',
[form.name, form.email, form.department, user.id]
);
}
import { createEventDispatcher } from 'svelte';
const dispatch = createEventDispatcher();
</script>
<form on:submit|preventDefault={handleSubmit}>
<h2>Edit User</h2>
<div class="form-group">
<label>Name:</label>
<input bind:value={form.name} type="text" required />
</div>
<div class="form-group">
<label>Email:</label>
<input bind:value={form.email} type="email" required />
</div>
<div class="form-group">
<label>Department:</label>
<select bind:value={form.department} required>
<option value="Engineering">Engineering</option>
<option value="Sales">Sales</option>
<option value="Marketing">Marketing</option>
</select>
</div>
<div class="form-actions">
<button type="submit" disabled={$updateUser.loading}>
{$updateUser.loading ? 'Updating...' : 'Update User'}
</button>
<button type="button" on:click={() => dispatch('cancel')}>
Cancel
</button>
</div>
</form>Data Import/Export
CSV Import
svelte
<script>
import { importCSV } from '@northprint/duckdb-wasm-adapter-svelte';
const csvImporter = importCSV({
onSuccess: (result) => {
console.log('Import completed:', result);
success = true;
setTimeout(() => { success = false; }, 3000);
},
onError: (error) => {
console.error('Import failed:', error);
}
});
let success = false;
let options = {
header: true,
delimiter: ','
};
async function handleFileChange(event) {
const file = event.target.files[0];
if (!file) return;
await csvImporter.execute(file, 'imported_users', {
header: options.header,
delimiter: options.delimiter,
nullString: 'NULL'
});
}
</script>
<div class="csv-importer">
<h3>Import Users from CSV</h3>
<div class="import-options">
<label>
<input bind:checked={options.header} type="checkbox" />
First row contains headers
</label>
<label>
Delimiter:
<select bind:value={options.delimiter}>
<option value=",">Comma (,)</option>
<option value=";">Semicolon (;)</option>
<option value="\t">Tab</option>
</select>
</label>
</div>
<div class="upload-area">
<input
type="file"
accept=".csv"
on:change={handleFileChange}
disabled={$csvImporter.loading}
/>
{#if $csvImporter.loading}
<div class="progress">
<div class="spinner"></div>
<p>Importing CSV file...</p>
</div>
{/if}
{#if $csvImporter.error}
<div class="error">
Import failed: {$csvImporter.error.message}
</div>
{/if}
{#if success}
<div class="success">
CSV imported successfully!
</div>
{/if}
</div>
</div>
<style>
.upload-area {
border: 2px dashed #ddd;
border-radius: 8px;
padding: 2rem;
text-align: center;
margin: 1rem 0;
}
.import-options {
display: flex;
gap: 1rem;
margin-bottom: 1rem;
}
.spinner {
width: 20px;
height: 20px;
border: 2px solid #f3f3f3;
border-top: 2px solid #3498db;
border-radius: 50%;
animation: spin 1s linear infinite;
margin: 0 auto;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
</style>Advanced Patterns
Custom Stores
javascript
// stores/userStore.js
import { writable, derived } from 'svelte/store';
import { duckdb } from '@northprint/duckdb-wasm-adapter-svelte';
// Initialize database
const db = duckdb({ autoConnect: true });
// Filters store
export const userFilters = writable({
department: '',
active: true,
search: ''
});
// Derived query based on filters
export const filteredUsers = derived(
userFilters,
($filters) => {
let sql = 'SELECT * FROM users WHERE 1=1';
const params = [];
if ($filters.department) {
sql += ' AND department = ?';
params.push($filters.department);
}
if ($filters.search) {
sql += ' AND name ILIKE ?';
params.push(`%${$filters.search}%`);
}
if ($filters.active !== null) {
sql += ' AND active = ?';
params.push($filters.active);
}
sql += ' ORDER BY name';
return db.query(sql, params);
}
);
// User statistics
export const userStats = derived(
filteredUsers,
($users) => {
if (!$users.data) return null;
const data = $users.data;
return {
total: data.length,
byDepartment: data.reduce((acc, user) => {
acc[user.department] = (acc[user.department] || 0) + 1;
return acc;
}, {}),
avgAge: data.reduce((sum, user) => sum + (user.age || 0), 0) / data.length
};
}
);Component Actions
javascript
// actions/duckdbAction.js
export function duckdbAction(node, options = {}) {
let db;
async function initialize() {
const { duckdb } = await import('@northprint/duckdb-wasm-adapter-svelte');
db = duckdb(options);
// Add database reference to the node
node.duckdb = db;
// Dispatch ready event
node.dispatchEvent(new CustomEvent('duckdb:ready', {
detail: { db }
}));
}
initialize();
return {
update(newOptions) {
// Handle option updates
Object.assign(options, newOptions);
},
destroy() {
// Cleanup if needed
if (db) {
// Close connections, etc.
}
}
};
}Usage:
svelte
<script>
import { duckdbAction } from './actions/duckdbAction.js';
let dbContainer;
function handleDbReady(event) {
const { db } = event.detail;
console.log('Database ready:', db);
}
</script>
<div
bind:this={dbContainer}
use:duckdbAction={{ autoConnect: true }}
on:duckdb:ready={handleDbReady}
>
<!-- Database-connected component -->
</div>Error Boundaries
svelte
<!-- ErrorBoundary.svelte -->
<script>
import { onDestroy } from 'svelte';
export let fallback = null;
let hasError = false;
let errorDetails = '';
function handleError(event) {
console.error('Svelte Error:', event.error);
hasError = true;
errorDetails = event.error.message;
}
function reset() {
hasError = false;
errorDetails = '';
}
// Global error handler
if (typeof window !== 'undefined') {
window.addEventListener('error', handleError);
window.addEventListener('unhandledrejection', (event) => {
handleError({ error: event.reason });
});
onDestroy(() => {
window.removeEventListener('error', handleError);
window.removeEventListener('unhandledrejection', handleError);
});
}
</script>
{#if hasError}
{#if fallback}
<svelte:component this={fallback} {errorDetails} {reset} />
{:else}
<div class="error-boundary">
<h3>Something went wrong</h3>
<details>
<summary>Error Details</summary>
<pre>{errorDetails}</pre>
</details>
<button on:click={reset}>Try Again</button>
</div>
{/if}
{:else}
<slot />
{/if}
<style>
.error-boundary {
border: 1px solid #d32f2f;
border-radius: 8px;
padding: 1rem;
margin: 1rem 0;
background-color: #ffebee;
}
details {
margin: 1rem 0;
}
pre {
background-color: #f5f5f5;
padding: 0.5rem;
border-radius: 4px;
overflow-x: auto;
}
</style>Performance Optimization
Lazy Loading
svelte
<script>
import { onMount } from 'svelte';
import { duckdb } from '@northprint/duckdb-wasm-adapter-svelte';
const db = duckdb({ autoConnect: true });
let currentPage = 1;
let hasMore = true;
let allUsers = [];
const PAGE_SIZE = 20;
async function loadMoreUsers() {
const offset = (currentPage - 1) * PAGE_SIZE;
const users = db.query(
'SELECT * FROM users ORDER BY name LIMIT ? OFFSET ?',
[PAGE_SIZE, offset]
);
// Wait for query to complete
users.subscribe(result => {
if (result.data) {
if (result.data.length < PAGE_SIZE) {
hasMore = false;
}
allUsers = [...allUsers, ...result.data];
currentPage++;
}
});
}
onMount(() => {
loadMoreUsers();
});
// Intersection observer for infinite scroll
function observeLastElement(node) {
const observer = new IntersectionObserver((entries) => {
if (entries[0].isIntersecting && hasMore) {
loadMoreUsers();
}
});
observer.observe(node);
return {
destroy() {
observer.disconnect();
}
};
}
</script>
<div class="user-list">
{#each allUsers as user, index (user.id)}
<div
class="user-card"
use:observeLastElement={index === allUsers.length - 1}
>
<h3>{user.name}</h3>
<p>{user.email}</p>
</div>
{/each}
{#if hasMore}
<div class="loading">Loading more users...</div>
{/if}
</div>Virtual Scrolling
svelte
<script>
import { onMount, tick } from 'svelte';
import { duckdb } from '@northprint/duckdb-wasm-adapter-svelte';
const db = duckdb({ autoConnect: true });
const users = db.query('SELECT * FROM users ORDER BY name');
let scrollContainer;
let itemHeight = 60;
let containerHeight = 400;
let scrollTop = 0;
$: totalHeight = ($users.data?.length || 0) * itemHeight;
$: visibleStart = Math.floor(scrollTop / itemHeight);
$: visibleEnd = Math.min(
visibleStart + Math.ceil(containerHeight / itemHeight) + 1,
$users.data?.length || 0
);
$: visibleItems = $users.data?.slice(visibleStart, visibleEnd) || [];
$: offsetY = visibleStart * itemHeight;
function handleScroll() {
scrollTop = scrollContainer.scrollTop;
}
</script>
<div
class="virtual-scroll-container"
style="height: {containerHeight}px"
bind:this={scrollContainer}
on:scroll={handleScroll}
>
<div style="height: {totalHeight}px; position: relative;">
<div style="transform: translateY({offsetY}px);">
{#each visibleItems as user, index (user.id)}
<div
class="virtual-item"
style="height: {itemHeight}px"
>
<h4>{user.name}</h4>
<p>{user.email}</p>
</div>
{/each}
</div>
</div>
</div>
<style>
.virtual-scroll-container {
overflow-y: auto;
border: 1px solid #ddd;
}
.virtual-item {
display: flex;
flex-direction: column;
justify-content: center;
padding: 0 1rem;
border-bottom: 1px solid #eee;
}
.virtual-item h4 {
margin: 0;
}
.virtual-item p {
margin: 0;
color: #666;
font-size: 0.9rem;
}
</style>Svelte 5 Runes
State Management with $state
Svelte 5 introduces runes for reactive state management:
svelte
<script>
import { duckdb } from '@northprint/duckdb-wasm-adapter-svelte';
// Using $state for reactive variables
let searchTerm = $state('');
let department = $state('');
let pageSize = $state(20);
const db = duckdb({ autoConnect: true });
// Query automatically reruns when state changes
const users = db.query(
`SELECT * FROM users
WHERE name ILIKE '%' || ? || '%'
AND (? = '' OR department = ?)
LIMIT ?`,
() => [searchTerm, department, department, pageSize]
);
</script>
<input bind:value={searchTerm} placeholder="Search users..." />
<select bind:value={department}>
<option value="">All Departments</option>
<option value="Engineering">Engineering</option>
<option value="Sales">Sales</option>
</select>
{#if $users.data}
<UserList users={$users.data} />
{/if}Derived State with $derived
Use $derived for computed values:
svelte
<script>
import { duckdb } from '@northprint/duckdb-wasm-adapter-svelte';
const db = duckdb({ autoConnect: true });
let selectedUserId = $state(null);
const users = db.query('SELECT * FROM users');
const orders = db.query('SELECT * FROM orders');
// Derived state updates automatically
const selectedUser = $derived(
$users.data?.find(u => u.id === selectedUserId)
);
const userOrders = $derived(
$orders.data?.filter(o => o.user_id === selectedUserId)
);
const orderStats = $derived({
count: userOrders?.length || 0,
total: userOrders?.reduce((sum, o) => sum + o.amount, 0) || 0,
average: userOrders?.length
? userOrders.reduce((sum, o) => sum + o.amount, 0) / userOrders.length
: 0
});
</script>
<UserSelector bind:selectedId={selectedUserId} users={$users.data} />
{#if selectedUser}
<div class="user-details">
<h2>{selectedUser.name}</h2>
<p>Orders: {orderStats.count}</p>
<p>Total: ${orderStats.total.toFixed(2)}</p>
<p>Average: ${orderStats.average.toFixed(2)}</p>
</div>
{/if}Side Effects with $effect
Handle side effects with the $effect rune:
svelte
<script>
import { duckdb } from '@northprint/duckdb-wasm-adapter-svelte';
const db = duckdb({ autoConnect: true });
let tableData = $state([]);
let chartInstance = $state(null);
const analytics = db.query(
'SELECT date, revenue FROM analytics ORDER BY date'
);
// Effect runs when dependencies change
$effect(() => {
if ($analytics.data && chartInstance) {
// Update chart when data changes
chartInstance.updateData($analytics.data);
}
});
// Cleanup effect
$effect(() => {
console.log('Analytics data loaded:', $analytics.data);
return () => {
// Cleanup when component unmounts
chartInstance?.destroy();
};
});
// Effect with async operations
$effect(async () => {
if ($analytics.data) {
try {
const processed = await processData($analytics.data);
tableData = processed;
} catch (error) {
console.error('Processing failed:', error);
}
}
});
</script>
<div bind:this={chartInstance}>
<!-- Chart renders here -->
</div>Component Props with $props
Modern prop handling with $props:
svelte
<!-- UserCard.svelte -->
<script>
import { duckdb } from '@northprint/duckdb-wasm-adapter-svelte';
// Destructure props with defaults
let {
userId,
showOrders = false,
showStats = true,
...restProps
} = $props();
const db = duckdb({ autoConnect: true });
const user = db.query(
'SELECT * FROM users WHERE id = ?',
[userId]
);
const orders = $derived(
showOrders
? db.query('SELECT * FROM orders WHERE user_id = ?', [userId])
: { data: null }
);
</script>
<div class="user-card" {...restProps}>
{#if $user.data}
<h3>{$user.data[0].name}</h3>
<p>{$user.data[0].email}</p>
{#if showStats}
<UserStats userId={userId} />
{/if}
{#if showOrders && $orders.data}
<OrderList orders={$orders.data} />
{/if}
{/if}
</div>Two-way Binding with $bindable
Create bindable component props:
svelte
<!-- FilterInput.svelte -->
<script>
let {
value = $bindable(''),
min = 0,
max = 100,
step = 1
} = $props();
// Internal state
let localValue = $state(value);
$effect(() => {
// Sync with parent
value = localValue;
});
</script>
<div class="filter-input">
<input
type="range"
bind:value={localValue}
{min}
{max}
{step}
/>
<span>{localValue}</span>
</div>
<!-- Parent Component -->
<script>
import { duckdb } from '@northprint/duckdb-wasm-adapter-svelte';
import FilterInput from './FilterInput.svelte';
let minSalary = $state(50000);
let maxSalary = $state(150000);
const db = duckdb({ autoConnect: true });
const users = db.query(
'SELECT * FROM users WHERE salary BETWEEN ? AND ?',
() => [minSalary, maxSalary]
);
</script>
<FilterInput bind:value={minSalary} min={0} max={200000} step={5000} />
<FilterInput bind:value={maxSalary} min={0} max={200000} step={5000} />
<UserList users={$users.data} />Advanced Patterns with Runes
svelte
<script>
import { duckdb } from '@northprint/duckdb-wasm-adapter-svelte';
// Reactive class with runes
class DataManager {
query = $state('');
results = $state([]);
loading = $state(false);
constructor(db) {
this.db = db;
}
async execute() {
this.loading = true;
try {
const result = await this.db.execute(this.query);
this.results = result.toArray();
} catch (error) {
console.error('Query failed:', error);
} finally {
this.loading = false;
}
}
}
const db = duckdb({ autoConnect: true });
const manager = new DataManager(db);
// Effect for auto-execution
$effect(() => {
if (manager.query && manager.query.length > 10) {
manager.execute();
}
});
</script>
<textarea bind:value={manager.query} placeholder="Enter SQL query..." />
<button onclick={() => manager.execute()} disabled={manager.loading}>
{manager.loading ? 'Executing...' : 'Execute'}
</button>
{#if manager.results.length > 0}
<DataTable data={manager.results} />
{/if}Migrating from Stores to Runes
svelte
<!-- Old Store Pattern -->
<script>
import { writable, derived } from 'svelte/store';
import { duckdb } from '@northprint/duckdb-wasm-adapter-svelte';
const searchTerm = writable('');
const users = writable([]);
const filteredUsers = derived(
[users, searchTerm],
([$users, $searchTerm]) =>
$users.filter(u => u.name.includes($searchTerm))
);
</script>
<!-- New Runes Pattern -->
<script>
import { duckdb } from '@northprint/duckdb-wasm-adapter-svelte';
let searchTerm = $state('');
let users = $state([]);
const filteredUsers = $derived(
users.filter(u => u.name.includes(searchTerm))
);
const db = duckdb({ autoConnect: true });
// Load users with effect
$effect(async () => {
const result = await db.execute('SELECT * FROM users');
users = result.toArray();
});
</script>Testing
Component Testing
javascript
// tests/UserList.test.js
import { render, fireEvent } from '@testing-library/svelte';
import UserList from '../src/components/UserList.svelte';
test('renders user list', async () => {
const { getByText, getByRole } = render(UserList);
// Should show loading initially
expect(getByText('Loading...')).toBeInTheDocument();
// Wait for data to load
await waitFor(() => {
expect(getByText('Users')).toBeInTheDocument();
});
});
test('filters users by department', async () => {
const { getByRole, getByText } = render(UserList);
const select = getByRole('combobox');
await fireEvent.change(select, { target: { value: 'Engineering' } });
// Should filter results
await waitFor(() => {
expect(getByText(/Engineering/)).toBeInTheDocument();
});
});Store Testing
javascript
// tests/userStore.test.js
import { get } from 'svelte/store';
import { userFilters, filteredUsers } from '../src/stores/userStore.js';
test('filters users correctly', () => {
// Set filter
userFilters.set({ department: 'Engineering', active: true, search: '' });
// Get filtered results
const filters = get(userFilters);
expect(filters.department).toBe('Engineering');
// Test derived store
const unsubscribe = filteredUsers.subscribe(users => {
// Assert query parameters are correct
expect(users).toBeDefined();
});
unsubscribe();
});Best Practices
1. Use TypeScript
typescript
// types.ts
export interface User {
id: number;
name: string;
email: string;
department: string;
active: boolean;
}
// Component with TypeScript
<script lang="ts">
import type { User } from './types.js';
import { duckdb } from '@northprint/duckdb-wasm-adapter-svelte';
const db = duckdb({ autoConnect: true });
const users = db.query<User>('SELECT * FROM users');
// users is now typed as QueryStore<User>
</script>2. Handle Loading States
svelte
{#if $users.loading}
<div class="loading-state">
<div class="spinner"></div>
<p>Loading users...</p>
</div>
{:else if $users.error}
<div class="error-state">
<h3>Failed to load users</h3>
<p>{$users.error.message}</p>
<button on:click={() => users.refresh()}>
Try Again
</button>
</div>
{:else}
<!-- Render data -->
{/if}3. Optimize Re-renders
svelte
<script>
import { derived } from 'svelte/store';
// Use derived stores for expensive computations
const userStats = derived(users, ($users) => {
if (!$users.data) return null;
return {
total: $users.data.length,
departments: [...new Set($users.data.map(u => u.department))]
};
});
// Memoize expensive functions
function expensiveCalculation(data) {
// Expensive operation
return result;
}
$: memoizedResult = $users.data ? expensiveCalculation($users.data) : null;
</script>4. Use Actions for Reusability
svelte
<script>
function tooltipAction(node, text) {
// Add tooltip functionality
return {
update(newText) {
// Update tooltip
},
destroy() {
// Cleanup
}
};
}
</script>
<div use:tooltipAction="User information">
<!-- Content -->
</div>This guide covers the essential patterns for building Svelte applications with DuckDB WASM Adapter. The reactive store system makes it intuitive to build responsive, data-driven applications with automatic updates when data changes.