Svelte 11: $state Deep Dive
easy⏱ 5 mincoursesvelte
Deep reactivity via Proxy
Wrapping an object or array literal in $state(...) doesn't just make the top-level binding reactive — Svelte proxies every nested object and array too. Reading team.players[0].goals inside markup registers a dependency on THAT exact property, and writing to it (player.goals++) notifies only the parts of the UI that read it. This is fundamentally different from React's useState, where you must treat state as immutable and replace whole objects/arrays to trigger a re-render.
<script>
let user = $state({ name: 'Ava', prefs: { theme: 'dark' } });
function rename() {
user.name = 'Nova'; // direct mutation — tracked!
}
function toggleTheme() {
user.prefs.theme = user.prefs.theme === 'dark' ? 'light' : 'dark'; // nested mutation — also tracked!
}
</script>
Arrays: push, splice, and index writes all work
Because the array itself is a reactive proxy, mutating methods like push, pop, splice, sort, and direct index assignment (items[i] = ...) are all tracked — Svelte doesn't need you to reassign items = [...items, x] the way vanilla JS reactivity systems often require. Reassignment still works too (items = [] to clear the list); both styles are valid, mutation is just no longer something to avoid.
<script>
let items = $state([1, 2, 3]);
function add() { items.push(items.length + 1); } // tracked
function clear() { items = []; } // reassignment also tracked
</script>
Deep reactivity has a cost — meet $state.raw
Proxying every nested level isn't free: for a huge or deeply nested object you don't need to mutate piecemeal (e.g. data fetched once and replaced wholesale), $state.raw(value) opts OUT of deep reactivity — only reassigning the top-level variable triggers updates, mutations to its nested properties are silently ignored. You won't need it often, but it's the right tool when you're managing large read-mostly data.