Skip to content

Commit 0fd4d2a

Browse files
committed
Merge branch 'main' into state-onchange
2 parents 36bfef9 + 441108b commit 0fd4d2a

File tree

33 files changed

+244
-218
lines changed

33 files changed

+244
-218
lines changed

documentation/docs/02-runes/03-$derived.md

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,48 @@ Anything read synchronously inside the `$derived` expression (or `$derived.by` f
5252

5353
To exempt a piece of state from being treated as a dependency, use [`untrack`](svelte#untrack).
5454

55+
## Overriding derived values
56+
57+
Derived expressions are recalculated when their dependencies change, but you can temporarily override their values by reassigning them (unless they are declared with `const`). This can be useful for things like _optimistic UI_, where a value is derived from the 'source of truth' (such as data from your server) but you'd like to show immediate feedback to the user:
58+
59+
```svelte
60+
<script>
61+
let { post, like } = $props();
62+
63+
let likes = $derived(post.likes);
64+
65+
async function onclick() {
66+
// increment the `likes` count immediately...
67+
likes += 1;
68+
69+
// and tell the server, which will eventually update `post`
70+
try {
71+
await like();
72+
} catch {
73+
// failed! roll back the change
74+
likes -= 1;
75+
}
76+
}
77+
</script>
78+
79+
<button {onclick}>🧡 {likes}</button>
80+
```
81+
82+
> [!NOTE] Prior to Svelte 5.25, deriveds were read-only.
83+
84+
## Deriveds and reactivity
85+
86+
Unlike `$state`, which converts objects and arrays to [deeply reactive proxies]($state#Deep-state), `$derived` values are left as-is. For example, [in a case like this](/playground/untitled#H4sIAAAAAAAAE4VU22rjMBD9lUHd3aaQi9PdstS1A3t5XvpQ2Ic4D7I1iUUV2UjjNMX431eS7TRdSosxgjMzZ45mjt0yzffIYibvy0ojFJWqDKCQVBk2ZVup0LJ43TJ6rn2aBxw-FP2o67k9oCKP5dziW3hRaUJNjoYltjCyplWmM1JIIAn3FlL4ZIkTTtYez6jtj4w8WwyXv9GiIXiQxLVs9pfTMR7EuoSLIuLFbX7Z4930bZo_nBrD1bs834tlfvsBz9_SyX6PZXu9XaL4gOWn4sXjeyzftv4ZWfyxubpzxzg6LfD4MrooxELEosKCUPigQCMPKCZh0OtQE1iSxcsmdHuBvCiHZXALLXiN08EL3RRkaJ_kDVGle0HcSD5TPEeVtj67O4Nrg9aiSNtBY5oODJkrL5QsHtN2cgXp6nSJMWzpWWGasdlsGEMbzi5jPr5KFr0Ep7pdeM2-TCelCddIhDxAobi1jqF3cMaC1RKp64bAW9iFAmXGIHfd4wNXDabtOLN53w8W53VvJoZLh7xk4Rr3CoL-UNoLhWHrT1JQGcM17u96oES5K-kc2XOzkzqGCKL5De79OUTyyrg1zgwXsrEx3ESfx4Bz0M5UjVMHB24mw9SuXtXFoN13fYKOM1tyUT3FbvbWmSWCZX2Er-41u5xPoml45svRahl9Wb9aasbINJixDZwcPTbyTLZSUsAvrg_cPuCR7s782_WU8343Y72Qtlb8OYatwuOQvuN13M_hJKNfxann1v1U_B1KZ_D_mzhzhz24fw85CSz2irtN9w9HshBK7AQAAA==)...
87+
88+
```svelte
89+
let items = $state([...]);
90+
91+
let index = $state(0);
92+
let selected = $derived(items[index]);
93+
```
94+
95+
...you can change (or `bind:` to) properties of `selected` and it will affect the underlying `items` array. If `items` was _not_ deeply reactive, mutating `selected` would have no effect.
96+
5597
## Update propagation
5698

5799
Svelte uses something called _push-pull reactivity_ — when state is updated, everything that depends on the state (whether directly or indirectly) is immediately notified of the change (the 'push'), but derived values are not re-evaluated until they are actually read (the 'pull').

documentation/docs/02-runes/04-$effect.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,8 @@ Teardown functions also run when the effect is destroyed, which happens when its
7474

7575
`$effect` automatically picks up any reactive values (`$state`, `$derived`, `$props`) that are _synchronously_ read inside its function body (including indirectly, via function calls) and registers them as dependencies. When those dependencies change, the `$effect` schedules a re-run.
7676

77+
If `$state` and `$derived` are used directly inside the `$effect` (for example, during creation of a [reactive class](https://svelte.dev/docs/svelte/$state#Classes)), those values will _not_ be treated as dependencies.
78+
7779
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](/playground/untitled#H4sIAAAAAAAAE31T246bMBD9lZF3pWSlBEirfaEQqdo_2PatVIpjBrDkGGQPJGnEv1e2IZfVal-wfHzmzJyZ4cIqqdCy9M-F0blDlnqArZjmB3f72XWRHVCRw_bc4me4aDWhJstSlllhZEfbQhekkMDKfwg5PFvihMvX5OXH_CJa1Zrb0-Kpqr5jkiwC48rieuDWQbqgZ6wqFLRcvkC-hYvnkWi1dWqa8ESQTxFRjfQWsOXiWzmr0sSLhEJu3p1YsoJkNUcdZUnN9dagrBu6FVRQHAM10sJRKgUG16bXcGxQ44AGdt7SDkTDdY02iqLHnJVU6hedlWuIp94JW6Tf8oBt_8GdTxlF0b4n0C35ZLBzXb3mmYn3ae6cOW74zj0YVzDNYXRHFt9mprNgHfZSl6mzml8CMoLvTV6wTZIUDEJv5us2iwMtiJRyAKG4tXnhl8O0yhbML0Wm-B7VNlSSSd31BG7z8oIZZ6dgIffAVY_5xdU9Qrz1Bnx8fCfwtZ7v8Qc9j3nB8PqgmMWlHIID6-bkVaPZwDySfWtKNGtquxQ23Qlsq2QJT0KIqb8dL0up6xQ2eIBkAg_c1FI_YqW0neLnFCqFpwmreedJYT7XX8FVOBfwWRhXstZrSXiwKQjUhOZeMIleb5JZfHWn2Yq5pWEpmR7Hv-N_wEqT8hEEAAA=)):
7880

7981
```ts
@@ -252,6 +254,8 @@ In general, `$effect` is best considered something of an escape hatch — useful
252254

253255
> [!NOTE] For things that are more complicated than a simple expression like `count * 2`, you can also use `$derived.by`.
254256
257+
If you're using an effect because you want to be able to reassign the derived value (to build an optimistic UI, for example) note that [deriveds can be directly overridden]($derived#Overriding-derived-values) as of Svelte 5.25.
258+
255259
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](/playground/untitled#H4sIAAAAAAAACpVRy26DMBD8FcvKgUhtoIdeHBwp31F6MGSJkBbHwksEQvx77aWQqooq9bgzOzP7mGTdIHipPiZJowOpGJAv0po2VmfnDv4OSBErjYdneHWzBJaCjcx91TWOToUtCIEE3cig0OIty44r5l1oDtjOkyFIsv3GINQ_CNYyGegd1DVUlCR7oU9iilDUcP8S8roYs9n8p2wdYNVFm4csTx872BxNCcjr5I11fdgonEkXsjP2CoUUZWMv6m6wBz2x7yxaM-iJvWeRsvSbSVeUy5i0uf8vKA78NIeJLSZWv1I8jQjLdyK4XuTSeIdmVKJGGI4LdjVOiezwDu1yG74My8PLCQaSiroe5s_5C2PHrkVGAgAA)):
256260

257261
```svelte

documentation/docs/98-reference/.generated/client-errors.md

Lines changed: 22 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -128,26 +128,33 @@ Cannot set prototype of `$state` object
128128
Updating state inside a derived or a template expression is forbidden. If the value should not be reactive, declare it without `$state`
129129
```
130130

131-
This error is thrown in a situation like this:
131+
This error occurs when state is updated while evaluating a `$derived`. You might encounter it while trying to 'derive' two pieces of state in one go:
132132

133133
```svelte
134134
<script>
135-
let count = $state(0);
136-
let multiple = $derived.by(() => {
137-
const result = count * 2;
138-
if (result > 10) {
139-
count = 0;
140-
}
141-
return result;
142-
});
135+
let count = $state(0);
136+
137+
let even = $state(true);
138+
139+
let odd = $derived.by(() => {
140+
even = count % 2 === 0;
141+
return !even;
142+
});
143143
</script>
144144
145-
<button onclick={() => count++}>{count} / {multiple}</button>
145+
<button onclick={() => count++}>{count}</button>
146+
147+
<p>{count} is even: {even}</p>
148+
<p>{count} is odd: {odd}</p>
146149
```
147150

148-
Here, the `$derived` updates `count`, which is `$state` and therefore forbidden to do. It is forbidden because the reactive graph could become unstable as a result, leading to subtle bugs, like values being stale or effects firing in the wrong order. To prevent this, Svelte errors when detecting an update to a `$state` variable.
151+
This is forbidden because it introduces instability: if `<p>{count} is even: {even}</p>` is updated before `odd` is recalculated, `even` will be stale. In most cases the solution is to make everything derived:
152+
153+
```js
154+
let count = 0;
155+
// ---cut---
156+
let even = $derived(count % 2 === 0);
157+
let odd = $derived(!even);
158+
```
149159

150-
To fix this:
151-
- See if it's possible to refactor your `$derived` such that the update becomes unnecessary
152-
- Think about why you need to update `$state` inside a `$derived` in the first place. Maybe it's because you're using `bind:`, which leads you down a bad code path, and separating input and output path (by splitting it up to an attribute and an event, or by using [Function bindings](bind#Function-bindings)) makes it possible avoid the update
153-
- If it's unavoidable, you may need to use an [`$effect`]($effect) instead. This could include splitting parts of the `$derived` into an [`$effect`]($effect) which does the updates
160+
If side-effects are unavoidable, use [`$effect`]($effect) instead.

packages/svelte/CHANGELOG.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,19 @@
11
# svelte
22

3+
## 5.25.0
4+
5+
### Minor Changes
6+
7+
- feat: make deriveds writable ([#15570](https://github.com/sveltejs/svelte/pull/15570))
8+
9+
## 5.24.1
10+
11+
### Patch Changes
12+
13+
- fix: use `get` in constructor for deriveds ([#15300](https://github.com/sveltejs/svelte/pull/15300))
14+
15+
- fix: ensure toStore root effect is connected to correct parent effect ([#15574](https://github.com/sveltejs/svelte/pull/15574))
16+
317
## 5.24.0
418

519
### Minor Changes

packages/svelte/messages/client-errors/errors.md

Lines changed: 20 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -84,26 +84,31 @@ See the [migration guide](/docs/svelte/v5-migration-guide#Components-are-no-long
8484

8585
> Updating state inside a derived or a template expression is forbidden. If the value should not be reactive, declare it without `$state`
8686
87-
This error is thrown in a situation like this:
87+
This error occurs when state is updated while evaluating a `$derived`. You might encounter it while trying to 'derive' two pieces of state in one go:
8888

8989
```svelte
9090
<script>
91-
let count = $state(0);
92-
let multiple = $derived.by(() => {
93-
const result = count * 2;
94-
if (result > 10) {
95-
count = 0;
96-
}
97-
return result;
98-
});
91+
let count = $state(0);
92+
93+
let even = $state(true);
94+
95+
let odd = $derived.by(() => {
96+
even = count % 2 === 0;
97+
return !even;
98+
});
9999
</script>
100100
101-
<button onclick={() => count++}>{count} / {multiple}</button>
101+
<button onclick={() => count++}>{count}</button>
102+
103+
<p>{count} is even: {even}</p>
104+
<p>{count} is odd: {odd}</p>
102105
```
103106

104-
Here, the `$derived` updates `count`, which is `$state` and therefore forbidden to do. It is forbidden because the reactive graph could become unstable as a result, leading to subtle bugs, like values being stale or effects firing in the wrong order. To prevent this, Svelte errors when detecting an update to a `$state` variable.
107+
This is forbidden because it introduces instability: if `<p>{count} is even: {even}</p>` is updated before `odd` is recalculated, `even` will be stale. In most cases the solution is to make everything derived:
108+
109+
```js
110+
let even = $derived(count % 2 === 0);
111+
let odd = $derived(!even);
112+
```
105113

106-
To fix this:
107-
- See if it's possible to refactor your `$derived` such that the update becomes unnecessary
108-
- Think about why you need to update `$state` inside a `$derived` in the first place. Maybe it's because you're using `bind:`, which leads you down a bad code path, and separating input and output path (by splitting it up to an attribute and an event, or by using [Function bindings](bind#Function-bindings)) makes it possible avoid the update
109-
- If it's unavoidable, you may need to use an [`$effect`]($effect) instead. This could include splitting parts of the `$derived` into an [`$effect`]($effect) which does the updates
114+
If side-effects are unavoidable, use [`$effect`]($effect) instead.

packages/svelte/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"name": "svelte",
33
"description": "Cybernetically enhanced web apps",
44
"license": "MIT",
5-
"version": "5.24.0",
5+
"version": "5.25.0",
66
"type": "module",
77
"types": "./types/index.d.ts",
88
"engines": {

packages/svelte/src/compiler/migrate/index.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1673,9 +1673,9 @@ function extract_type_and_comment(declarator, state, path) {
16731673
const match = /@type {(.+)}/.exec(comment_node.value);
16741674
if (match) {
16751675
// try to find JSDoc comments after a hyphen `-`
1676-
const jsdocComment = /@type {.+} (?:\w+|\[.*?\]) - (.+)/.exec(comment_node.value);
1677-
if (jsdocComment) {
1678-
cleaned_comment += jsdocComment[1]?.trim();
1676+
const jsdoc_comment = /@type {.+} (?:\w+|\[.*?\]) - (.+)/.exec(comment_node.value);
1677+
if (jsdoc_comment) {
1678+
cleaned_comment += jsdoc_comment[1]?.trim();
16791679
}
16801680
return {
16811681
type: match[1],

packages/svelte/src/compiler/phases/2-analyze/visitors/shared/utils.js

Lines changed: 1 addition & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,6 @@ export function validate_assignment(node, argument, state) {
2121
const binding = state.scope.get(argument.name);
2222

2323
if (state.analysis.runes) {
24-
if (binding?.kind === 'derived') {
25-
e.constant_assignment(node, 'derived state');
26-
}
27-
2824
if (binding?.node === state.analysis.props_id) {
2925
e.constant_assignment(node, '$props.id()');
3026
}
@@ -38,25 +34,6 @@ export function validate_assignment(node, argument, state) {
3834
e.snippet_parameter_assignment(node);
3935
}
4036
}
41-
if (
42-
argument.type === 'MemberExpression' &&
43-
argument.object.type === 'ThisExpression' &&
44-
(((argument.property.type === 'PrivateIdentifier' || argument.property.type === 'Identifier') &&
45-
state.derived_state.some(
46-
(derived) =>
47-
derived.name === /** @type {PrivateIdentifier | Identifier} */ (argument.property).name &&
48-
derived.private === (argument.property.type === 'PrivateIdentifier')
49-
)) ||
50-
(argument.property.type === 'Literal' &&
51-
argument.property.value &&
52-
typeof argument.property.value === 'string' &&
53-
state.derived_state.some(
54-
(derived) =>
55-
derived.name === /** @type {Literal} */ (argument.property).value && !derived.private
56-
)))
57-
) {
58-
e.constant_assignment(node, 'derived state');
59-
}
6037
}
6138

6239
/**
@@ -81,7 +58,6 @@ export function validate_no_const_assignment(node, argument, scope, is_binding)
8158
} else if (argument.type === 'Identifier') {
8259
const binding = scope.get(argument.name);
8360
if (
84-
binding?.kind === 'derived' ||
8561
binding?.declaration_kind === 'import' ||
8662
(binding?.declaration_kind === 'const' && binding.kind !== 'each')
8763
) {
@@ -96,12 +72,7 @@ export function validate_no_const_assignment(node, argument, scope, is_binding)
9672
// );
9773

9874
// TODO have a more specific error message for assignments to things like `{:then foo}`
99-
const thing =
100-
binding.declaration_kind === 'import'
101-
? 'import'
102-
: binding.kind === 'derived'
103-
? 'derived state'
104-
: 'constant';
75+
const thing = binding.declaration_kind === 'import' ? 'import' : 'constant';
10576

10677
if (is_binding) {
10778
e.constant_binding(node, thing);

packages/svelte/src/compiler/phases/3-transform/client/visitors/MemberExpression.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@ export function MemberExpression(node, context) {
1111
if (node.property.type === 'PrivateIdentifier') {
1212
const field = context.state.private_state.get(node.property.name);
1313
if (field) {
14-
return context.state.in_constructor ? b.member(node, 'v') : b.call('$.get', node);
14+
return context.state.in_constructor && (field.kind === 'raw_state' || field.kind === 'state')
15+
? b.member(node, 'v')
16+
: b.call('$.get', node);
1517
}
1618
}
1719

packages/svelte/src/store/index-client.js

Lines changed: 28 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,12 @@ import {
66
} from '../internal/client/reactivity/effects.js';
77
import { get, writable } from './shared/index.js';
88
import { createSubscriber } from '../reactivity/create-subscriber.js';
9+
import {
10+
active_effect,
11+
active_reaction,
12+
set_active_effect,
13+
set_active_reaction
14+
} from '../internal/client/runtime.js';
915

1016
export { derived, get, readable, readonly, writable } from './shared/index.js';
1117

@@ -39,19 +45,34 @@ export { derived, get, readable, readonly, writable } from './shared/index.js';
3945
* @returns {Writable<V> | Readable<V>}
4046
*/
4147
export function toStore(get, set) {
42-
let init_value = get();
48+
var effect = active_effect;
49+
var reaction = active_reaction;
50+
var init_value = get();
51+
4352
const store = writable(init_value, (set) => {
4453
// If the value has changed before we call subscribe, then
4554
// we need to treat the value as already having run
46-
let ran = init_value !== get();
55+
var ran = init_value !== get();
4756

4857
// TODO do we need a different implementation on the server?
49-
const teardown = effect_root(() => {
50-
render_effect(() => {
51-
const value = get();
52-
if (ran) set(value);
58+
var teardown;
59+
// Apply the reaction and effect at the time of toStore being called
60+
var previous_reaction = active_reaction;
61+
var previous_effect = active_effect;
62+
set_active_reaction(reaction);
63+
set_active_effect(effect);
64+
65+
try {
66+
teardown = effect_root(() => {
67+
render_effect(() => {
68+
const value = get();
69+
if (ran) set(value);
70+
});
5371
});
54-
});
72+
} finally {
73+
set_active_reaction(previous_reaction);
74+
set_active_effect(previous_effect);
75+
}
5576

5677
ran = true;
5778

packages/svelte/src/version.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,5 @@
44
* The current version, as set in package.json.
55
* @type {string}
66
*/
7-
export const VERSION = '5.24.0';
7+
export const VERSION = '5.25.0';
88
export const PUBLIC_VERSION = '5';

packages/svelte/tests/compiler-errors/samples/runes-no-derived-assignment/_config.js

Lines changed: 0 additions & 8 deletions
This file was deleted.

packages/svelte/tests/compiler-errors/samples/runes-no-derived-assignment/main.svelte

Lines changed: 0 additions & 5 deletions
This file was deleted.

packages/svelte/tests/compiler-errors/samples/runes-no-derived-binding/_config.js

Lines changed: 0 additions & 8 deletions
This file was deleted.

packages/svelte/tests/compiler-errors/samples/runes-no-derived-binding/main.svelte

Lines changed: 0 additions & 6 deletions
This file was deleted.

packages/svelte/tests/compiler-errors/samples/runes-no-derived-state-field-assignment/_config.js

Lines changed: 0 additions & 8 deletions
This file was deleted.

packages/svelte/tests/compiler-errors/samples/runes-no-derived-state-field-assignment/main.svelte

Lines changed: 0 additions & 10 deletions
This file was deleted.

packages/svelte/tests/compiler-errors/samples/runes-no-derived-state-field-update/_config.js

Lines changed: 0 additions & 8 deletions
This file was deleted.

0 commit comments

Comments
 (0)