Errors and fixes

Svelte: the store does not update, assignment, update and context

Svelte reacts to assignments, not to mutations inside an object. Change an object in the store without calling `set` or `update` and the UI stays as it is.

Key points

  • A mutation inside an object notifies no subscriber: use `update`, or assign a new object.
  • The ` Svelte store not updating prefix works only inside a component, not in an imported `.js` module.
  • Svelte 5 still supports stores, but `$state` does not replace a shared store on its own.

Short answer: Svelte notifies subscribers only when something calls set or update on the store. Mutate an object inside it and the reference stays the same, so the UI notices nothing.

The mechanism: the store does not look inside

A store is a box with two things in it: a value and a list of listening functions. The box has no eyes. It cannot know you called push on an array it happens to hold, because nobody tells it: notification starts at the set call, not at the content.

The compiler applies the same logic to local state: it rewrites assignments into invalidations. A method that mutates an array is not an assignment, so it produces no invalidation. That is why the bug looks identical in a store and in a component.

import { writable } from "svelte/store";

export const cart = writable({ items: [] });

// non notifica nessuno: il riferimento non cambia
cart.value.items.push(item);

cart.value does not exist: the store never exposes the value as a property. The real version of the bug is quieter and calls get(cart).items.push(item), which returns the new array length and emits nothing.

Diagnosis: ask the store, not the UI

Before touching the component, check whether the store emitted. A manual subscription with a log separates the two halves of the problem in a minute, without opening the debugger.

import { get } from "svelte/store";
import { cart } from "$lib/stores/cart";

const stop = cart.subscribe((value) => {
  console.log("emit", value.items.length);
});

get(cart).items.push("probe"); // nessun log: la scrittura non emette
stop();

No log means the fault is in the write and the UI is doing its job. A log with stale text means the component reads a copy: const items = $cart.items evaluates the value once and never follows later changes to that field.

The fix: a new object

// 1. update: il modo idiomatico per gli oggetti
cart.update((c) => ({ ...c, items: [...c.items, item] }));

// 2. set con un nuovo riferimento
cart.set({ items: [...get(cart).items, item] });

Both lines build a new array inside a new object. The UI responds because the identity changed, not because the content differs. That is not a style preference: it is the contract, and it holds for set called from a module too.

The $ prefix and the compiler boundary


  import { cart } from "$lib/stores/cart";


{$cart.items.length} articoli

The $ is syntax sugar: the compiler replaces it with a subscription and unsubscribes on unmount. Outside a component it does not exist. In a .js module use get(cart) for a single read, or subscribe when you need the stream.

Svelte 5: runes and stores side by side

$state covers the local state of one component; a store is still the tool for state shared between distant components. The $store syntax keeps working inside .svelte files, while in a module the rune does not apply. Pick one model per kind of state and write it in the readme, or every new file reopens the debate.

When the usual fix makes it worse

The most common shortcut is to mutate inside update and return the same object. The store emits, the component reading it re-renders, and the case looks closed. Memoised children still receive the previous reference and skip their render, so the symptom moves one level down and reappears in a table that will not refresh.

The second shortcut is a deep clone on every write, through structuredClone or a JSON round trip. It loses Date, Map and class instances, and on a long list it costs a full walk for each keystroke.

One limit to budget for: no granularity

Stores notify per value, not per field. When a single store holds filters, rows and selection, every write wakes every subscriber. There is no native selector like the ones state-management libraries offer, so granularity comes from splitting the store. On a large catalogue the cost shows in the component profiler, not in the console.

Prevention

  • One file per store, exported once: two writable values with the same name are two different stores.
  • Always return a new object from update, even when you touch a single field.
  • In tests, update the store and assert the rendered text, not the value read through get.
  • Split the store when two parts of the UI read fields that never affect each other.

Where to go next

In the catalogue: components, pricing block and blocks. The guides: accessibility checklist and UI kit, component library or design system. The comparison: copy-paste or headless.

Frequently asked questions

Why does store.value = x not update the UI?

Because that property does not exist on the store: the value is read with get and written with set. An object without a set call never emits, so no subscriber runs.

Are update and set equivalent?

No. set replaces the value with the one it receives, update receives the current one and returns a new one. With nested objects update saves you a separate read.

Why is $store undefined in a .js file?

The dollar syntax is compiled inside components only. In a module you import the store and call get, or subscribe and unsubscribe when you are done.

Must I rewrite everything with runes in Svelte 5?

No. Stores remain supported and are still the natural choice for shared state. Runes cover local state; they do not replace a global container.

How do I debug a missing update?

Add a subscription with a log call, then perform one write. No log at all points at the write; a log with a static page points at the component reading a local copy.