DesQTA uses a multi-layered state management approach combining Svelte 5 runes, Svelte stores, and service-level state. This guide explains each layer and when to use them.
┌─────────────────────────────────────┐
│ Component State ($state) │ ← Local, component-specific
├─────────────────────────────────────┤
│ Global Stores (writable) │ ← Shared across components
├─────────────────────────────────────┤
│ Service State (classes) │ ← Business logic state
├─────────────────────────────────────┤
│ Backend State (Rust) │ ← Persistent, secure state
└─────────────────────────────────────┘
Use $state for local component state that doesn't need to be shared.
< script lang = "ts" >
// Simple state
let count = $state ( 0 );
let name = $state ( '' );
let isOpen = $state ( false );
// Complex state
let user = $state < User | null >( null );
let items = $state < Item []>([]);
// Update state
function increment () {
count ++ ;
}
function addItem ( item : Item ) {
items = [ ... items, item];
}
</ script >
< button onclick = {increment} > Count : {count} </ button >
// ✅ Good - Direct assignment
count = 10 ;
items = [ ... items, newItem];
user = { ... user, name: 'New Name' };
// ✅ Good - Array methods that create new arrays
items = items. filter ( i => i.id !== id);
items = items. map ( i => ({ ... i, updated: true }));
// ❌ Avoid - Mutating arrays/objects directly
items. push (newItem); // Won't trigger reactivity
user.name = 'New Name' ; // Won't trigger reactivity
// Initialize from props
let { initialCount } : { initialCount : number } = $props ();
let count = $state (initialCount);
// Initialize from async data
let data = $state < Data | null >( null );
onMount ( async () => {
data = await fetchData ();
});
Use $derived for computed values that depend on other state.
let count = $state ( 0 );
let doubled = $derived (count * 2 );
let isEven = $derived (count % 2 === 0 );
let assessments = $state < Assessment []>([]);
let filter = $state ( '' );
// Filtered and sorted assessments
let filteredAssessments = $derived (
assessments
. filter ( a => a.title. includes (filter))
. sort (( a , b ) => a.dueDate. getTime () - b.dueDate. getTime ())
);
// Count of filtered items
let filteredCount = $derived (filteredAssessments. length );
let user = $state < User | null >( null );
let theme = $state ( 'dark' );
let displayName = $derived (
user ? `${ user . firstName } ${ user . lastName }` : 'Guest'
);
let themeClass = $derived (
`theme-${ theme } ${ user ? 'user-theme' : 'guest-theme'}`
);
let selectedTab = $state < 'list' | 'board' | 'calendar' >( 'list' );
let currentView = $derived (
selectedTab === 'list' ? 'list-view' :
selectedTab === 'board' ? 'board-view' :
'calendar-view'
);
Use $effect for side effects that need to run when state changes.
let count = $state ( 0 );
$effect (() => {
console. log ( 'Count changed:' , count);
// Update DOM, call API, etc.
});
let isActive = $state ( false );
$effect (() => {
if ( ! isActive) return ;
const interval = setInterval (() => {
console. log ( 'Tick' );
}, 1000 );
// Cleanup function
return () => {
clearInterval (interval);
};
});
let user = $state < User | null >( null );
$effect (() => {
if ( ! user) return ;
// Only runs when user exists
loadUserData (user.id);
});
let userId = $state ( 1 );
let includeDetails = $state ( false );
$effect (() => {
// Runs when userId OR includeDetails changes
loadUser (userId, includeDetails);
});
Use Svelte stores for state that needs to be shared across multiple components.
// src/lib/stores/theme.ts
import { writable, derived } from 'svelte/store' ;
export const theme = writable < 'light' | 'dark' | 'system' >( 'system' );
export const accentColor = writable ( '#3b82f6' );
// Derived store
export const themeClass = derived (
theme,
$theme => `theme-${ $theme }`
);
< script lang = "ts" >
import { theme, accentColor } from '$lib/stores/theme' ;
// Auto-subscribe with $ prefix
$ : currentTheme = $theme;
$ : currentAccent = $accentColor;
// Update store
function toggleTheme () {
theme. update ( t => t === 'dark' ? 'light' : 'dark' );
}
function setAccent ( color : string ) {
accentColor. set (color);
}
</ script >
< div class = "theme-{$theme}" >
< p >Current theme : {$theme} </ p >
< p > Accent : {$accentColor} </ p >
< button onclick = {toggleTheme} > Toggle </ button >
</ div >
// src/lib/stores/theme.ts
export const theme = writable < 'light' | 'dark' | 'system' >( 'system' );
export const accentColor = writable ( '#3b82f6' );
export const currentTheme = writable ( 'default' );
export const themeManifest = writable < ThemeManifest | null >( null );
// Load theme from settings
export async function loadTheme () {
const subset = await invoke ( 'get_settings_subset' , { keys: [ 'theme' ] });
theme. set (subset?.theme || 'system' );
}
// src/lib/stores/user.ts
import { writable } from 'svelte/store' ;
import type { UserInfo } from '$lib/services/authService' ;
export const user = writable < UserInfo | undefined >( undefined );
export const isAuthenticated = derived (user, $user => !! $user);
Services can maintain their own internal state for business logic.
// src/lib/services/authService.ts
class AuthService {
private user : UserInfo | null = null ;
private sessionValid : boolean = false ;
async checkSession () : Promise < boolean > {
this .sessionValid = await invoke ( 'check_session_exists' );
return this .sessionValid;
}
async loadUser () : Promise < UserInfo | null > {
if ( ! this .user) {
this .user = await fetchUserInfo ();
}
return this .user;
}
clearUser () {
this .user = null ;
this .sessionValid = false ;
}
}
export const authService = new AuthService ();
< script lang = "ts" >
import { theme } from '$lib/stores/theme' ;
// Sync local state with store
let localTheme = $state ($theme);
// Update both when changed
function updateTheme ( newTheme : 'light' | 'dark' | 'system' ) {
localTheme = newTheme;
theme. set (newTheme);
}
// Subscribe to store changes
$effect (() => {
localTheme = $theme;
});
</ script >
let settings = $state < Settings >({});
// Load from backend
onMount ( async () => {
const backendSettings = await invoke ( 'get_settings' );
settings = backendSettings;
});
// Save to backend when changed
$effect (() => {
if (settings) {
invoke ( 'save_settings' , { settings });
}
});
import { useDataLoader } from '$lib/utils/useDataLoader' ;
let assessments = $state < Assessment []>([]);
onMount ( async () => {
const data = await useDataLoader ({
cacheKey: 'assessments' ,
ttlMinutes: 10 ,
context: 'assessments' ,
functionName: 'loadAssessments' ,
fetcher : async () => {
return await invoke ( 'get_assessments' );
},
onDataLoaded : ( data ) => {
assessments = data;
}
});
});
import { setIdb, getWithIdbFallback } from '$lib/services/idbCache' ;
// Save state
async function saveState ( state : State ) {
await setIdb ( 'myState' , state);
}
// Load state
async function loadState () : Promise < State | null > {
return await getWithIdbFallback ( 'myState' , 'myState' , () => null );
}
let formData = $state ({
name: '' ,
email: '' ,
message: ''
});
let errors = $state < Record < string , string >>({});
let isSubmitting = $state ( false );
async function submit () {
isSubmitting = true ;
errors = {};
try {
await submitForm (formData);
// Reset form
formData = { name: '' , email: '' , message: '' };
} catch (e) {
errors = e.errors;
} finally {
isSubmitting = false ;
}
}
let items = $state < Item []>([]);
let filter = $state ( '' );
let sortBy = $state < 'name' | 'date' >( 'name' );
let filteredItems = $derived (
items
. filter ( item => item.name. includes (filter))
. sort (( a , b ) => {
if (sortBy === 'name' ) {
return a.name. localeCompare (b.name);
}
return a.date. getTime () - b.date. getTime ();
})
);
let isOpen = $state ( false );
let modalData = $state < ModalData | null >( null );
function openModal ( data : ModalData ) {
modalData = data;
isOpen = true ;
}
function closeModal () {
isOpen = false ;
// Clear data after animation
setTimeout (() => {
modalData = null ;
}, 300 );
}
let data = $state < Data | null >( null );
let loading = $state ( false );
let error = $state < string | null >( null );
async function loadData () {
loading = true ;
error = null ;
try {
data = await fetchData ();
} catch (e) {
error = e instanceof Error ? e.message : 'Unknown error' ;
} finally {
loading = false ;
}
}
// ✅ Component state - only used in one component
let isExpanded = $state ( false );
// ✅ Store - shared across components
export const theme = writable ( 'dark' );
// ✅ Service state - business logic
class AuthService {
private user : UserInfo | null = null ;
}
// ✅ Good - Derive instead of storing
let items = $state < Item []>([]);
let filteredItems = $derived (items. filter ( i => i.active));
// ❌ Avoid - Storing derived data
let items = $state < Item []>([]);
let filteredItems = $state < Item []>([]);
// ✅ Good - Local state when possible
let isOpen = $state ( false );
// ❌ Avoid - Global store for local state
export const modalOpen = writable ( false );
// ✅ Good - Type everything
let user = $state < User | null >( null );
let items = $state < Item []>([]);
// ❌ Avoid - Any types
let user = $state < any >( null );
// ❌ Wrong - Mutating array
items. push (newItem);
// ✅ Correct - Creating new array
items = [ ... items, newItem];
// ❌ Wrong - Storing computed value
let doubled = $state ( 0 );
$effect (() => {
doubled = count * 2 ;
});
// ✅ Correct - Using $derived
let doubled = $derived (count * 2 );
// ❌ Wrong - Effect for simple computation
let doubled = $state ( 0 );
$effect (() => {
doubled = count * 2 ;
});
// ✅ Correct - Use $derived
let doubled = $derived (count * 2 );