Shared state and context

Svelte · Practical · 13 min

Apps need state that many components read: sessions, carts, themes. Svelte 5 needs no separate store library for it.

Runes work in `.svelte.js` / `.svelte.ts` modules, so shared state can live outside components:

// cart.svelte.js
export const cart = $state({ items: [] });
export function add(item) { cart.items.push(item); }

Any component importing cart sees the same reactive object. One rule: you can't export a `$state` variable that you reassign (export let count = $state(0) then count++ elsewhere won't propagate), because importers receive the value, not the binding. Export an object and mutate its properties, or export getter functions.

Classes are a clean way to bundle state with behaviour: fields declared with $state/$derived become reactive accessors.

Module-level state is a singleton per process. In a server-rendered app that means it is shared between all users' requests, so never keep per-user data there. Use context instead: setContext(key, value) in a parent during initialisation and getContext(key) in any descendant. Context is scoped to the component tree, so on the server each request gets its own. Svelte 5.40+ adds `createContext()`, which returns typed get and set functions (plus a has check) and avoids string keys.

Put a $state object (or class instance) in context so descendants see updates. Svelte stores (writable, $store syntax) still work and remain useful for streams of values, but runes cover most shared-state needs.

Example

// counter.svelte.js
export class Counter {
  count = $state(0);
  double = $derived(this.count * 2);
  increment = () => { this.count++; };   // arrow keeps `this` when passed around
}
export const globalCounter = new Counter();   // singleton (careful with SSR)

// session.js - per-tree state via context (Svelte 5.40+)
import { createContext } from 'svelte';
export const [getSession, setSession] = createContext();

// Layout.svelte <script>
//   import { setSession } from './session.js';
//   let { user, children } = $props();
//   setSession({ get user() { return user; } });   // stays reactive

// Avatar.svelte <script> (any depth below)
//   import { getSession } from './session.js';
//   import { globalCounter } from './counter.svelte.js';
//   const session = getSession();
//   <button onclick={globalCounter.increment}>{session.user.name} {globalCounter.double}</button>

Remember

  • Runes in .svelte.js modules create shared reactive state; export objects, not reassigned variables
  • Classes with $state fields bundle state and methods
  • Use context (setContext/getContext or createContext) for per-tree and SSR-safe state

Watch out for

  • Keeping per-user data in module-level state in an SSR app, leaking it between requests.

Check yourself

  1. Why can't you `export let count = $state(0)` from a module and reassign it with count++ elsewhere?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: svelte.dev/docs/svelte/context