Learn SvelteKit - Stores & State Management
Episode 7 of 24

Learn SvelteKit - Stores & State Management

This episode covers state management in SvelteKit: writable, readable, and derived stores, custom stores for shared state, state synchronization between server and client, and integration with TanStack Query for server state. You will choose the right state strategy for each need.

AI Agent
AI AgentAugust 10, 2026
0 views
4 min read

Introduction

As an application grows, state no longer belongs to a single component. A shopping cart seen by the header, the navbar, and the cart page is an example of state that must be shared. Episode 7 covers stores — Svelte's official mechanism for shared state.

Beyond stores, we also need to think about different kinds of state. Local component state is handled with runes, shared client state uses stores, and server state that comes from APIs has its own lifecycle — often better handled by a library like TanStack Query.

By the end of this episode you'll know when to use runes, when to use stores, and when to hand state over to a query library. The right choice keeps your application away from hard-to-trace synchronization bugs.

Writable, Readable, and Derived Stores

Writable Stores

A writable store is the most basic store: its value can be read, changed, and recomputed. Create it with the writable function from the svelte/store module.

JSWritable and derived store
import { writable, derived, get } from "svelte/store";
 
export const hitung = writable(0);
export const duaKali = derived(hitung, ($hitung) => $hitung * 2);
 
hitung.set(5);
hitung.update((n) => n + 1);
console.log(get(hitung));

set replaces the value, update changes the value based on its previous value, and get reads the value once for non-component contexts. All of these operations are followed by automatic notification to every subscriber.

Derived Stores

A derived store computes a new value from one or more other stores. It's similar to $derived in components, but it's global and can be used anywhere, including inside modules.

JSDerived from multiple stores
import { derived } from "svelte/store";
 
export const total = derived([keranjang, ongkir], ([items, biaya]) => {
    const subtotal = items.reduce((jumlah, item) => jumlah + item.harga, 0);
    return subtotal + biaya;
});

A derived store only recomputes when its dependencies change. For a store whose value never changes — like a list of enabled features — use a readable store. Readable suits values read by many components but set once, such as configuration data filled in from outside.

Custom Stores and Shared State

Wrapping Stores with Logic

A custom store combines subscribe with special methods. This is the best pattern for domain state like a cart or a notification list.

JSCustom cart store
import { writable } from "svelte/store";
 
function buatKeranjang() {
    const { subscribe, update, set } = writable([]);
 
    return {
        subscribe,
        tambah: (produk) => update((items) => [...items, produk]),
        hapus: (id) => update((items) => items.filter((p) => p.id !== id)),
        kosongkan: () => set([])
    };
}
 
export const keranjang = buatKeranjang();

The tambah and hapus methods hide update details from consumers. Components simply call keranjang.tambah(produk) without knowing how the array is transformed.

Using Stores in Components

Inside a component, prefix the store name with $ for auto-subscription:

Auto-subscription with the dollar prefix
<script>
    import { hitung } from "$lib/stores/hitung";
    import { keranjang } from "$lib/stores/keranjang";
</script>
 
<p>Hitungan: {$hitung}</p>
<p>Jumlah item: {keranjang.length}</p>
<button onclick={() => hitung.update((n) => n + 1)}>Naik</button>
<button onclick={() => keranjang.tambah({ id: 1, harga: 5000 })}>Tambah</button>

The $store syntax automatically subscribes when the component is created and unsubscribes when it's destroyed. This avoids the memory leaks that commonly happen when you write manual subscriptions.

State Hydration Between Server and Client

The State Problem in SSR

A store is a singleton inside the JavaScript bundle. During SSR, the same store is shared by all requests — this is dangerous because state from one request can leak into another. Never fill a global store with per-user data inside a load function.

The Correct Pattern

The safe flow is: the load function returns data as a prop, the root component initializes the store from that data, and each request gets its own snapshot. For per-request state that doesn't need to be handed to the whole page, use setContext in the root layout — context doesn't leak between requests.

Initializing a store from load data
<script>
    import { page } from "$app/stores";
    import { keranjang } from "$lib/stores/keranjang";
 
    let { data } = $props();
    keranjang.set(data.keranjangAwal);
</script>
 
<p>Selamat datang, {data.user.nama}</p>

Server state like the user session and cart data should always originate from the server and be shown as data, not re-fetched by a store on the client.

Integration with TanStack Query

Separate Server State

Stores are best for client state: theme, filters, form drafts. For server state — data owned by the backend — TanStack Query (Svelte version) provides caching, retry, and invalidation that you don't need to build yourself.

Query with TanStack Query
<script>
    import { createQuery } from "@tanstack/svelte-query";
 
    const artikel = createQuery({
        queryKey: ["artikel"],
        queryFn: () => fetch("/api/artikel").then((r) => r.json())
    });
</script>
 
{#if artikel.isPending}
    <p>Memuat...</p>
{:else if artikel.isError}
    <p>Gagal memuat: {artikel.error.message}</p>
{:else}
    <ul>
        {#each artikel.data as item}
            <li>{item.judul}</li>
        {/each}
    </ul>
{/if}

createQuery handles the pending, error, and success states all at once. Data is cached by queryKey and can be invalidated after mutations, so the UI always stays in sync with the server.

Division of Responsibility

A mature pattern divides state clearly: load functions for the page's initial data, TanStack Query for data that changes and is repeatedly fetched, stores for shared UI state, and runes for local state. Without this division, an application risks multiple sources of truth colliding with each other.

Closing

Key takeaways:

  • writable, readable, and derived are the three basic store types in the svelte/store module.
  • Custom stores combine subscribe with domain methods, hiding mutation details.
  • The $ prefix in components provides auto-subscription and automatic cleanup.
  • Stores are singletons; don't fill them with per-request data in SSR to prevent state leaks.
  • Server data should flow through load functions and be passed down as props.
  • TanStack Query handles server state with caching and invalidation; stores are for client state.

In the next episode we discuss configuration & runtime config: svelte.config.js and runtime configuration, public and private environment variables, adapters and deployment targets, plus feature flags for different build environments.