Skip to content

Commit a5de086

Browse files
feat: turn reactive_declaration_non_reactive_property into a runtime warning (#14192)
* turn `reactive_declaration_non_reactive_property` into a runtime warning * ignore warning * Update packages/svelte/src/internal/client/reactivity/effects.js Co-authored-by: Simon H <[email protected]> * Update packages/svelte/src/internal/client/runtime.js Co-authored-by: Simon H <[email protected]> * fix * test * changeset * Update .changeset/witty-turtles-bake.md Co-authored-by: Simon H <[email protected]> * add some details * check * regenerate --------- Co-authored-by: Simon H <[email protected]>
1 parent 87863da commit a5de086

File tree

20 files changed

+217
-82
lines changed

20 files changed

+217
-82
lines changed

.changeset/witty-turtles-bake.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'svelte': patch
3+
---
4+
5+
chore: turn reactive_declaration_non_reactive_property into a runtime warning

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

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,40 @@ Mutating a value outside the component that created it is strongly discouraged.
8686
%component% mutated a value owned by %owner%. This is strongly discouraged. Consider passing values to child components with `bind:`, or use a callback instead
8787
```
8888

89+
### reactive_declaration_non_reactive_property
90+
91+
```
92+
A `$:` statement (%location%) read reactive state that was not visible to the compiler. Updates to this state will not cause the statement to re-run. The behaviour of this code will change if you migrate it to runes mode
93+
```
94+
95+
In legacy mode, a `$:` [reactive statement](https://svelte.dev/docs/svelte/legacy-reactive-assignments) re-runs when the state it _references_ changes. This is determined at compile time, by analysing the code.
96+
97+
In runes mode, effects and deriveds re-run when there are changes to the values that are read during the function's _execution_.
98+
99+
Often, the result is the same — for example these can be considered equivalent:
100+
101+
```js
102+
$: sum = a + b;
103+
```
104+
105+
```js
106+
const sum = $derived(a + b);
107+
```
108+
109+
In some cases — such as the one that triggered the above warning — they are _not_ the same:
110+
111+
```js
112+
const add = () => a + b;
113+
114+
// the compiler can't 'see' that `sum` depends on `a` and `b`, but
115+
// they _would_ be read while executing the `$derived` version
116+
$: sum = add();
117+
```
118+
119+
Similarly, reactive properties of [deep state](https://svelte.dev/docs/svelte/$state#Deep-state) are not visible to the compiler. As such, changes to these properties will cause effects and deriveds to re-run but will _not_ cause `$:` statements to re-run.
120+
121+
When you [migrate this component](https://svelte.dev/docs/svelte/v5-migration-guide) to runes mode, the behaviour will change accordingly.
122+
89123
### state_proxy_equality_mismatch
90124

91125
```

documentation/docs/98-reference/.generated/compile-warnings.md

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -726,12 +726,6 @@ Reactive declarations only exist at the top level of the instance script
726726
Reassignments of module-level declarations will not cause reactive statements to update
727727
```
728728

729-
### reactive_declaration_non_reactive_property
730-
731-
```
732-
Properties of objects and arrays are not reactive unless in runes mode. Changes to this property will not cause the reactive statement to update
733-
```
734-
735729
### script_context_deprecated
736730

737731
```

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

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,38 @@ The easiest way to log a value as it changes over time is to use the [`$inspect`
5454
5555
> %component% mutated a value owned by %owner%. This is strongly discouraged. Consider passing values to child components with `bind:`, or use a callback instead
5656
57+
## reactive_declaration_non_reactive_property
58+
59+
> A `$:` statement (%location%) read reactive state that was not visible to the compiler. Updates to this state will not cause the statement to re-run. The behaviour of this code will change if you migrate it to runes mode
60+
61+
In legacy mode, a `$:` [reactive statement](https://svelte.dev/docs/svelte/legacy-reactive-assignments) re-runs when the state it _references_ changes. This is determined at compile time, by analysing the code.
62+
63+
In runes mode, effects and deriveds re-run when there are changes to the values that are read during the function's _execution_.
64+
65+
Often, the result is the same — for example these can be considered equivalent:
66+
67+
```js
68+
$: sum = a + b;
69+
```
70+
71+
```js
72+
const sum = $derived(a + b);
73+
```
74+
75+
In some cases — such as the one that triggered the above warning — they are _not_ the same:
76+
77+
```js
78+
const add = () => a + b;
79+
80+
// the compiler can't 'see' that `sum` depends on `a` and `b`, but
81+
// they _would_ be read while executing the `$derived` version
82+
$: sum = add();
83+
```
84+
85+
Similarly, reactive properties of [deep state](https://svelte.dev/docs/svelte/$state#Deep-state) are not visible to the compiler. As such, changes to these properties will cause effects and deriveds to re-run but will _not_ cause `$:` statements to re-run.
86+
87+
When you [migrate this component](https://svelte.dev/docs/svelte/v5-migration-guide) to runes mode, the behaviour will change accordingly.
88+
5789
## state_proxy_equality_mismatch
5890

5991
> Reactive `$state(...)` proxies and the values they proxy have different identities. Because of this, comparisons with `%operator%` will produce unexpected results

packages/svelte/messages/compile-warnings/script.md

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,6 @@
2626

2727
> Reassignments of module-level declarations will not cause reactive statements to update
2828
29-
## reactive_declaration_non_reactive_property
30-
31-
> Properties of objects and arrays are not reactive unless in runes mode. Changes to this property will not cause the reactive statement to update
32-
3329
## state_referenced_locally
3430

3531
> State referenced in its own scope will never update. Did you mean to reference it inside a closure?

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

Lines changed: 0 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -26,30 +26,5 @@ export function MemberExpression(node, context) {
2626
context.state.analysis.needs_context = true;
2727
}
2828

29-
if (context.state.reactive_statement) {
30-
const left = object(node);
31-
32-
if (left !== null) {
33-
const binding = context.state.scope.get(left.name);
34-
35-
if (binding && binding.kind === 'normal') {
36-
const parent = /** @type {Node} */ (context.path.at(-1));
37-
38-
if (
39-
binding.scope === context.state.analysis.module.scope ||
40-
binding.declaration_kind === 'import' ||
41-
(binding.initial &&
42-
binding.initial.type !== 'ArrayExpression' &&
43-
binding.initial.type !== 'ObjectExpression' &&
44-
binding.scope.function_depth <= 1)
45-
) {
46-
if (parent.type !== 'MemberExpression' && parent.type !== 'CallExpression') {
47-
w.reactive_declaration_non_reactive_property(node);
48-
}
49-
}
50-
}
51-
}
52-
}
53-
5429
context.next();
5530
}

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

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1+
/** @import { Location } from 'locate-character' */
12
/** @import { Expression, LabeledStatement, Statement } from 'estree' */
23
/** @import { ReactiveStatement } from '#compiler' */
34
/** @import { ComponentContext } from '../types' */
5+
import { dev, is_ignored, locator } from '../../../../state.js';
46
import * as b from '../../../../utils/builders.js';
57
import { build_getter } from '../utils.js';
68

@@ -48,14 +50,21 @@ export function LabeledStatement(node, context) {
4850
sequence.push(serialized);
4951
}
5052

53+
const location =
54+
dev && !is_ignored(node, 'reactive_declaration_non_reactive_property')
55+
? locator(/** @type {number} */ (node.start))
56+
: undefined;
57+
5158
// these statements will be topologically ordered later
5259
context.state.legacy_reactive_statements.set(
5360
node,
5461
b.stmt(
5562
b.call(
5663
'$.legacy_pre_effect',
5764
sequence.length > 0 ? b.thunk(b.sequence(sequence)) : b.thunk(b.block([])),
58-
b.thunk(b.block(body))
65+
b.thunk(b.block(body)),
66+
location && b.literal(location.line),
67+
location && b.literal(location.column)
5968
)
6069
)
6170
);

packages/svelte/src/compiler/warnings.js

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,6 @@ export const codes = [
102102
"perf_avoid_nested_class",
103103
"reactive_declaration_invalid_placement",
104104
"reactive_declaration_module_script_dependency",
105-
"reactive_declaration_non_reactive_property",
106105
"state_referenced_locally",
107106
"store_rune_conflict",
108107
"css_unused_selector",
@@ -641,14 +640,6 @@ export function reactive_declaration_module_script_dependency(node) {
641640
w(node, "reactive_declaration_module_script_dependency", "Reassignments of module-level declarations will not cause reactive statements to update");
642641
}
643642

644-
/**
645-
* Properties of objects and arrays are not reactive unless in runes mode. Changes to this property will not cause the reactive statement to update
646-
* @param {null | NodeLike} node
647-
*/
648-
export function reactive_declaration_non_reactive_property(node) {
649-
w(node, "reactive_declaration_non_reactive_property", "Properties of objects and arrays are not reactive unless in runes mode. Changes to this property will not cause the reactive statement to update");
650-
}
651-
652643
/**
653644
* State referenced in its own scope will never update. Did you mean to reference it inside a closure?
654645
* @param {null | NodeLike} node

packages/svelte/src/constants.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,8 @@ export const IGNORABLE_RUNTIME_WARNINGS = /** @type {const} */ ([
4444
'hydration_attribute_changed',
4545
'hydration_html_changed',
4646
'ownership_invalid_binding',
47-
'ownership_invalid_mutation'
47+
'ownership_invalid_mutation',
48+
'reactive_declaration_non_reactive_property'
4849
]);
4950

5051
/**
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { DEV } from 'esm-env';
2+
import { FILENAME } from '../../../constants.js';
3+
import { dev_current_component_function } from '../runtime.js';
4+
5+
/**
6+
*
7+
* @param {number} [line]
8+
* @param {number} [column]
9+
*/
10+
export function get_location(line, column) {
11+
if (!DEV || line === undefined) return undefined;
12+
13+
var filename = dev_current_component_function?.[FILENAME];
14+
var location = filename && `${filename}:${line}:${column}`;
15+
16+
return sanitize_location(location);
17+
}
18+
19+
/**
20+
* Prevent devtools trying to make `location` a clickable link by inserting a zero-width space
21+
* @param {string | undefined} location
22+
*/
23+
export function sanitize_location(location) {
24+
return location?.replace(/\//g, '/\u200b');
25+
}

0 commit comments

Comments
 (0)