Skip to content

Commit 95c6b65

Browse files
authored
docs: document class state fields (#9563)
Co-authored-by: Rich Harris <[email protected]>
1 parent 225a5bf commit 95c6b65

File tree

2 files changed

+41
-49
lines changed

2 files changed

+41
-49
lines changed

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

+18
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,22 @@ Reactive state is declared with the `$state` rune:
2424
</button>
2525
```
2626

27+
You can also use `$state` in class fields (whether public or private):
28+
29+
```js
30+
// @errors: 7006 2554
31+
class Todo {
32+
done = $state(false);
33+
text = $state();
34+
35+
constructor(text) {
36+
this.text = text;
37+
}
38+
}
39+
```
40+
41+
> In this example, the compiler transforms `done` and `text` into `get`/`set` methods on the class prototype referencing private fields
42+
2743
### What this replaces
2844

2945
In non-runes mode, a `let` declaration is treated as reactive state if it is updated at some point. Unlike `$state(...)`, which works anywhere in your app, `let` only behaves this way at the top level of a component.
@@ -47,6 +63,8 @@ Derived state is declared with the `$derived` rune:
4763

4864
The expression inside `$derived(...)` should be free of side-effects. Svelte will disallow state changes (e.g. `count++`) inside derived expressions.
4965

66+
As with `$state`, you can mark class fields as `$derived`.
67+
5068
### What this replaces
5169

5270
The non-runes equivalent would be `$: double = count * 2`. There are some important differences to be aware of:

sites/svelte-5-preview/src/routes/docs/content/02-examples/02-fine-grained-reactivity.md

+23-49
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,11 @@ In Svelte 4, reactivity centres on the _component_ and the top-level state decla
1616
function addTodo(event) {
1717
if (event.key !== 'Enter') return;
1818
19-
let done = false;
20-
let text = event.target.value;
21-
2219
todos = [
2320
...todos,
2421
{
25-
done,
26-
text
22+
done: false,
23+
text: event.target.value
2724
}
2825
];
2926
@@ -43,45 +40,25 @@ In Svelte 4, reactivity centres on the _component_ and the top-level state decla
4340
<p>{remaining(todos)} remaining</p>
4441
```
4542

46-
...editing any individual `todo` will invalidate the entire list. You can see this for yourself by [opening the playground](/#H4sIAAAAAAAAE2VSu27DMAz8FVUdnACBvDu2gQ79g25xBlWiE6EKZch0msLwv1cPJwHajaSOdzyKM--NhZFXh5mjvACv-Nsw8B2nnyEm4xUsQchHN3kVK_WovBmo7bAjC8TIaTeyhh2O-1AKxX5CRcYh83CRBg2eNgmzZXN87kg5HJ0FYd1pU3hQ0qrJSgrAYrvPEA80eczcIkxI4BMJa1r2EgOhHcJWWMATnVPT8kddav0RgBu4AtJD2_QsV8QX_LCXpmHFOwb2Ysuy5moie4siwVov7Qj7Z5ngRqGceUj6E5C4SjvBo_mxFCFEinf3ATqKpLt7EqlyvBwf3f-JA1VR3G3W5fMLsDY4TMQcVsGQdt_YzKvzhZUJMb-CVOf1n-SYgrgsqrW5tllxZfk0qKsk2Mxpy3G8leeJiqfRdFydQX19ulvHc1_KQa-d0eW9sy6z0lzGSdJH1UM7_72P5XkxdTm04eguTpvegOYV-QmW4_IL1Ksxyq8CAAA=), adding some todos, and watching the console in the bottom right. `remaining(todos)` is recalculated every time we edit the `text` of a todo, even though it can't possibly affect the result.
43+
...editing any individual `todo` will invalidate the entire list. You can see this for yourself by [opening the playground](/#H4sIAAAAAAAAE2VSy27jMAz8FVV7cAIE8t21DfSwf7C3OgdVohOhCmXIdLaF4H9fPewE6N7I0ZAzpBj4aCzMvHkPHOUNeMPfpomfOH1PKZnvYAliPrvFq4S0s_Jmon7AgSwQI6fdzDr2fn6NUATHBRUZh8zDTRo0eDlkzpGF9DyQcjg7C8K6y6HyoKRVi5UUidXxtVA80OKx9BbRIYHPTVjXs5cUCO0QjsICXuiai9Yf6lLrP5F4gDsgPbTNyAoiPuGbvXQdq35j7F4dWdHchhjoMVdJBxJCZOy0A2EPBkpuGjZKO8PpiRJ8UcOKHEl_ARJ3aRfYGWsJzg_N_6nRQFXt87X1c_fYGpwWYg6bOIl2f7EL28grqzMj_AKprtsHyTkHWbLV5t4Xxa3Lh0HdZMEu5PUm61ufJyvdRDdwdQX1-eG-Bl7qcg56q0yr2CvbuiiFOjnJP9ROffh5GOvzVNp66uO13Zw2owHNG_ILrOf1H3DaaQeoAgAA), adding some todos, and watching the console in the bottom right. `remaining(todos)` is recalculated every time we edit the `text` of a todo, even though it can't possibly affect the result.
4744

4845
Worse, everything inside the `each` block needs to be checked for updates. When a list gets large enough, this behaviour has the potential to cause performance headaches.
4946

50-
With runes, it's easy to make reactivity _fine-grained_, meaning that things will only update when they need to. Begin by using the `$state` rune:
47+
With runes, it's easy to make reactivity _fine-grained_, meaning that things will only update when they need to. Mark `todos` as `$state`, create a `Todo` class with `done` and `text` state fields, then instantiate the class inside `addTodo`:
5148

5249
```diff
5350
<script>
5451
- let todos = [];
5552
+ let todos = $state([]);
5653

57-
function remaining(todos) {
58-
console.log('recalculating');
59-
return todos.filter(todo => !todo.done).length;
60-
}
61-
62-
function addTodo(event) {
63-
if (event.key !== 'Enter') return;
64-
65-
- let done = false;
66-
- let text = event.target.value;
67-
+ let done = $state(false);
68-
+ let text = $state(event.target.value);
69-
70-
todos = [...todos, {
71-
done,
72-
text
73-
}];
74-
75-
event.target.value = '';
76-
}
77-
</script>
78-
```
79-
80-
Next, update `done` and `text` to use `get` and `set` properties:
81-
82-
```diff
83-
<script>
84-
let todos = $state([]);
54+
+ class Todo {
55+
+ done = $state(false);
56+
+ text = $state();
57+
+
58+
+ constructor(text) {
59+
+ this.text = text;
60+
+ }
61+
+ }
8562

8663
function remaining(todos) {
8764
console.log('recalculating');
@@ -91,29 +68,26 @@ Next, update `done` and `text` to use `get` and `set` properties:
9168
function addTodo(event) {
9269
if (event.key !== 'Enter') return;
9370

94-
let done = $state(false);
95-
let text = $state(event.target.value);
96-
97-
todos = [...todos, {
98-
- done,
99-
- text
100-
+ get done() { return done },
101-
+ set done(value) { done = value },
102-
+ get text() { return text },
103-
+ set text(value) { text = value }
104-
}];
71+
- todos = [
72+
- ...todos,
73+
- {
74+
- done: false,
75+
- text: event.target.value
76+
- }
77+
- ];
78+
+ todos = [...todos, new Todo(event.target.value)];
10579

10680
event.target.value = '';
10781
}
10882
</script>
10983
```
11084

111-
In [this version of the app](/#H4sIAAAAAAAAE2VTTY-kIBD9Kwy7iZp08O6oyR72H-yt7QMDpU2GBiNl70yM_30QsJ2PG0W9eu9VUSy0Vxocrc4LNfwGtKJ_xpGeKL6PW-DuoBF87Ow8ie2mdmJSI7ad6VADErTSOtKQ3w45Qn6-FM8-5ZP9bAQqa8gEN66MMkMesAVZtnSHwhpnNTBthzybQHAtZs3RA7PA4SET4DyZqMG8U4QpkJCmJU_bgUlroGAazIDXULR-U-dS_vPAHO5g8KGtehJv2Cu8k6emIdlf49mzgkTN1ETscRM5Wuy5drA7DCOANzzSkRb5NACyO9czFA-yfVhnxlg4n3ZDHQ5JKPcmk4kovJ52iNshkdbjkrMQfwIOydVnruDyK1eAPLhSG4kr4tbLw_vPvjw4y_ah1-WxGKZWZpyRWFP58Ur73zRLeoeVlAGx_AIurml7uAuHoFlLdW-jYmJ5UUZWQbBZwptvRhPPgdoWtumouIJ4fbFvHY11IQaZKrdx7ZV1GZWWcnMS1qYe2-X7tq7H_tbl2PqvcLNS9QokrXCaYb2sH9bw9GZFAwAA), editing the `text` of a todo won't cause unrelated things to be updated.
85+
In [this version of the app](/#H4sIAAAAAAAAE21SwW6DMAz9lSybBEhTuDNA2mF_sFvpIUtMGzVNUGK6TYh_XxKgSNtOsWO_Z_vZE-2VBk-rw0QNvwKt6Osw0GeK30N0_A00QvC9HZ2IP7UXTg3YdqZDDUjQSutJQ548coT8cCxeQigEhebek_cQJlP0O5TWwJ7Zc-0hJYcQwhfuoY0ikFjj0Y0CrctjTrFxBchZebbi4rMyzfGZF3w_GoHKGuLgypVR5pSndu8skd5qYNqe8syB4FqMmmNIzLbOHODozDImC2IhuERCmpY8RIPFsQqmwZzw_PJfdS5llCGHG5h9AtWT5Ydd4Js8NA3J3kxgzwqy1LyLsEl8YIwl-5kY-CQ7J0PuToDsxvUIxfEO_BsMLFm2NVmX-y5NrcwwIrGmCu1I-2maae17JmXKmB6Bi_O6cO6TkdSupbq1S8WV5UMZWaWCzZQ0igtaefaseGNNR8UZxOXDfnV0wSUf5IqM6m7IulwqTWXsJMlcD-30e7vzvu-6HNpwvVcrVa9A0iocE8zH-QeS_FSn-AIAAA==), editing the `text` of a todo won't cause unrelated things to be updated.
11286

11387
## Gotchas
11488

115-
If we only do the first step (adding `$state`) and skip the second (exposing the state via `get` and `set` properties), [the app breaks](/#H4sIAAAAAAAAE2VSy27jMAz8FVVdwDEQyHfXNtDD_sHe4hxUiU6EKJIh0WkLw_--etgx0N5IcTjDoTjTQWnwtD7N1PA70Jq-jyM9UvweY-IfoBFC7u3kRHxpvHBqxK43PWpAglZaT1ryxyNHOJzO5VsoheIwGYHKGuLgzpVR5nJI2JLMsdyjsMZbDUzby6FwILgWk-YYgEXiCBAHODmTNViYFMElEtJ25CUGTFoDJdNgLnhNTcsPdS7lvwA8wAMMPrXVQPILu8E3eWlbUvw1gb0oSdZcTWSPUWS3OHDtYZswrQC-cC9nWuTuAsgeXE9QPsm2ZZ0YYyk-bgP1GEWOWxIpc7ycn92_mQNVUWy2m2r_GtMoM05IrKmDQWk_TTuvm1hIlRDzK3BxXf-P-xTE5WEj1aPLiivLhzKyToLtnLYex1t5dlQ8mban4gri9mG_epr7Ug5y7Ywut86mykpzFSdJH9eM3fzzXpb9gppq7MIx3q1UgwJJa3QTLOflP0Toax7HAgAA) — toggling the checkboxes won't cause `remaining(todos)` to be recalculated.
89+
If we only do the first step (adding `$state`) and skip the second (creating a class with state fields), [the app breaks](/#H4sIAAAAAAAAE2VSyU7DMBD9FWOQ0kqVcw9JJA78AbemB2NPWgvXjuxJAVn5d7wkVILbLG_mvVkCHZUGT5tjoIZfgTb0ZZrogeL3lBx_A40QfW9nJ1Kk9cKpCfvBDKgBCVppPenIk0eOsDue9s8xFZPjbAQqa4iDK1dGmfMuY_ckpPSAwhpvNTBtz7vKgeBazJpjBFa5R4Q4wNmZwsGiUgSXm5CuJw_JYNIa2DMN5oyXXLT8YedSvkXgDm5g8JdbjaRE2Ad8k4euI9Wrid2rPSmc6xADbvMdizsgYyzHDlsgbMaASU1DRq49HO5RhC9sSKFD7s6A7Mb1DBtiKcbpl_M_NAqoqm2-tr7fwLTKTDMSa5o4ibSfpgvryAupMyI8AheX9VDcZyNTtlLd-sK4dnlXRjaZsAt5vUn62ueOSr_RDVRcQHy826-Blrrsg1wr0yq2yrYuTKFOSvKF2qkPfx9jub9KW099_LqrlWpUIGmDbobltPwAmGXpQrACAAA=) — toggling the checkboxes won't cause `remaining(todos)` to be recalculated.
11690

11791
That's because in runes mode, Svelte no longer invalidates everything when you change something inside an `each` block. Previously, Svelte tried to statically determine the dependencies of the mutated value in order to invalidate them, causing confusing bugs related to overfiring (invalidating things that weren't actually affected) and underfiring (missing affected variables). It made apps slower by default and harder to reason about, especially in more complex scenarios.
11892

119-
In runes mode, the rules around triggering updates are simpler: Only state declared by a `$state` or `$props` rune causes a rerender. In the [broken example](/#H4sIAAAAAAAAE2VSy27jMAz8FVVdwDEQyHfXNtDD_sHe4hxUiU6EKJIh0WkLw_--etgx0N5IcTjDoTjTQWnwtD7N1PA70Jq-jyM9UvweY-IfoBFC7u3kRHxpvHBqxK43PWpAglZaT1ryxyNHOJzO5VsoheIwGYHKGuLgzpVR5nJI2JLMsdyjsMZbDUzby6FwILgWk-YYgEXiCBAHODmTNViYFMElEtJ25CUGTFoDJdNgLnhNTcsPdS7lvwA8wAMMPrXVQPILu8E3eWlbUvw1gb0oSdZcTWSPUWS3OHDtYZswrQC-cC9nWuTuAsgeXE9QPsm2ZZ0YYyk-bgP1GEWOWxIpc7ycn92_mQNVUWy2m2r_GtMoM05IrKmDQWk_TTuvm1hIlRDzK3BxXf-P-xTE5WEj1aPLiivLhzKyToLtnLYex1t5dlQ8mban4gri9mG_epr7Ug5y7Ywut86mykpzFSdJH9eM3fzzXpb9gppq7MIx3q1UgwJJa3QTLOflP0Toax7HAgAA), `todo` is declared by the `#each` block, and neither the `text` nor the `done` property are referencing values of `$state` runes. One solution would be to turn `text` and `done` into `$state` references, as shown above. [The other solution](/#H4sIAAAAAAAACmVS226jMBD9lam7EokUmXcKSH3YP9i3EK1ce0isOjayh7QV4t9rG2iq9m0uZ86Z28R6bTCw6jgxK67IKvY8DOzA6GNITrihIYx-cKOXKVIH6fVAbWc7MkhATrkADfwJJAh3x9P-KaZish-tJO0seLwKbbU97zJ2D1NKdySdDc4gN-68KzxKYeRoBEVgkTkixCON3i4aPHZK6DMJNC08JIMrZ3HPDdozXXLR_ENdKPUvAnd4Q0tf2rqHJcJf8QMemgaKvzayF3tYNNchOtrmO3LOs33YODpK4hX0wgQ8bDHCd6pg4Sbhz0j8JsyIS34-fRH_hkSVotiGqMv7om2t7TASOFvFdpV7s820zjVDmRHTIwp5Wa8hAvw_gE6roFrpW7soriwv2qoqCzZTxh_1iae2V647Mj1B0zF5Qfn64t47ttRmH9W36rSIrbouF8WpTB3lc9RDO_38gvn-F3U5tPHFrk7pXqNiFfkR59P8CWtDxuCdAgAA) would be to bind to `todos[i].text` instead of `todo.text` — this way, Svelte picks up the reference to the `todos` `$state` and invalidates it as a whole. Keep in mind that you lose the fine-grained reactivity this way — the whole array is invalidated on every keystroke.
93+
In runes mode, the rules around triggering updates are simpler: Only state declared with `$state` or `$derived` or `$props` causes a rerender. In the [broken example](/#H4sIAAAAAAAAE2VSyU7DMBD9FWOQ0kqVcw9JJA78AbemB2NPWgvXjuxJAVn5d7wkVILbLG_mvVkCHZUGT5tjoIZfgTb0ZZrogeL3lBx_A40QfW9nJ1Kk9cKpCfvBDKgBCVppPenIk0eOsDue9s8xFZPjbAQqa4iDK1dGmfMuY_ckpPSAwhpvNTBtz7vKgeBazJpjBFa5R4Q4wNmZwsGiUgSXm5CuJw_JYNIa2DMN5oyXXLT8YedSvkXgDm5g8JdbjaRE2Ad8k4euI9Wrid2rPSmc6xADbvMdizsgYyzHDlsgbMaASU1DRq49HO5RhC9sSKFD7s6A7Mb1DBtiKcbpl_M_NAqoqm2-tr7fwLTKTDMSa5o4ibSfpgvryAupMyI8AheX9VDcZyNTtlLd-sK4dnlXRjaZsAt5vUn62ueOSr_RDVRcQHy826-Blrrsg1wr0yq2yrYuTKFOSvKF2qkPfx9jub9KW099_LqrlWpUIGmDbobltPwAmGXpQrACAAA=), `todo` is declared by the `#each` block, and neither the `text` nor the `done` property are referencing state. One solution would be to turn `text` and `done` into `$state` fields, as shown above. [The other solution](/#H4sIAAAAAAAACmVS226jMBD9lam7EokUmXcKSH3YP9i3EK1ce0isOjayh7QV4t9rG2iq9m0uZ86Z28R6bTCw6jgxK67IKvY8DOzA6GNITrihIYx-cKOXKVIH6fVAbWc7MkhATrkADfwJJAh3x9P-KaZish-tJO0seLwKbbU97zJ2D1NKdySdDc4gN-68KzxKYeRoBEVgkTkixCON3i4aPHZK6DMJNC08JIMrZ3HPDdozXXLR_ENdKPUvAnd4Q0tf2rqHJcJf8QMemgaKvzayF3tYNNchOtrmO3LOs33YODpK4hX0wgQ8bDHCd6pg4Sbhz0j8JsyIS34-fRH_hkSVotiGqMv7om2t7TASOFvFdpV7s820zjVDmRHTIwp5Wa8hAvw_gE6roFrpW7soriwv2qoqCzZTxh_1iae2V647Mj1B0zF5Qfn64t47ttRmH9W36rSIrbouF8WpTB3lc9RDO_38gvn-F3U5tPHFrk7pXqNiFfkR59P8CWtDxuCdAgAA) would be to bind to `todos[i].text` instead of `todo.text` — this way, Svelte picks up the reference to the `todos` `$state` and invalidates it as a whole. Keep in mind that you lose the fine-grained reactivity this way — the whole array is invalidated on every keystroke.

0 commit comments

Comments
 (0)