Feature Image

Svelte 5 States: Avoiding Common Reactivity Traps

Aug 26, 2024

2 min read

Svelte 5 introduces a new reactivity system with $state. While powerful, it comes with some potential pitfalls. Let's explore four common reactivity traps and how to avoid them.

1. The Tick Trap: Updating Form Inputs

When programmatically updating form input values and submitting the form, use await tick() to ensure the DOM has updated before submission.

html
<script>
let form;
let name = $state("");
async function handleSubmit() {
name = "New Name";
await tick(); // Wait for DOM update
form.requestSubmit();
}
</script>
<form bind:this={form}>
<input bind:value={name} name="name" />
<button on:click={handleSubmit}>Submit</button>
</form>

2. Deep Reactivity: Use Svelte-Specific Collections

For reactive updates with collections, Svelte 5 provides its own implementations of common data structures. Instead of using native JavaScript collections, use these Svelte-specific versions to ensure deep reactivity:

  • SvelteSet: A reactive version of JavaScript's Set
  • SvelteMap: A reactive version of JavaScript's Map
  • SvelteURL: A reactive version of the URL object
  • SvelteDate: A reactive version of the Date object

Here's an example using SvelteSet:

html
<script>
import { SvelteSet } from 'svelte/reactivity';
let items = $state(new SvelteSet(['apple', 'banana']));
function addItem(item) {
items.add(item); // This will trigger reactivity
}
function removeItem(item) {
items.delete(item); // This will also trigger reactivity
}
</script>
<ul>
{#each [...items] as item}
<li>{item} <button on:click={() => removeItem(item)}>Remove</button></li>
{/each}
</ul>
<button on:click={() => addItem('cherry')}>Add Cherry</button>

Using these Svelte-specific data structures ensures that any changes to the collections will trigger reactivity throughout your application, even for nested or complex data structures.

3. Preventing Circular Updates in Effects: The Untrack Solution

Use untrack() to prevent infinite update loops when modifying state inside an $effect.

html
<script>
import { untrack } from 'svelte';
let count = $state(0);
$effect(() => {
console.log(`Count is ${count}`);
untrack(() => {
if (count < 5) count++; // Won't trigger the effect again
});
});
</script>
<p>Count: {count}</p>

4. Computed Values

For computed values, use $derived for simple calculations and $derived.by for more complex computations. This prevents circular updates and ensures efficient reactivity.

Simple Derived Values

For straightforward computations, use $derived:

html
<script>
let count = $state(0);
let doubled = $derived(count * 2);
</script>
<p>Count: {count}, Doubled: {doubled}</p>
<button on:click={() => count++}>Increment</button>

Complex Derived Values

For more complex calculations, especially those involving loops or conditionals, use $derived.by:

html
<script>
let numbers = $state([1, 2, 3]);
let total = $derived.by(() => {
let sum = 0;
for (const n of numbers) {
sum += n;
}
return sum;
});
</script>
<button on:click={() => numbers.push(numbers.length + 1)}>
{numbers.join(' + ')} = {total}
</button>

In this example, $derived.by efficiently computes the sum of the numbers array, updating only when the array changes.

Conclusion

By being aware of these traps and using tick(), Svelte-specific data structures, untrack(), and $derived/$derived.by, you can effectively use Svelte 5's reactivity system while avoiding common pitfalls.

For more details, check out the official Svelte 5 preview documentation.