Skip to main content

Event Handling Standards

Overview

This document defines the standard approach for handling user interactions and events in this Svelte application.

Core Principle: Event-Driven Architecture

Standard: Use explicit event handlers over reactive effects ($effect) for user interactions.

Why:

  • ✅ Clear, predictable flow
  • ✅ Easier to debug
  • ✅ Better control over when logic runs
  • ✅ Avoids reactive loops and race conditions

Event Handler Naming Convention

Standard: Use the handleX naming pattern for all event handlers.

Common Patterns

PatternUse CaseExample
handleXClickClick eventshandleSearchClick()
handleXSubmitForm submissionshandleModalSubmit()
handleXCancelCancel/close actionshandleModalCancel()
handleXKeydownKeyboard eventshandleSearchKeydown()

Example:

function handleSearchClick() {
if (!searchInput.trim()) return;
searchQuery = searchInput;
performSearch();
}

function handleSearchKeydown(event: KeyboardEvent) {
if (event.key === 'Enter') {
handleSearchClick();
}
}

Event-Driven vs Reactive Patterns

Use Event Handlers For:

User interactions - Buttons, forms, keyboard input

function handleSearchClick() {
searchQuery = searchInput;
performSearch();
}

Use $effect Only For:

Side effects - Browser APIs, event listeners, cleanup

$effect(() => {
const handler = () => {
currentPath = window.location.hash || '#/';
};
window.addEventListener('hashchange', handler);
return () => window.removeEventListener('hashchange', handler);
});

Never Use $effect For:

User interaction logic - Use handlers instead

// Bad - Creates reactive loops
$effect(() => {
if (searchInput) {
performSearch();
}
});

// ✅ Good - Explicit control
function handleSearchClick() {
performSearch();
}

Central Logic Functions

Standard: Extract complex logic into functions called by event handlers.

// Event Handlers - Respond to user actions
function handleSearchClick() {
searchQuery = searchInput;
currentPage = 1;
performSearch(); // Call central logic
}

function handlePaginationClick(newPage: number) {
currentPage = newPage;
performSearch(); // Reuse central logic
}

// Core Logic - Called from multiple handlers
async function performSearch() {
// Update URL
const url = new URL(window.location.href);
url.searchParams.set('q', searchQuery);
window.history.pushState({}, '', url);

// Fetch results
isLoading = true;
try {
searchResults = await fetchResults(searchQuery, currentPage);
} finally {
isLoading = false;
}
}

Benefits:

  • Handlers stay simple
  • Logic is reusable
  • Easy to test

Component Event Flow

Standard Flow:

User Action → Event Handler → Update State → Central Logic → UI Updates

Example:

// State
let isModalOpen = $state(false);
let inputValue = $state('');
let isSubmitting = $state(false);

// Handlers
function handleAddClick() {
inputValue = '';
isModalOpen = true;
}

function handleModalCancel() {
inputValue = '';
isModalOpen = false;
}

async function handleModalSubmit() {
if (!inputValue.trim()) return;

isSubmitting = true;
try {
await saveItem(inputValue);
handleModalCancel(); // Close on success
} catch (error) {
console.error('Failed to save:', error);
} finally {
isSubmitting = false;
}
}

Inline vs Named Handlers

Use Named Handlers For:

  • Complex logic (>2 lines)
  • Reused logic
  • Logic that needs testing

Use Inline Handlers For:

  • Simple state toggles: onclick={() => (isOpen = !isOpen)}
  • Simple assignments: onclick={() => (count = 0)}

Form Handling

Standard pattern:

// State
let formData = $state({ name: '', email: '' });
let errors = $state<Record<string, string>>({});
let isSubmitting = $state(false);

// Validation
function validateForm(): boolean {
errors = {};
if (!formData.name.trim()) {
errors.name = 'Name is required';
}
return Object.keys(errors).length === 0;
}

// Handler
async function handleSubmit(event: Event) {
event.preventDefault();
if (!validateForm()) return;

isSubmitting = true;
try {
await saveFormData(formData);
} finally {
isSubmitting = false;
}
}

Lifecycle vs Events

Use onMount For:

  • Initial data loading from URL
  • Component initialization
onMount(() => {
const query = new URLSearchParams(window.location.search).get('q');
if (query) {
searchInput = query;
performSearch();
}
});

Use Event Handlers For:

  • All user interactions after mount

Anti-Patterns to Avoid

Don't use $effect for user interactions

// Bad
$effect(() => {
if (inputValue) doSomething();
});

// ✅ Good
function handleInputChange() {
if (inputValue) doSomething();
}

Don't put complex logic inline

<!-- Bad -->
<Button onclick={() => {
if (!validate()) return;
isSubmitting = true;
fetch('/api/save', { ... })
}}>Save</Button>

<!-- ✅ Good -->
<Button onclick={handleSave}>Save</Button>

Don't forget preventDefault on forms

// Bad
function handleSubmit() {
saveData();
}

// ✅ Good
function handleSubmit(event: Event) {
event.preventDefault();
saveData();
}

Quick Reference

When to Use What

ScenarioSolution
Button clickhandleXClick()
Form submithandleXSubmit(event) with event.preventDefault()
Keyboard shortcuthandleXKeydown(event)
Complex logic reused by handlersCentral function (no handle prefix)
Initial loadonMount()
Browser API side effects$effect() with cleanup
User interaction logicNever $effect()

Examples from Codebase

  • search.svelte - handleSearchClick(), handlePaginationClick(), performSearch()
  • integration.svelte - handleAddClick(), handleModalCancel(), handleModalSubmit()

Last Updated: 2025-12-30