Svelte 20: untrack & tick — Utility Functions
easy⏱ 5 mincoursesvelte
untrack breaks an otherwise-infinite reactive loop
Imagine an $effect that reads count to decide whether to log something, but ALSO wants to update a history array with the current count — if that write is read reactively elsewhere and loops back, you can get runaway re-runs. Wrapping the read in untrack(() => count) tells Svelte 'use this value, but don't treat it as a reason to re-run me' — perfect for effects that need to read state for bookkeeping without becoming dependent on it.
<script>
import { untrack } from 'svelte';
let count = $state(0);
let renderCount = $state(0);
$effect(() => {
count; // tracked: effect reruns when count changes
// reading renderCount normally here would make this effect
// depend on its OWN write below -> infinite loop
const current = untrack(() => renderCount);
renderCount = current + 1;
});
</script>
tick() awaits the next DOM flush
State changes inside Svelte are batched — several synchronous assignments in a row produce ONE DOM update, not one per assignment, for performance. That means immediately after changing state, the DOM hasn't caught up yet. await tick() pauses your function until that pending DOM update has actually happened, which matters when your very next line needs to read a freshly-rendered element (its height, whether it now overflows, moving focus into it).
<script>
import { tick } from 'svelte';
let items = $state(['a', 'b']);
let listEl;
async function addAndFocus(item) {
items.push(item);
await tick(); // wait for the new <li> to actually be in the DOM
listEl.lastElementChild?.focus();
}
</script>
Putting both together: a capped activity log
The demo below adds a new activity entry every time you click a button, and uses an $effect that keeps a rolling 'total events ever logged' counter via untrack (so the counter itself doesn't cause the effect to loop), while a separate handler uses tick() to wait for the new <li> to be painted before scrolling it into view and briefly highlighting it. This is deliberately the most advanced lesson in the module — it combines runes, DOM timing, and dependency control in one small, real component.
Reach for these last, not first
untrack and tick solve real problems, but if you find yourself needing them often, it's worth asking whether the surrounding design could be simpler — most components never need either one. Keep them in your back pocket for the moments described above: breaking a self-referential effect, and syncing imperative DOM work to Svelte's batched update cycle.