Skip to content

Commit e19136a

Browse files
committed
side effects
1 parent 6783e77 commit e19136a

File tree

1 file changed

+367
-0
lines changed

1 file changed

+367
-0
lines changed

apps/svelte.dev/content/docs/svelte/03-runes/02-side-effects.md

Lines changed: 367 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,370 @@ title: Side effects
44

55
- `$effect` (.pre)
66
- when not to use it, better patterns for what to do instead
7+
8+
Side effects play a crucial role in applications. They are triggered by state changes and can then interact with external systems, like logging something, setting up a server connection or synchronize with a third-party library that has no knowledge of Svelte's reactivity model.
9+
10+
## `$effect` fundamentals
11+
12+
To run _side-effects_ when the component is mounted to the DOM, and when values change, we can use the `$effect` rune ([demo](/#H4sIAAAAAAAAE31T24rbMBD9lUG7kAQ2sbdlX7xOYNk_aB_rQhRpbAsU2UiTW0P-vbrYubSlYGzmzMzROTPymdVKo2PFjzMzfIusYB99z14YnfoQuD1qQh-7bmdFQEonrOppVZmKNBI49QthCc-OOOH0LZ-9jxnR6c7eUpOnuv6KeT5JFdcqbvbcBcgDz1jXKGg6ncFyBedYR6IzLrAZwiN5vtSxaJA-EzadfJEjKw11C6GR22-BLH8B_wxdByWpvUYtqqal2XB6RVkG1CoHB6U1WJzbnYFDiwb3aGEdDa3Bm1oH12sQLTcNPp7r56m_00mHocSG97_zd7ICUXonA5fwKbPbkE2ZtMJGGVkEdctzQi4QzSwr9prnFYNk5hpmqVuqPQjNnfOJoMF22lUsrq_UfIN6lfSVyvQ7grB3X2mjMZYO3XO9w-U5iLx42qg29md3BP_ni5P4gy9ikTBlHxjLzAtPDlyYZmRdjAbGq7HprEQ7p64v4LU_guu0kvAkhBim3nMplWl8FreQD-CW20aZR0wq12t-KqDWeBywhvexKC3memmDwlHAv9q4Vo2ZK8KtK0CgX7u9J8wXbzdKv-nRnfF_2baTqlYoWUF2h5efl9-n0O6koAMAAA==)):
13+
14+
```svelte
15+
<script>
16+
let size = $state(50);
17+
let color = $state('#ff3e00');
18+
19+
let canvas;
20+
21+
$effect(() => {
22+
const context = canvas.getContext('2d');
23+
context.clearRect(0, 0, canvas.width, canvas.height);
24+
25+
// this will re-run whenever `color` or `size` change
26+
context.fillStyle = color;
27+
context.fillRect(0, 0, size, size);
28+
});
29+
</script>
30+
31+
<canvas bind:this={canvas} width="100" height="100" />
32+
```
33+
34+
The function passed to `$effect` will run when the component mounts, and will re-run after any changes to the values it reads that were declared with `$state` or `$derived` (including those passed in with `$props`). Re-runs are batched (i.e. changing `color` and `size` in the same moment won't cause two separate runs), and happen after any DOM updates have been applied.
35+
36+
You can return a function from `$effect`, which will run immediately before the effect re-runs, and before it is destroyed ([demo](/#H4sIAAAAAAAAE42SzW6DMBCEX2Vl5RDaVCQ9JoDUY--9lUox9lKsGBvZC1GEePcaKPnpqSe86_m0M2t6ViqNnu0_e2Z4jWzP3pqGbRhdmrHwHWrCUHvbOjF2Ei-caijLTU4aCYRtDUEKK0-ccL2NDstNrbRWHoU10t8Eu-121gTVCssSBa3XEaQZ9GMrpziGj0p5OAccCgSHwmEgJZwrNNihg6MyhK7j-gii4uYb_YyGUZ5guQwzPdL7b_U4ZNSOvp9T2B3m1rB5cLx4zMkhtc7AHz7YVCVwEFzrgosTBMuNs52SKDegaPbvWnMH8AhUXaNUIY6-hHCldQhUIcyLCFlfAuHvkCKaYk8iYevGGgy2wyyJnpy9oLwG0sjdNe2yhGhJN32HsUzi2xOapNpl_bSLIYnDeeoVLZE1YI3QSpzSfo7-8J5PKbwOmdf2jC6JZyD7HxpPaMk93aHhF6utVKVCyfbkWhy-hh9Z3o_2nQIAAA==)).
37+
38+
```svelte
39+
<script>
40+
let count = $state(0);
41+
let milliseconds = $state(1000);
42+
43+
$effect(() => {
44+
// This will be recreated whenever `milliseconds` changes
45+
const interval = setInterval(() => {
46+
count += 1;
47+
}, milliseconds);
48+
49+
return () => {
50+
// if a callback is provided, it will run
51+
// a) immediately before the effect re-runs
52+
// b) when the component is destroyed
53+
clearInterval(interval);
54+
};
55+
});
56+
</script>
57+
58+
<h1>{count}</h1>
59+
60+
<button onclick={() => (milliseconds *= 2)}>slower</button>
61+
<button onclick={() => (milliseconds /= 2)}>faster</button>
62+
```
63+
64+
## `$effect` dependencies
65+
66+
`$effect` automatically picks up any reactivy values (`$state`, `$derived`, `$props`) that are _synchronously_ read inside its function body and registers them as dependencies. When those dependencies change, the `$effect` schedules a rerun.
67+
68+
Values that are read asynchronously — after an `await` or inside a `setTimeout`, for example — will _not_ be tracked. Here, the canvas will be repainted when `color` changes, but not when `size` changes ([demo](/#H4sIAAAAAAAAE31T24rbMBD9lUG7kCxsbG_LvrhOoPQP2r7VhSjy2BbIspHGuTT436tLnMtSCiaOzpw5M2dGPrNaKrQs_3VmmnfIcvZ1GNgro9PgD3aPitCdbT8a4ZHCCiMH2pS6JIUEVv5BWMOzJU64fM9evswR0ave3EKLp7r-jFm2iIwri-s9tx5ywDPWNQpaLl9gvYFz4JHotfVqmvBITi9mJA3St4gtF5-qWZUuvEQo5Oa7F8tewT2XrIOsqL2eWpRNS7eGSkpToFZaOEilwODKjBoOLWrco4FtsLQF0XLdoE2S5LGmm6X6QSflBxKod8IW6afssB8_uAslndJuJNA9hWKw9VO91pmJ92XunHlu_J1nMDk8_p_8q0hvO9NFtA47qavcW12fIzJBmM26ZG9ZVjKIs7ke05hdyT0Ixa11Ad-P6ZUtWbgNheI7VJvYQiH14Bz5a-SYxvtwIqHonqsR12ff8ORkQ-chP70T-L9eGO4HvYAFwRh9UCxS13h0YP2CgmoyG5h3setNhWZF_ZDD23AE2ytZwZMQ4jLYgVeV1I2LYgfZBey4aaR-xCppB8VPOdQKjxes4UMgxcVcvwHf4dzAv9K4ko1eScLO5iDQXQFzL5gl7zdJt-nZnXYfbddXspZYsZzMiNPv6S8Bl41G7wMAAA==)):
69+
70+
```ts
71+
// @filename: index.ts
72+
declare let canvas: {
73+
width: number;
74+
height: number;
75+
getContext(type: '2d', options?: CanvasRenderingContext2DSettings): CanvasRenderingContext2D;
76+
};
77+
declare let color: string;
78+
declare let size: number;
79+
80+
// ---cut---
81+
$effect(() => {
82+
const context = canvas.getContext('2d');
83+
context.clearRect(0, 0, canvas.width, canvas.height);
84+
85+
// this will re-run whenever `color` changes...
86+
context.fillStyle = color;
87+
88+
setTimeout(() => {
89+
// ...but not when `size` changes
90+
context.fillRect(0, 0, size, size);
91+
}, 0);
92+
});
93+
```
94+
95+
An effect only reruns when the object it reads changes, not when a property inside it changes. (If you want to observe changes _inside_ an object at dev time, you can use [`$inspect`](/docs/svelte/misc/debugging#$inspect).)
96+
97+
```svelte
98+
<script>
99+
let state = $state({ value: 0 });
100+
let derived = $derived({ value: state.value * 2 });
101+
102+
// this will run once, because `state` is never reassigned (only mutated)
103+
$effect(() => {
104+
state;
105+
});
106+
107+
// this will run whenever `state.value` changes...
108+
$effect(() => {
109+
state.value;
110+
});
111+
112+
// ...and so will this, because `derived` is a new object each time
113+
$effect(() => {
114+
derived;
115+
});
116+
</script>
117+
118+
<button onclick={() => (state.value += 1)}>
119+
{state.value}
120+
</button>
121+
122+
<p>{state.value} doubled is {derived.value}</p>
123+
```
124+
125+
## When not to use `$effect`
126+
127+
In general, `$effect` is best considered something of an escape hatch — useful for things like analytics and direct DOM manipulation — rather than a tool you should use frequently. In particular, avoid using it to synchronise state. Instead of this...
128+
129+
```svelte
130+
<script>
131+
let count = $state(0);
132+
let doubled = $state();
133+
134+
// don't do this!
135+
$effect(() => {
136+
doubled = count * 2;
137+
});
138+
</script>
139+
```
140+
141+
...do this:
142+
143+
```svelte
144+
<script>
145+
let count = $state(0);
146+
let doubled = $derived(count * 2);
147+
</script>
148+
```
149+
150+
> For things that are more complicated than a simple expression like `count * 2`, you can also use [`$derived.by`](#$derived-by).
151+
152+
You might be tempted to do something convoluted with effects to link one value to another. The following example shows two inputs for "money spent" and "money left" that are connected to each other. If you update one, the other should update accordingly. Don't use effects for this ([demo](/#H4sIAAAAAAAACpVRy2rDMBD8lWXJwYE0dg-9KFYg31H3oNirIJBlYa1DjPG_F8l1XEop9LgzOzP7mFAbSwHF-4ROtYQCL97jAXn0sQh3skx4wNANfR2RMtS98XyuXMWWGLhjZUHCa1GcVix4cgwSdoEVU1bsn4wl_Y1I2kS6inekNdWcZXuQZ5giFDWpfwl5WYyT2fynbB1g1UWbTVbm2w6utOpKNq1TGucHhri6rLBX7kYVwtW4RtyVHUhOyXeGVj3klLxnyJP0i8lXNJUx6en-v6A48K85kTimpi0sYj-yAo-Wlh9FcL1LY4K3ahSgLT1OC3ZTXkBxfKN2uVC6T5LjAduuMdpQg4L7geaP-RNHPuClMQIAAA==)):
153+
154+
```svelte
155+
<script>
156+
let total = 100;
157+
let spent = $state(0);
158+
let left = $state(total);
159+
160+
$effect(() => {
161+
left = total - spent;
162+
});
163+
164+
$effect(() => {
165+
spent = total - left;
166+
});
167+
</script>
168+
169+
<label>
170+
<input type="range" bind:value={spent} max={total} />
171+
{spent}/{total} spent
172+
</label>
173+
174+
<label>
175+
<input type="range" bind:value={left} max={total} />
176+
{left.value}/{total} left
177+
</label>
178+
```
179+
180+
Instead, use callbacks where possible ([demo](/#H4sIAAAAAAAACo2SP2-DMBDFv8rp1CFR84cOXQhU6p6tY-ngwoEsGWPhI0pk8d0rG5yglqGj37v7veMJh7VUZDH9dKhFS5jiuzG4Q74Z_7AXUky4Q9sNfemVzJa9NPxW6IIVMXDHQkEOL0lyipo1pBlyeLIsmDbJ9u4oqhdG2A2mLrgedMmy0zCYSjB9eMaGtuC8WXBkPtOBRd8QHy5CDXSa3Jk7HbOfDgjWuAo_U71kz9vr6Bgc2X44orPjow2dKfFNKhSTSW0GBl9iXmAvdEMFQqDmLgBH6HQYyt3ie0doxTV3IWqEY2DN88eohqePvsf9O9mf_if4HMSVXD89NfEI99qvbMs3RdPv4MXYaSWtUeKWQq3oOlfZCJNCcnildlFgWMcdtl0la0kVptwPNH6NP_uzV0acAgAA)):
181+
182+
```svelte
183+
<script>
184+
let total = 100;
185+
let spent = $state(0);
186+
let left = $state(total);
187+
188+
function updateSpent(e) {
189+
spent = +e.target.value;
190+
left = total - spent;
191+
}
192+
193+
function updateLeft(e) {
194+
left = +e.target.value;
195+
spent = total - left;
196+
}
197+
</script>
198+
199+
<label>
200+
<input
201+
type="range"
202+
value={spent}
203+
oninput={updateSpent}
204+
max={total}
205+
/>
206+
{spent}/{total} spent
207+
</label>
208+
209+
<label>
210+
<input
211+
type="range"
212+
value={left}
213+
oninput={updateLeft}
214+
max={total}
215+
/>
216+
{left}/{total} left
217+
</label>
218+
```
219+
220+
If you need to use bindings, for whatever reason (for example when you want some kind of "writable `$derived`"), consider using getters and setters to synchronise state ([demo](/#H4sIAAAAAAAACo2SQW-DMAyF_4pl7dBqXcsOu1CYtHtvO44dsmKqSCFExFRFiP8-xRCGth52tJ_9PecpA1bakMf0Y0CrasIU35zDHXLvQuGvZJhwh77p2nPoZP7casevhS3YEAM3rAzk8Jwkx9jzjixDDg-eFdMm2S6KoWolyK6ItuCqs2fWjYXOlYrpPTA2tIUhiAVH5iPtWbUX4v1VmY6Okzpzp2OepgNEGu_CT1St2fP2fXQ0juwwHNHZ4ScNmxn1RUaCybR1HUMIMS-wVfZCBYJQ80GAIzRWhvJh9d4RanXLB7Ea4SCsef4Qu1IG68Xu387h9D_GJ2ne8ZXpxTZUv1w994amjxCaMc1Se2dUn0Jl6DaHeFEuhWT_QvUqOlnHHdZNqStNJabcdjR-jt8IbC-7lgIAAA==)):
221+
222+
```svelte
223+
<script>
224+
let total = 100;
225+
let spent = $state(0);
226+
227+
let left = {
228+
get value() {
229+
return total - spent;
230+
},
231+
set value(v) {
232+
spent = total - v;
233+
}
234+
};
235+
</script>
236+
237+
<label>
238+
<input type="range" bind:value={spent} max={total} />
239+
{spent}/{total} spent
240+
</label>
241+
242+
<label>
243+
<input type="range" bind:value={left.value} max={total} />
244+
{left.value}/{total} left
245+
</label>
246+
```
247+
248+
If you absolutely have to update `$state` within an effect and run into an infinite loop because you read and write to the same `$state`, use [untrack](functions#untrack).
249+
250+
## `$effect.pre`
251+
252+
In rare cases, you may need to run code _before_ the DOM updates. For this we can use the `$effect.pre` rune:
253+
254+
```svelte
255+
<script>
256+
import { tick } from 'svelte';
257+
258+
let div;
259+
let messages = [];
260+
261+
// ...
262+
263+
$effect.pre(() => {
264+
if (!div) return; // not yet mounted
265+
266+
// reference `messages` so that this code re-runs whenever it changes
267+
messages;
268+
269+
// autoscroll when new messages are added
270+
if (
271+
div.offsetHeight + div.scrollTop >
272+
div.scrollHeight - 20
273+
) {
274+
tick().then(() => {
275+
div.scrollTo(0, div.scrollHeight);
276+
});
277+
}
278+
});
279+
</script>
280+
281+
<div bind:this={div}>
282+
{#each messages as message}
283+
<p>{message}</p>
284+
{/each}
285+
</div>
286+
```
287+
288+
Apart from the timing, `$effect.pre` works exactly like [`$effect`](#$effect) — refer to its documentation for more info.
289+
290+
## `$effect.tracking`
291+
292+
The `$effect.tracking` rune is an advanced feature that tells you whether or not the code is running inside a tracking context, such as an effect or inside your template ([demo](/#H4sIAAAAAAAAE3XP0QrCMAwF0F-JRXAD595rLfgdzodRUyl0bVgzQcb-3VYFQfExl5tDMgvrPCYhT7MI_YBCiiOR2Aq-UxnSDT1jnlOcRlMSlczoiHUXOjYxpOhx5-O12rgAJg4UAwaGhDyR3Gxhjdai4V1v2N2wqus9tC3Y3ifMQjbehaqq4aBhLtEv_Or893icCsdLve-Caj8nBkU67zMO5HtGCfM3sKiWNKhV0zwVaBqd3x3ixVmHFyFLuJyXB-moOe8pAQAA)):
293+
294+
```svelte
295+
<script>
296+
console.log('in component setup:', $effect.tracking()); // false
297+
298+
$effect(() => {
299+
console.log('in effect:', $effect.tracking()); // true
300+
});
301+
</script>
302+
303+
<p>in template: {$effect.tracking()}</p> <!-- true -->
304+
```
305+
306+
This allows you to (for example) add things like subscriptions without causing memory leaks, by putting them in child effects. Here's a `readable` function that listens to changes from a callback function as long as it's inside a tracking context:
307+
308+
```ts
309+
import { tick } from 'svelte';
310+
311+
export default function readable<T>(
312+
initial_value: T,
313+
start: (callback: (value: T) => void) => () => void
314+
) {
315+
let value = $state(initial_value);
316+
317+
let subscribers = 0;
318+
let stop: null | (() => void) = null;
319+
320+
return {
321+
get value() {
322+
// If in a tracking context ...
323+
if ($effect.tracking()) {
324+
$effect(() => {
325+
// ...and there's no subscribers yet...
326+
if (subscribers === 0) {
327+
// ...invoke the function and listen to changes to update state
328+
stop = start((fn) => (value = fn(value)));
329+
}
330+
331+
subscribers++;
332+
333+
// The return callback is called once a listener unlistens
334+
return () => {
335+
tick().then(() => {
336+
subscribers--;
337+
// If it was the last subscriber...
338+
if (subscribers === 0) {
339+
// ...stop listening to changes
340+
stop?.();
341+
stop = null;
342+
}
343+
});
344+
};
345+
});
346+
}
347+
348+
return value;
349+
}
350+
};
351+
}
352+
```
353+
354+
## `$effect.root`
355+
356+
The `$effect.root` rune is an advanced feature that creates a non-tracked scope that doesn't auto-cleanup. This is useful for
357+
nested effects that you want to manually control. This rune also allows for creation of effects outside of the component initialisation phase.
358+
359+
```svelte
360+
<script>
361+
let count = $state(0);
362+
363+
const cleanup = $effect.root(() => {
364+
$effect(() => {
365+
console.log(count);
366+
});
367+
368+
return () => {
369+
console.log('effect root cleanup');
370+
};
371+
});
372+
</script>
373+
```

0 commit comments

Comments
 (0)