synops/docs/erfaringer/svelte5_reaktivitet.md
vegard 0a467066ba Synops v2: arkitektur, retninger og dokumentasjon
Nystart basert på arkitektonisk innsikt fra Sidelinja v1.
Koden er ny, visjon og primitiver er validert gjennom tidligere arbeid.

Inneholder:
- Komplett arkitekturdokumentasjon (docs/arkitektur.md)
- 6 vedtatte retninger (docs/retninger/)
- Alle concepts, features, proposals og erfaringer fra v1
- Server-oppsett og drift (docs/setup/)
- LiteLLM-konfigurasjon (API-nøkler via env)
- Editor.svelte referanse fra v1

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-17 06:43:08 +01:00

2.5 KiB

Erfaring: Svelte 5 Reaktivitet

1. $state i .svelte.ts krever getters

Svelte 5 sin $state lager reaktive proxyer. Når en funksjon returnerer et objekt med $state-verdier, mister man reaktiviteten hvis man returnerer verdien direkte:

// FEIL — mister reaktivitet, verdien fryses ved retur
function createThing() {
  let count = $state(0);
  return { count };  // snapshot, ikke reaktiv
}

// RIKTIG — getter bevarer proxy-tilgang
function createThing() {
  let count = $state(0);
  return {
    get count() { return count; }
  };
}

Referanse: web/src/lib/chat/pg.svelte.ts — alle returnerte verdier bruker getters.

2. SSR kjører alt utenfor onMount

SvelteKit server-renderer kjører komponent-script ved SSR. Alt som bruker browser-APIer (WebSocket, fetch til relative URLer, setInterval, sessionStorage) krasjer på serveren hvis det ikke er beskyttet.

// FEIL — krasjer ved SSR
let chat = createChat(channelId);  // kjøres på server

// RIKTIG — kun i browser
import { onMount } from 'svelte';
let chat = $state<ChatConnection | null>(null);
onMount(() => {
  chat = createChat(channelId);
  return () => chat?.destroy();
});

Alternativt kan factory-funksjonen selv sjekke:

import { browser } from '$app/environment';
if (browser) { /* ... */ }

Referanse: web/src/lib/chat/create.svelte.tsbrowser-guard i factory, web/src/lib/blocks/ChatBlock.svelteonMount for opprettelse.

3. $derived og $effect med null-initialisert state

Når en $state-variabel starter som null (fordi den settes i onMount), må $derived og $effect håndtere null-tilfellet:

let chat = $state<ChatConnection | null>(null);
let messages = $derived(chat?.messages ?? []);  // fallback til tom liste

$effect(() => {
  const count = messages.length;  // trygt, alltid array
  if (count > prevCount) scrollToBottom();
  prevCount = count;
});

Referanse: web/src/lib/blocks/ChatBlock.svelte$derived med optional chaining.

4. Polling: full-fetch slår inkrementell akkumulering

Vi prøvde først å bruke en latestTimestamp-cursor for å hente kun nye meldinger og appende dem. Dette ga duplikater — telleren vokste mens man tastet (polling i bakgrunnen).

Løsning: Enkel refresh() som alltid henter full liste og erstatter messages i sin helhet. For et lite volum meldinger er dette enklere og tryggere enn inkrementell logikk.

Referanse: web/src/lib/chat/pg.svelte.tsrefresh() gjør full fetch, ingen akkumulering.