Syed Umar AnisSvelteSvelte Reactivity : Beyond the basics
Syed Umar AnisSvelteSvelte Reactivity : Beyond the basics
SvelteWeb

Svelte Reactivity : Beyond the basics

Svelte 5 introduces a powerful and refined reactivity system that gives developers greater control and clarity. In this post, we’ll explore the key concepts behind Svelte 5’s reactivity model, from derived values to tracking rules and best practices.

$derived Is Only Evaluated If Needed

In Svelte 5, $derived computations are lazy—they are only evaluated when their value is actually needed. This helps avoid unnecessary work and keeps your app fast. If a $derived value is never accessed, it won’t run, no matter how many times its dependencies change.

This behavior encourages a more declarative mindset: you can safely define many derived values without worrying about overhead unless they’re actively used.

const count = $state(0);

const double = $derived(() => {
  console.log('Computing double');
  return count * 2;
});

// double is not computed until accessed

Avoid Side Effects Inside $derived

A crucial rule: don’t put side effects inside $derived. Derived values are meant to be pure computations based on state. Since they might be reevaluated multiple times or not at all, including side effects—like network calls or DOM mutations—can lead to unpredictable behavior.

If you need to perform side effects, use $effect instead.

// ❌ Bad: Side effect inside $derived
const stateValue = $state(1);
const data = $derived(() => {
  fetch(`/api/data/${stateValue}`); // don't do this!
  return stateValue * 2;
});

// ✅ Good: Use $effect for side effects
const stateValue = $state(1);
const date = $derived(stateValue * 2);
$effect(() => {
  fetch('/api/data/${stateValue}');
});

Reactivity Only Tracks Synchronous Code

Svelte’s reactivity system only tracks synchronous dependencies. That means any state accessed inside an await block or after a setTimeout, for example, won’t be tracked automatically.

$effect(() => {
  console.log(stateValue1); // tracked

  await someAsyncCall(); // anything after this isn't tracked
  console.log(stateValue2); // NOT tracked
});

To ensure consistent behavior, access all reactive state before any await or asynchronous operation.

Use untrack to Skip Dependency Tracking

Sometimes, you might want to use a reactive value without having it register as a dependency. That’s where untrack comes in:

$effect(() => {
  console.log(stateValue1); // tracked
  untrack(() => {
    console.log(stateValue2); // NOT tracked
  });
});

This tells Svelte not to rerun any $effect or $derived just because stateValue2 changed—useful when you’re reading reactive values as a snapshot or for logging/debugging purposes.

Dependency Tracking Is Branch-Sensitive

Svelte tracks dependencies per execution. So if your reactive code includes conditional logic, different runs might track different variables depending on which branch is executed.

$derived(() => {
  if (mode === 'advanced') {
    return advancedValue; // only tracked when this branch runs
  } else {
    return simpleValue;
  }
});

This can be a powerful feature, but it also means that changes to previously unused state won’t trigger updates unless the condition flips. Be aware of this if your UI behaves inconsistently.

Functions Can Be Tracked Too

One subtle but important change in Svelte 5 is that functions themselves can be tracked for dependencies—meaning if you access reactive state inside a function that’s called within a reactive block, those dependencies will still be registered.

function compute() {
  return count * 2;
}

$derived(() => compute()); // count is tracked!

This makes it easier to abstract logic without losing reactivity.

$state and $derived in .svelte.js Files

With Svelte 5, you can use $state, $derived and $effect inside a *.svelte.js file.

However, there are some limitations. For instance, $effect only works if it is registered during component initialisation.

So if you try to run an $effect outside of a component’s lifecycle context (e.g. in top-level module scope), it won’t work.

Make changes through reactive proxy

When you make an object reactive, let’s say by wrapping it inside $state(), ensure that you use the object reference returned by $state() to make changes instead of the original object.

This can be a bit confusing in certain situations where the svelte reactivity abstraction can feel a bit leaky. For instance, if you add an object to a reactive array, the original object reference should be discarded and the proxied version should be retrieved from the array again for future use.

Final Thoughts

Svelte 5’s reactivity model is powerful and elegant but demands a bit more understanding than previous versions. Knowing when and how code is tracked can make the difference between a blazing-fast robust app and one full of subtle bugs.

Hi, I’m Umar

Leave a Reply

Your email address will not be published. Required fields are marked *