Ask
163
@tomato_tobin ·

svelte 5 throws state_unsafe_mutation when i set a count inside $derived Svelte

Svelte 5.20-ish. I have a filtered list and I wanted a count of what survived the filter, so:

let items = $state([]);
let visibleCount = $state(0);
let visible = $derived.by(() => {
  const out = items.filter(i => i.active);
  visibleCount = out.length;
  return out;
});

Dev server dies with state_unsafe_mutation: Updating state inside a derived or template expression is forbidden.

I understand that it's forbidden. I don't understand why. In Svelte 4 this was a reactive statement and it just worked. What's the actual replacement pattern for deriving two things from one computation?

10 answers Share
Report

Answering anonymously — a moderator will review it first.

  • @muslin_mira · 4w ago · 3 replies

    Replacement is: stop having two sources of truth, derive both.

    let items = $state([]);
    let visible = $derived(items.filter(i => i.active));
    let visibleCount = $derived(visible.length);
    

    That's the whole thing. Deriveds are memoised, so deriving from a derived doesn't re-run the filter.

    Why it's forbidden: deriveds in Svelte 5 are pull-based. They run when something reads them, which might be twice, might be never if the component isn't rendering. If a derived writes state, the value of that state depends on whether anything happened to read the derived. That's how you get a bug that only reproduces when a panel is collapsed. Svelte 4's $: had exactly that class of bug, it just never told you about it.

    158
    Share
    Reply

    Answering anonymously — a moderator will review it first.

    Report
    • @tomato_tobin · 4w ago

      'Depends on whether anything read it' — ok, that lands. I'd been thinking of $derived as 'runs when deps change' and it's actually 'runs when someone asks'.

      44
      Share
      Reply

      Answering anonymously — a moderator will review it first.

      Report
    • @muslin_mira · 4w ago

      Right. And if you genuinely need a side effect when something changes, that's $effect, and you should feel mildly guilty every time you write one.

      33
      Share
      Reply

      Answering anonymously — a moderator will review it first.

      Report
  • @drainfield_dez · 4w ago

    If the real computation is heavier than a filter and you really do want one pass producing two values, derive the object:

    let result = $derived.by(() => {
      const out = [];
      let skipped = 0;
      for (const i of items) i.active ? out.push(i) : skipped++;
      return { out, skipped };
    });
    

    Then read result.out and result.skipped. One pass, no state writes. This is the pattern for anything expensive enough that you care.

    61
    Share
    Reply

    Answering anonymously — a moderator will review it first.

    Report
  • @edge_runtime_bo · 4w ago

    Worth knowing the other common source of this exact error, because it catches people who didn't write anything weird in a derived: mutating state from a template expression. A function called inside {#each} that pushes to an array will do it. Same rule, much less obvious. If the stack trace points at your markup rather than at a $derived, that's what happened.

    27
    Share
    Reply

    Answering anonymously — a moderator will review it first.

    Report
  • @oncall_omar · 4w ago · 3 replies

    you can wrap the write in untrack() and it'll stop complaining

    26
    Share
    Reply

    Answering anonymously — a moderator will review it first.

    Report
    • @muslin_mira · 4w ago

      Please don't. untrack stops a read registering as a dependency, it does not make writing state from a derived correct. You still get a value whose freshness depends on read order. It'll work in dev and then serve a stale count in a component that isn't currently on screen.

      22
      Share
      Reply

      Answering anonymously — a moderator will review it first.

      Report
    • @oncall_omar · 4w ago

      Fair, retracting. I'd used it for a legitimate case (reading config inside an effect without subscribing to it) and over-generalised.

      14
      Share
      Reply

      Answering anonymously — a moderator will review it first.

      Report
  • @rubberduck_rae · 4w ago

    Migration note since you came from 4: the whole family of $: total = a + b maps to $derived, and $: doSomething(a) maps to $effect. Most of the pain in migrating is discovering how many of your $: lines were quietly both at once.

    15
    Share
    Reply

    Answering anonymously — a moderator will review it first.

    Report
  • @cookie_domain_al · 4w ago

    Not your bug, but confirm items is actually $state and not something you reassigned out of a prop. Reassigning a prop and expecting it to stay reactive is the other 40% of Svelte 5 confusion on my team.

    9
    Share
    Reply

    Answering anonymously — a moderator will review it first.

    Report