Skip to content

document class state fields #9563

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Nov 20, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions sites/svelte-5-preview/src/routes/docs/content/01-api/02-runes.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,22 @@ Reactive state is declared with the `$state` rune:
</button>
```

You can also use `$state` in class fields (whether public or private):

```js
// @errors: 7006 2554
class Todo {
done = $state(false);
text = $state();

constructor(text) {
this.text = text;
}
}
```

> In this example, the compiler transforms `done` and `text` into `get`/`set` methods on the class prototype referencing private fields

### What this replaces

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.
Expand All @@ -47,6 +63,8 @@ Derived state is declared with the `$derived` rune:

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

As with `$state`, you can mark class fields as `$derived`.

### What this replaces

The non-runes equivalent would be `$: double = count * 2`. There are some important differences to be aware of:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,11 @@ In Svelte 4, reactivity centres on the _component_ and the top-level state decla
function addTodo(event) {
if (event.key !== 'Enter') return;

let done = false;
let text = event.target.value;

todos = [
...todos,
{
done,
text
done: false,
text: event.target.value
}
];

Expand All @@ -43,45 +40,25 @@ In Svelte 4, reactivity centres on the _component_ and the top-level state decla
<p>{remaining(todos)} remaining</p>
```

...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.
...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.

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.

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:
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`:

```diff
<script>
- let todos = [];
+ let todos = $state([]);

function remaining(todos) {
console.log('recalculating');
return todos.filter(todo => !todo.done).length;
}

function addTodo(event) {
if (event.key !== 'Enter') return;

- let done = false;
- let text = event.target.value;
+ let done = $state(false);
+ let text = $state(event.target.value);

todos = [...todos, {
done,
text
}];

event.target.value = '';
}
</script>
```

Next, update `done` and `text` to use `get` and `set` properties:

```diff
<script>
let todos = $state([]);
+ class Todo {
+ done = $state(false);
+ text = $state();
+
+ constructor(text) {
+ this.text = text;
+ }
+ }

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

let done = $state(false);
let text = $state(event.target.value);

todos = [...todos, {
- done,
- text
+ get done() { return done },
+ set done(value) { done = value },
+ get text() { return text },
+ set text(value) { text = value }
}];
- todos = [
- ...todos,
- {
- done: false,
- text: event.target.value
- }
- ];
+ todos = [...todos, new Todo(event.target.value)];

event.target.value = '';
}
</script>
```

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.
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.

## Gotchas

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.
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.

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.

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.
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.