Reactivity utilities and fine control

Svelte · Advanced · 13 min

Large data, external libraries and custom reactive sources need more control than plain $state.

Deep proxies are convenient but not free. Svelte gives you finer tools.

  • `$state.raw(value)` stores the value without proxying. Only reassignment is reactive (list = [...list, x]); mutations are ignored. Use it for large arrays or API responses you replace wholesale, and for objects from libraries.
  • `$state.snapshot(value)` returns a plain, non-proxied deep copy: pass it to structuredClone, JSON.stringify, charting libraries or anything that dislikes Proxies.
  • `untrack(fn)` from svelte reads state inside an effect or derived without making it a dependency.
  • `svelte/reactivity` provides reactive versions of built-ins: SvelteMap, SvelteSet, SvelteDate, SvelteURL, SvelteURLSearchParams. A plain Map in $state is not made reactive, because only plain objects and arrays are proxied.
  • `MediaQuery` from the same module is a reactive media query (new MediaQuery('(min-width: 800px)').current).
  • `createSubscriber(start)` turns any external event source into something reactive: reading it inside an effect subscribes, and the source is started and stopped automatically as readers come and go. It is how MediaQuery is built.

Equality matters too: setting $state to the same primitive value doesn't trigger updates, and $derived only notifies when its result changes. Class instances (other than those using runes in fields) are not proxied, so store reactive fields on the class itself.

Example

<script>
  import { untrack } from 'svelte';
  import { SvelteMap, MediaQuery, createSubscriber } from 'svelte/reactivity';

  let rows = $state.raw([]);                        // replace, don't mutate
  rows = Array.from({ length: 10_000 }, (_, i) => ({ id: i }));

  const votes = new SvelteMap();                    // reactive Map
  const wide = new MediaQuery('(min-width: 800px)');

  let count = $state(0), logged = $state(0);
  $effect(() => console.log(count, untrack(() => logged)));   // depends on count only

  // Custom reactive source: is the tab visible?
  const subscribe = createSubscriber((update) => {
    document.addEventListener('visibilitychange', update);
    return () => document.removeEventListener('visibilitychange', update);
  });
  const visible = { get current() { subscribe(); return !document.hidden; } };
</script>

<button onclick={() => votes.set('svelte', (votes.get('svelte') ?? 0) + 1)}>
  vote ({votes.get('svelte') ?? 0})</button>
<p>{rows.length} rows, {wide.current ? 'wide' : 'narrow'}, visible: {visible.current}</p>

Remember

  • $state.raw skips proxying: reassign to update
  • $state.snapshot gives a plain copy for libraries and serialisation
  • Use SvelteMap/SvelteSet/SvelteURL for reactive built-ins; untrack to avoid dependencies
  • createSubscriber adapts external event sources

Watch out for

  • Putting a Map in $state and calling .set(), expecting updates; use SvelteMap.

Check yourself

  1. `let data = $state.raw({ n: 1 }); data.n = 2;` Does the UI showing data.n update?

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

Official docs: svelte.dev/docs/svelte/svelte-reactivity