Skip to content
AITroveRead. Build. Understand.
Make this comfortable

Svelte Runes: Source State, Derived Views, and Effect Cleanup

Last updated: 5 Oct 20268 min read
tutorial
IntermediateBy AITrove Editorial

Svelte's $state rune declares reactive local state. For arrays and plain objects it can create deep reactive proxies, while $state.raw treats a value as a replaceable unit. The distinction matters when a server snapshot is large: deep tracking permits granular changes, but a read-only snapshot contract may be clearer and cheaper to reason about when each accepted update replaces the array. $derived calculates a read-only result from state it reads. $effect is for work outside that derivation, such as a browser subscription, and can return cleanup. Copying a filtered list and its count through separate effects adds writable states that can temporarily disagree. The case snapshot and search phrase should own the truth; filtered rows and count should be consequences.

Working case

A permit-review queue receives 47,000 authorized summaries. A search input writes its phrase into one variable, an effect copies matching cases into a second variable, and another effect copies the count into a third. A batch arrives between these steps, leaving the heading at 47 while the rows now reflect a new snapshot. The implementation also stores the entire case array in local storage, exposing private titles and causing long synchronous writes. Replacing the chain with one raw snapshot, one phrase, and derived rows makes the visible relationship explicit. A separate browser effect, if needed, stores only a small nonprivate display preference and cleans up any timer it starts.

Implementation boundary

svelte
<script lang="ts">
  type PermitSummary = { id: number; title: string };
  let { initialCases }: { initialCases: PermitSummary[] } = $props();
  let cases = $state.raw<PermitSummary[]>(initialCases);
  let phrase = $state('');
  let visibleCases = $derived(
    cases.filter(permitCase =>
      permitCase.title.toLocaleLowerCase().includes(phrase.trim().toLocaleLowerCase())
    )
  );
  let visibleCount = $derived(visibleCases.length);
  function replaceCase(updated: PermitSummary) {
    cases = cases.map(permitCase => permitCase.id === updated.id ? updated : permitCase);
  }
</script>
<input aria-label="Find permit" value={phrase}
  oninput={(event) => phrase = event.currentTarget.value} />
<p>{visibleCount} permitted cases</p>
<ul>
  {#each visibleCases as permitCase (permitCase.id)}
    <li>{permitCase.title}</li>
  {/each}
</ul>

Choose a state boundary before choosing a rune. A read-only authorized batch can use $state.raw, with immutable replacement when a case changes. A small locally editable form can use deep $state so field edits remain reactive. Compute visible cases from snapshot and phrase; compute the count from those cases. Keep the derivation pure: no network calls, storage writes, or mutation of the batch while filtering. An effect should own only its external subscription or timer and return a cleanup function that runs before replacement and at teardown. If filtering becomes expensive, first measure changed-phrase cost and DOM row count. Server-side paged search, a worker, or a bounded list solves different bottlenecks; none should be added merely because the array is large. Authorization stays with the server snapshot.

Cost and boundaries

A linear search across n summaries takes O(n) time per changed phrase and can allocate O(n) references. Derived caching avoids repeated work when its dependencies have not changed; it cannot make the next changed query constant-time. A raw snapshot avoids deep proxy bookkeeping, but nested mutation will not announce an update, so replacement is part of the contract. A deep proxy supports local field edits and has tracking overhead that grows with the edited graph. A search index costs memory and update work. A timer-based effect that saves a preference adds scheduling and cleanup obligations. Measure input delay, filter duration, rendered-row work, and retained memory separately on the same device.

Failure trace

Type while a new authorized batch arrives and assert that the rows and heading describe one snapshot. Edit a nested case field inside a raw batch without replacing it; that negative test should show why the boundary forbids the mutation. Replace the batch and confirm the view updates. Block browser storage and verify search still works. Mount and destroy the queue repeatedly while an effect owns a timer, then confirm no timer writes after teardown. Clear the phrase and verify that only authorized cases return. Finally, render just 47 rows from the 47,000 records and compare search work with DOM work before choosing a server query.

Verification

  • Rows and count depend on one authorized batch and phrase.
  • Raw snapshot updates replace the declared value.
  • A storage failure never changes the permitted result set.

Practice drill

Begin with 47 summaries and grow to 47,000. Capture filter time, row-render time, and memory. Use one source batch and one phrase, then derive both matching rows and count. Deliberately insert case 81 while a query is active and check agreement. Replace one summary immutably; do not rely on a nested edit of raw state. Add a display-preference effect with a short timer, verify cleanup during rapid typing, and deny storage access. Write down the threshold at which local filtering stops meeting the input-latency budget and specify the server search contract that would replace it.

Decision note

Use raw replacement for server snapshots, deep state for local edits, derived views for calculations, and effects for external synchronization.

Common Mistakes

  • Using effects to copy values that can be derived.
  • Expecting a nested edit inside $state.raw to trigger a view update.
  • Persisting private case records when only a display preference is needed.

Related lessons

Svelte Reactivity and Server Boundaries; Svelte Props, Callbacks, and Keyed Editor Ownership; SvelteKit Form Actions, Validation, and Mutation Replay; SvelteKit Request-Scoped Load and Hydration State; Vue Source State, Computed Views, and Large Snapshots; Angular Signal Sources, Computed Views, and Effect Scope.

Apply and check

Build Project: Svelte permit review and approval action and review Web Development: Svelte reactivity and server boundaries quiz.

web-tech
web-development
Storage details