Skip to content

Commit ee04f97

Browse files
authored
docs: enhance $effect documentation (#10680)
- explain when not to use `$effect`, closes #10193 - explain that only synchronous reads are tracked, closes #10475 - explain nuance around reruns and object reads, closes #10392
1 parent c4473df commit ee04f97

File tree

1 file changed

+180
-14
lines changed
  • sites/svelte-5-preview/src/routes/docs/content/01-api

1 file changed

+180
-14
lines changed

sites/svelte-5-preview/src/routes/docs/content/01-api/02-runes.md

Lines changed: 180 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -161,24 +161,101 @@ In essence, `$derived(expression)` is equivalent to `$derived.by(() => expressio
161161

162162
To run side-effects like logging or analytics whenever some specific values change, or when a component is mounted to the DOM, we can use the `$effect` rune:
163163

164-
```diff
164+
```svelte
165+
<script>
166+
let count = $state(0);
167+
let doubled = $derived(count * 2);
168+
169+
$effect(() => {
170+
console.log({ count, doubled });
171+
});
172+
</script>
173+
174+
<button on:click={() => count++}>
175+
{doubled}
176+
</button>
177+
178+
<p>{count} doubled is {doubled}</p>
179+
```
180+
181+
`$effect` will automatically subscribe to any `$state` or `$derived` values it reads _synchronously_ and reruns whenever their values change — that means, values after an `await` or inside a `setTimeout` will _not_ be tracked. `$effect` will run after the DOM has been updated.
182+
183+
```svelte
184+
<script>
185+
let count = $state(0);
186+
let doubled = $derived(count * 2);
187+
188+
$effect(() => {
189+
// runs after the DOM has been updated
190+
// when the component is mounted
191+
// and whenever `count` changes,
192+
// but not when `doubled` changes,
193+
console.log(count);
194+
195+
setTimeout(() => console.log(doubled));
196+
});
197+
</script>
198+
199+
<button on:click={() => count++}>
200+
{doubled}
201+
</button>
202+
203+
<p>{count} doubled is {doubled}</p>
204+
```
205+
206+
An effect only reruns when the object it reads changes, not when a property inside it changes. If you want to react to _any_ change inside an object for inspection purposes at dev time, you may want to use [`inspect`](#inspect).
207+
208+
```svelte
209+
<script>
210+
let object = $state({ count: 0 });
211+
let derived_object = $derived({
212+
doubled: object.count * 2
213+
});
214+
215+
$effect(() => {
216+
// never reruns, because object does not change,
217+
// only its property changes
218+
object;
219+
console.log('object');
220+
});
221+
222+
$effect(() => {
223+
// reruns, because object.count changes
224+
object.count;
225+
console.log('object.count');
226+
});
227+
228+
$effect(() => {
229+
// reruns, because $derived produces a new object on each rerun
230+
derived_object;
231+
console.log('derived_object');
232+
});
233+
</script>
234+
235+
<button on:click={() => object.count++}>
236+
{doubled}
237+
</button>
238+
239+
<p>{count} doubled is {doubled}</p>
240+
```
241+
242+
You can return a function from `$effect`, which will run immediately before the effect re-runs, and before it is destroyed.
243+
244+
```svelte
165245
<script>
166246
let count = $state(0);
167247
let doubled = $derived(count * 2);
168248
169-
+ $effect(() => {
170-
+ // runs when the component is mounted, and again
171-
+ // whenever `count` or `doubled` change,
172-
+ // after the DOM has been updated
173-
+ console.log({ count, doubled });
174-
+
175-
+ return () => {
176-
+ // if a callback is provided, it will run
177-
+ // a) immediately before the effect re-runs
178-
+ // b) when the component is destroyed
179-
+ console.log('cleanup');
180-
+ };
181-
+ });
249+
$effect(() => {
250+
console.log({ count, doubled });
251+
252+
return () => {
253+
// if a callback is provided, it will run
254+
// a) immediately before the effect re-runs
255+
// b) when the component is destroyed
256+
console.log('cleanup');
257+
};
258+
});
182259
</script>
183260
184261
<button on:click={() => count++}>
@@ -188,6 +265,93 @@ To run side-effects like logging or analytics whenever some specific values chan
188265
<p>{count} doubled is {doubled}</p>
189266
```
190267

268+
> `$effect` was designed for managing side effects such as logging or connecting to external systems like third party libraries that have an imperative API. If you're managing state or dataflow, you should use it with caution – most of the time, you're better off using a different pattern. Below are some use cases and what to use instead.
269+
270+
If you update `$state` inside an `$effect`, you most likely want to use `$derived` instead.
271+
272+
```svelte
273+
<script>
274+
let count = $state(0);
275+
// Don't do this:
276+
let doubled = $state();
277+
$effect(() => {
278+
doubled = count * 2;
279+
});
280+
// Do this instead:
281+
let doubled = $derived(count * 2);
282+
</script>
283+
```
284+
285+
This also applies to more complex calculations that require more than a simple expression and write to more than one variable. In these cases, you can use `$derived.by`.
286+
287+
```svelte
288+
<script>
289+
// Don't do this:
290+
let result_1 = $state();
291+
let result_2 = $state();
292+
$effect(() => {
293+
// ... some lengthy code resulting in
294+
result_1 = someValue;
295+
result_2 = someOtherValue;
296+
});
297+
// Do this instead:
298+
let { result_1, result_2 } = $derived.by(() => {
299+
// ... some lengthy code resulting in
300+
return {
301+
result_1: someValue,
302+
result_2: someOtherValue
303+
};
304+
});
305+
</script>
306+
```
307+
308+
When reacting to a state change and writing to a different state as a result, think about if it's possible to model the code through event handling instead.
309+
310+
```svelte
311+
<!-- Don't do this -->
312+
<script>
313+
let value = $state();
314+
let value_uppercase = $state();
315+
$effect(() => {
316+
value_uppercase = value.toUpperCase();
317+
});
318+
</script>
319+
320+
<Text bind:value />
321+
322+
<!-- Do this instead: -->
323+
<script>
324+
let value = $state();
325+
let value_uppercase = $state();
326+
function onValueChange(new_text) {
327+
value = new_text;
328+
value_uppercase = new_text.toUpperCase();
329+
}
330+
</script>
331+
332+
<Text {value} {onValueChange}>
333+
```
334+
335+
If you want to have something update from above but also modify it from below (i.e. you want some kind of "writable `$derived`"), and events aren't an option, you can also use an object with getters and setters.
336+
337+
```svelte
338+
<script>
339+
let { value } = $props();
340+
let proxy = {
341+
get value() {
342+
return value.toUpperCase();
343+
},
344+
set value(val) {
345+
value = val.toLowerCase();
346+
}
347+
};
348+
</script>
349+
350+
<input bind:value={proxy.value} />
351+
```
352+
353+
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).
354+
191355
### What this replaces
192356

193357
The portions of `$: {}` that are triggering side-effects can be replaced with `$effect` while being careful to migrate updates of reactive variables to use `$derived`. There are some important differences:
@@ -236,6 +400,8 @@ In rare cases, you may need to run code _before_ the DOM updates. For this we ca
236400
</div>
237401
```
238402

403+
Apart from the timing, `$effect.pre` works exactly like [`$effect`](#effect) — refer to its documentation for more info.
404+
239405
### What this replaces
240406

241407
Previously, you would have used `beforeUpdate`, which — like `afterUpdate` — is deprecated in Svelte 5.

0 commit comments

Comments
 (0)