SvelteKit Fundamentals

Deep dive into SvelteKit 5 and how DesQTA uses it

SvelteKit 5 Overview

DesQTA is built on SvelteKit 5, the latest version of the SvelteKit framework. SvelteKit provides file-based routing, server-side rendering capabilities, and a powerful development experience.

Project Structure

DesQTA follows SvelteKit's standard directory structure:

src/
├── routes/              # File-based routing
│   ├── +layout.svelte  # Root layout
│   ├── +page.svelte    # Homepage
│   ├── +error.svelte   # Error page
│   ├── assessments/    # Assessments route
│   │   └── +page.svelte
│   └── [dynamic]/      # Dynamic routes
├── lib/                 # Shared code
│   ├── components/     # Reusable components
│   ├── services/       # Business logic
│   ├── stores/         # State management
│   └── utils/          # Utilities
└── app.css             # Global styles

File-Based Routing

SvelteKit uses the file system to define routes:

Basic Routes

  • +page.svelte: Page component
  • +layout.svelte: Layout wrapper (applies to child routes)
  • +error.svelte: Error boundary
  • +loading.svelte: Loading state

Route Examples

// src/routes/+page.svelte
// Renders at: /

// src/routes/assessments/+page.svelte
// Renders at: /assessments

// src/routes/assessments/[id]/+page.svelte
// Renders at: /assessments/:id

Layout Hierarchy

<!-- src/routes/+layout.svelte -->
<div class="app-layout">
  <AppHeader />
  <AppSidebar />
  <slot /> <!-- Child routes render here -->
</div>

<!-- src/routes/assessments/+page.svelte -->
<!-- Automatically wrapped by +layout.svelte -->
<div>Assessments content</div>

Svelte 5 Runes

DesQTA uses Svelte 5 runes for reactive state management. Runes are compile-time primitives that provide better performance and type safety.

$state - Mutable State

// Component state
let count = $state(0);
let user = $state<User | null>(null);
let items = $state<string[]>([]);

// Update state
count = 10;
user = { id: 1, name: 'John' };
items = [...items, 'new item'];

Key Points:

  • Automatically reactive
  • Type-safe
  • No need for $: reactive statements
  • Works with any data type

$derived - Computed Values

// Computed from state
let doubled = $derived(count * 2);
let filtered = $derived(items.filter(i => i.startsWith('a')));
let fullName = $derived(user ? `${user.firstName} ${user.lastName}` : '');

// Complex derived state
let sortedAssessments = $derived(
  assessments
    .filter(a => a.dueDate >= today)
    .sort((a, b) => a.dueDate - b.dueDate)
);

Key Points:

  • Automatically updates when dependencies change
  • Memoized (only recomputes when needed)
  • Can reference other $derived values
  • Type-safe

$effect - Side Effects

// Run side effects when state changes
$effect(() => {
  console.log('Count changed:', count);
  // Update DOM, call API, etc.
});

// Cleanup function
$effect(() => {
  const interval = setInterval(() => {
    count++;
  }, 1000);
  
  return () => {
    clearInterval(interval);
  };
});

// Conditional effects
$effect(() => {
  if (user) {
    loadUserData(user.id);
  }
});

Key Points:

  • Runs after DOM updates
  • Can return cleanup function
  • Automatically tracks dependencies
  • Runs synchronously by default

$props - Component Props

// Define props interface
interface Props {
  title: string;
  count?: number;
  items: string[];
}

// Declare props
let { title, count = 0, items }: Props = $props();

// Use in template
<h1>{title}</h1>
<p>Count: {count}</p>

Key Points:

  • Type-safe props
  • Optional props with defaults
  • Destructured assignment
  • No export let needed

Component Patterns

Basic Component

<script lang="ts">
  interface Props {
    title: string;
    count: number;
  }
  
  let { title, count }: Props = $props();
  
  let localState = $state(0);
  let doubled = $derived(count * 2);
  
  $effect(() => {
    console.log('Count changed:', count);
  });
</script>

<div>
  <h1>{title}</h1>
  <p>Count: {count}, Doubled: {doubled}</p>
  <button onclick={() => localState++}>
    Local: {localState}
  </button>
</div>

Component with Slots

<!-- Card.svelte -->
<div class="card">
  <header>
    <slot name="header" />
  </header>
  <main>
    <slot /> <!-- Default slot -->
  </main>
  <footer>
    <slot name="footer" />
  </footer>
</div>

<!-- Usage -->
<Card>
  <svelte:fragment slot="header">
    <h2>Title</h2>
  </svelte:fragment>
  <p>Content</p>
  <svelte:fragment slot="footer">
    <button>Action</button>
  </svelte:fragment>
</Card>

Component with Actions

<script lang="ts">
  function clickOutside(node: HTMLElement) {
    function handleClick(event: MouseEvent) {
      if (!node.contains(event.target as Node)) {
        node.dispatchEvent(new CustomEvent('clickoutside'));
      }
    }
    
    document.addEventListener('click', handleClick);
    
    return {
      destroy() {
        document.removeEventListener('click', handleClick);
      }
    };
  }
</script>

<div use:clickOutside on:clickoutside={() => console.log('Clicked outside')}>
  Content
</div>

Lifecycle Hooks

onMount

import { onMount } from 'svelte';

onMount(async () => {
  // Runs once when component mounts
  const data = await fetchData();
  items = data;
  
  // Return cleanup function
  return () => {
    // Cleanup on unmount
    cleanup();
  };
});

onDestroy

import { onDestroy } from 'svelte';

let interval: ReturnType<typeof setInterval>;

onMount(() => {
  interval = setInterval(() => {
    count++;
  }, 1000);
});

onDestroy(() => {
  clearInterval(interval);
});

BeforeUpdate / AfterUpdate

import { beforeUpdate, afterUpdate } from 'svelte';

beforeUpdate(() => {
  // Runs before DOM updates
  console.log('Before update');
});

afterUpdate(() => {
  // Runs after DOM updates
  console.log('After update');
});

Stores (Global State)

While runes handle component state, DesQTA uses Svelte stores for global state:

// src/lib/stores/theme.ts
import { writable } from 'svelte/store';

export const theme = writable<'light' | 'dark' | 'system'>('system');
export const accentColor = writable('#3b82f6');

// Usage in components
import { theme, accentColor } from '$lib/stores/theme';

// Subscribe
$: currentTheme = $theme;
$: currentAccent = $accentColor;

// Update
theme.set('dark');
accentColor.set('#ff0000');

Derived Stores

import { derived } from 'svelte/store';

export const themeProperties = derived(
  [currentTheme, themeManifest],
  ([$currentTheme, $manifest]) => {
    if (!$manifest) return {};
    return $manifest.customProperties;
  }
);

Data Loading

Page Data Loaders

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

export const load: PageLoad = async ({ fetch, depends }) => {
  depends('app:assessments');
  
  const response = await fetch('/api/assessments');
  const assessments = await response.json();
  
  return {
    assessments
  };
};

// Use in +page.svelte
<script lang="ts">
  import type { PageData } from './$types';
  
  let { data }: { data: PageData } = $props();
  // data.assessments is available
</script>

Layout Data Loaders

// src/routes/+layout.ts
export const load = async () => {
  const user = await getUser();
  return { user };
};

// Available in all child routes

Error Handling

Error Boundaries

<!-- src/routes/+error.svelte -->
<script lang="ts">
  import { page } from '$app/stores';
  import { onMount } from 'svelte';
  
  let { error, status }: { error: Error; status: number } = $props();
</script>

<div class="error-page">
  <h1>{status}</h1>
  <p>{error.message}</p>
  <a href="/">Go Home</a>
</div>

Error Handling in Components

let error = $state<string | null>(null);

try {
  const data = await fetchData();
  items = data;
} catch (e) {
  error = e instanceof Error ? e.message : 'Unknown error';
}

{#if error}
  <ErrorMessage {error} />
{/if}

Form Handling

Form Actions

// src/routes/login/+page.server.ts
import { fail } from '@sveltejs/kit';
import type { Actions } from './$types';

export const actions: Actions = {
  default: async ({ request }) => {
    const data = await request.formData();
    const email = data.get('email');
    const password = data.get('password');
    
    if (!email || !password) {
      return fail(400, { error: 'Missing fields' });
    }
    
    // Authenticate
    const user = await authenticate(email, password);
    
    return { success: true, user };
  }
};

// In +page.svelte
<form method="POST">
  <input name="email" type="email" />
  <input name="password" type="password" />
  <button type="submit">Login</button>
</form>

Advanced Patterns

Async Components

<script lang="ts">
  let promise = $state(fetchData());
  
  async function refetch() {
    promise = fetchData();
  }
</script>

{#await promise}
  <LoadingSpinner />
{:then data}
  <DataDisplay {data} />
{:catch error}
  <ErrorMessage {error} />
{/await}

Keyed Blocks

{#key selectedId}
  <DetailView id={selectedId} />
{/key}

Snippet Props

<!-- Component.svelte -->
<script lang="ts">
  let { children, header }: { 
    children: import('svelte').Snippet;
    header?: import('svelte').Snippet;
  } = $props();
</script>

<div>
  {#if header}
    {@render header()}
  {/if}
  {@render children()}
</div>

Best Practices

1. Use Runes for Component State

// ✅ Good - Use $state
let count = $state(0);

// ❌ Avoid - Don't use reactive statements
let count = 0;
$: doubled = count * 2;

2. Keep Components Focused

// ✅ Good - Single responsibility
<AssessmentCard {assessment} />

// ❌ Avoid - Too many responsibilities
<AssessmentCardWithFiltersAndSorting />

3. Extract Logic to Services

// ✅ Good - Business logic in service
import { assessmentService } from '$lib/services/assessmentService';
const data = await assessmentService.loadAssessments();

// ❌ Avoid - Logic in component
const data = await fetch('/api/assessments').then(r => r.json());

4. Use TypeScript

// ✅ Good - Type everything
interface Assessment {
  id: number;
  title: string;
  dueDate: Date;
}

// ❌ Avoid - Any types
let assessment: any;

Next Steps