State & Reactivity
Learning Objectives
- You can separate independent state from derived values.
- You can explain reactive dependencies and avoid redundant state.
- You can test calculations separately from component interactions.
A user interface is a view of state. The challenge is keeping that view consistent as its values change. Svelte’s reactivity connects state reads in a component to later updates.
The practice application’s counter already used $state. We now distinguish three things: a stored value, a value calculated from other values, and a side effect outside that calculation.
Store what can change independently
Create client/src/lib/examples/ReadingProgress.svelte:
<script lang="ts"> let completed = $state(0); const total = 5; let remaining = $derived(total - completed); let finished = $derived(remaining === 0);
const completeSection = () => { if (completed < total) completed += 1; };</script>
<p aria-live="polite">{completed} of {total} sections complete</p><p>{remaining} remaining</p><button type="button" onclick={completeSection} disabled={finished}> Complete section</button>Mount the component on /lab as in Chapter 2.3. We use one button only to make changes observable; the next chapter studies event handling in more detail.
completed is independently changeable state. remaining and finished follow from it. Storing all three as independently updated variables would require every handler to update them consistently. An omitted update could produce “5 complete, 1 remaining.”
$state and $derived are Svelte compiler features, not functions imported from a JavaScript utility package. They work in Svelte-processed source. Ordinary TypeScript files do not acquire rune behavior merely because their variables are named similarly.
Read the state reference and derived-state reference for their specific semantics.
Dependencies come from reads
The expression in $derived(total - completed) reads completed. Changing it invalidates the calculation. A plain assignment such as the following is different:
let remaining = total - completed;That initializes a value once. It does not declare that the value must continue following completed.
Similarly, copying a reactive property into an ordinary local variable can create a snapshot rather than a continuing relationship. Ask whether you need the value now or a computation that follows future changes.
Calculations should be free of side effects. A derived value should not also send a request, log an audit event, or increment the state it depends on. Otherwise reading an output can unexpectedly perform work.
Changing board totals
0 / 5 points
A board has open and blocked as independently changing $state
values and a constant capacity. Its available-space display must
follow both values. Which design expresses that relationship?
Objects and arrays
For plain objects and arrays, $state supports deeply reactive changes. For example:
<script lang="ts"> let preferences = $state({ compact: false, fontSize: 16 });</script>
<button type="button" onclick={() => (preferences.compact = !preferences.compact)}> {preferences.compact ? "Use comfortable spacing" : "Use compact spacing"}</button>Do not generalize this to every object in JavaScript. A Date, a Map, or a custom class is not simply the same as a deeply proxied plain object. Prefer small ordinary state shapes while learning the model.
Two instances of ReadingProgress have independent completed values because each instance creates its own state. Moving mutable state to a shared module changes its lifetime and scope. Chapter 2.8 will make that choice explicit.
Two progress panels
0 / 5 points
A page renders two Timer components from the same implementation. Starting the first timer must not start the second. Where should each timer’s changing elapsed value normally be created?
Extract calculations when they deserve their own tests
Suppose a reading plan contains durations. Its remaining-time rule can be tested without rendering a component.
Create client/src/lib/examples/reading.ts:
export type ReadingSection = { minutes: number; complete: boolean };
export const remainingMinutes = (sections: ReadingSection[]): number => { return sections .filter((section) => !section.complete) .reduce((total, section) => total + section.minutes, 0);};Create reading.test.ts beside it:
import { expect, it } from "vitest";import { remainingMinutes } from "./reading";
it("counts only unfinished sections", () => { const sections = [ { minutes: 10, complete: true }, { minutes: 7, complete: false }, { minutes: 3, complete: false }, ]; expect(remainingMinutes(sections)).toBe(10); expect(sections[0].complete).toBe(true);});
it("has no remaining time for an empty plan", () => { expect(remainingMinutes([])).toBe(0);});This function assumes validated nonnegative durations. It is a calculation, not a network boundary validator. State that precondition rather than adding unrelated defensive rules to every small function.
A component can use $derived(remainingMinutes(sections)) while the function remains ordinary TypeScript. A pure test verifies the calculation; a component interaction test verifies that a click results in the expected visible update. Neither replaces the other automatically.
Effects are for synchronization, not duplicate state
Sometimes a component must synchronize with a system outside its declarative output. For example, a temporary development log can observe a value:
<script lang="ts"> let completed = $state(0); $effect(() => { console.log("Completed sections:", completed); });</script>Effects run in the browser and track reactive reads made synchronously in their callback. They can return cleanup when they allocate resources. They are not the normal way to calculate remaining; $derived expresses that relationship directly. Avoid using effects to continually assign state that another effect then assigns back.
We will meet cleanup around network requests later. Do not add an effect to every component simply because the framework provides one. The effect guidance explains both its purpose and common overuse.
Test the state boundary
Add a component test for ReadingProgress that clicks Complete section five times, observes 0 remaining, and finds the button disabled. Use await user.click(...) for each click. Test a second instance or rerender a wrapper to establish that unrelated instances do not share progress.
Temporarily replace the derived remaining with a one-time initialization. Confirm that a test catches the stale output. Restore the intended relationship rather than changing the expected result to match the bug.
Calculate a selection total
Before adding another interactive component, let’s isolate its calculation. The following exercise calculates selected track duration and remaining allowance from arrays. It needs the JavaScript array operations from the primer, not list rendering or input bindings. The next chapter adds those interactions.
Calculate a playlist allowance
0 / 15 points
Task
Implement selectionTotals(tracks, selectedIds, limit) in src/lib/selection.ts.
A track has a unique string id, a title, and positive integer minutes.
The selected IDs are distinct, refer to existing tracks, and their total does
not exceed the nonnegative integer limit. These are input preconditions.
Requirements
Return exactly { usedMinutes, remainingMinutes }. Count only selected tracks,
and subtract their duration from the limit. Empty selection uses zero minutes.
Do not change either input collection or any track. Match by ID rather than by
array position. Do not add Svelte state or a DOM to this calculation.
For tracks lasting 4, 7, and 3 minutes, selecting the first and third with a limit
of 12 gives { usedMinutes: 7, remainingMinutes: 5 }. Other IDs and orders must
work too.
Submit
Submit only src/lib/selection.ts. The grader runs Vitest, awarding 5 points
for selected totals, 5 for empty/exact-limit boundaries, and 5 for varied IDs,
orders, and unchanged inputs. The visual selector comes after list rendering
in the next chapter.
Check Your Understanding
- Which reading-progress values need independent storage, and which can be derived?
- Why does a one-time calculation not track later changes?
- When is a pure-function test more direct than a component test?
- Why is an effect a poor default for calculating a display value?
- What changes when instance state is moved into a module shared by several components?