Published 12/5/2025 · 5 min read
Tags: svelte , reactivity , fundamentals
Lesson 3: Reactivity Basics
Reactivity is Svelte’s superpower. When your data changes, your UI updates automatically. But Svelte’s approach is different from other frameworks — and simpler.
How Svelte Tracks Changes
In React, you need useState to make a value reactive. In Vue, you need ref() or reactive(). In Svelte, you just use let:
<script>
let count = 0
</script>
<button onclick={() => count++}>
Clicked {count} times
</button>
That’s it. Svelte sees that count is used in the template and compiles update code for it. When count changes, the button text updates.
This works because Svelte is a compiler. It analyzes your code and generates the exact JavaScript needed to update the DOM when variables change.
The Assignment Rule
Here’s the key insight: Svelte’s reactivity is triggered by assignment.
This updates the UI:
count = count + 1
This also updates the UI:
count++
But this does NOT update the UI:
let numbers = [1, 2, 3]
numbers.push(4) // UI won't update!
Why? Because you haven’t assigned anything to numbers. You’ve mutated the array, but numbers still points to the same array.
The fix is to reassign:
let numbers = [1, 2, 3]
numbers = [...numbers, 4] // UI updates
Or use a shorthand:
numbers.push(4)
numbers = numbers // This triggers the update
The same applies to objects. Mutating a property won’t trigger updates unless you reassign:
let user = { name: 'Alice', age: 30 }
// Won't trigger update:
user.age = 31
// Will trigger update:
user = { ...user, age: 31 }
// Or:
user.age = 31
user = user
Reactive Declarations
What if you have a value that depends on other values? Like a fullName that combines firstName and lastName?
Use $: (called a reactive declaration):
<script>
let firstName = 'Gareth'
let lastName = 'Redfern'
$: fullName = `${firstName} ${lastName}`
</script>
<p>Hello, {fullName}</p>
<input bind:value={firstName} />
<input bind:value={lastName} />
Whenever firstName or lastName changes, fullName automatically updates. Svelte figures out the dependencies for you.
You can do calculations:
<script>
let width = 10
let height = 5
$: area = width * height
$: perimeter = 2 * (width + height)
</script>
Or derive from arrays:
<script>
let items = [
{ name: 'Apples', price: 1.50 },
{ name: 'Bananas', price: 0.75 },
{ name: 'Oranges', price: 2.00 }
]
$: total = items.reduce((sum, item) => sum + item.price, 0)
</script>
Reactive Statements
$: can also run entire blocks of code when dependencies change:
<script>
let count = 0
$: console.log(`count is now ${count}`)
$: if (count > 10) {
alert('Count is getting high!')
count = 0
}
</script>
For multiple statements, use a block:
<script>
let name = ''
$: {
console.log(`Name changed to: ${name}`)
document.title = `Hello, ${name}`
}
</script>
This is useful for side effects — things that need to happen when state changes but aren’t just derived values.
Comparing to Vue
If you’re coming from Vue, here’s how the concepts map:
Vue computed:
const firstName = ref("Gareth");
const lastName = ref("Redfern");
const fullName = computed(() => `${firstName.value} ${lastName.value}`);
Svelte equivalent:
let firstName = 'Gareth'
let lastName = 'Redfern'
$: fullName = `${firstName} ${lastName}`
Vue watch:
watch(count, (newValue) => {
console.log(`count is now ${newValue}`);
});
Svelte equivalent:
$: console.log(`count is now ${count}`)
The Svelte versions are more concise, but the mental model is similar.
Common Patterns
Filtered lists:
<script>
let items = ['Apple', 'Banana', 'Cherry', 'Date']
let search = ''
$: filtered = items.filter(item =>
item.toLowerCase().includes(search.toLowerCase())
)
</script>
<input bind:value={search} placeholder="Search..." />
<ul>
{#each filtered as item}
<li>{item}</li>
{/each}
</ul>
Dependent fetches (be careful with this pattern):
<script>
let userId = 1
let user = null
$: loadUser(userId)
async function loadUser(id) {
const response = await fetch(`/api/users/${id}`)
user = await response.json()
}
</script>
Validations:
<script>
let email = ''
let password = ''
$: emailValid = email.includes('@')
$: passwordValid = password.length >= 8
$: formValid = emailValid && passwordValid
</script>
<button disabled={!formValid}>Submit</button>
Debugging Reactivity
When things aren’t updating as expected, check:
- Are you assigning? Remember, mutation without assignment doesn’t trigger updates.
- Is the variable used in the template? Svelte only tracks variables that appear in your markup.
- Are dependencies clear?
$:tracks variables mentioned in the expression.
You can always add a reactive statement to debug:
$: console.log({ count, items, user })
Key Takeaways
- Variables declared with
letare reactive when used in templates - Reactivity is triggered by assignment, not mutation
- Use
$:for derived values and reactive statements $: derived = expressioncreates a value that auto-updates$: { code }runs code whenever dependencies change- Always reassign arrays and objects after mutation
Related Articles
- x402 with SvelteKit: Full-Stack Example
Build a complete SvelteKit application with x402 payments - wallet connection, protected routes, and automatic payment handling.
- Interacting with Programs from Svelte
Build Svelte components that interact with Solana programs - token balances, transfers, and real-time updates.
- Signing Messages and Transactions in the Browser
Learn to sign messages for authentication and build transactions that users approve through their wallet.