Skip to content

Cross-Component State

The simplest cross-component state is a reactive value exported from a .svelte.js module. Every importer shares one instance:

theme.svelte.js
export const theme = $state({ mode: 'light' });
export function toggle() {
theme.mode = theme.mode === 'light' ? 'dark' : 'light';
}

Any component imports theme and toggle and stays in sync. This is ideal for genuinely global, single-instance state: the color theme, a toast queue, a feature-flag set.

There’s a catch that matters the moment you use SvelteKit (or any SSR). A module-level $state is a single instance shared by everything that imports it — including, on the server, every user request handled by the same process. Put a logged-in user in a module-level singleton and, under SSR, one request can see another user’s data.

Pattern 2: per-subtree instances with context

Section titled “Pattern 2: per-subtree instances with context”

When you need state that is shared within a subtree but isolated per instance — and safe under SSR — combine a runed class/object with setContext / getContext. The parent creates the state and puts it in context; descendants read it. Each component instance gets its own, created during that request’s render.

Parent.svelte
<script>
import { setContext } from 'svelte';
// created fresh per instance / per request — not a module singleton
const cart = $state({ items: [] });
setContext('cart', cart);
</script>
<slot />
DeepChild.svelte
<script>
import { getContext } from 'svelte';
const cart = getContext('cart'); // the same reactive object the parent set
</script>
<button onclick={() => cart.items.push(item)}>add ({cart.items.length})</button>

Because the state is created inside the component tree (not at module load), each render — and each SSR request — gets its own copy. A common idiom is a small factory (createCart()) that returns runed state, called in the parent and shared via context.

flowchart TB
  local["Just one component? local $state"] --> up["A few related components? lift $state to the parent, pass props"]
  up --> ctx["A subtree, per-instance / per-user? setContext + runed state"]
  ctx --> mod["Truly global, single instance? export $state from a .svelte.js module"]
  mod --> store["Need the subscribe contract? a store"]
Where each sharing pattern fits
  1. Local$state in the component. Start here.
  2. Lift — move $state to the closest common parent, pass down as props. For a handful of related components.
  3. ContextsetContext/getContext with runed state for a subtree that needs its own instance (and SSR safety).
  4. Module — export $state from a .svelte.js module for truly global, single-instance state.
  5. Store — when you need the subscribe contract (interop, external push sources).

Reach for the lowest level that solves your problem; escalate only when you must.

What is the SSR singleton trap?
How do you share state within a subtree safely per instance / per request?
What kind of state is a `.svelte.js` module singleton appropriate for?
What is the guiding principle for choosing a sharing pattern?