Skip to content

Commit e846ebb

Browse files
committed
feat: allow $derived($state()) for deep reactive deriveds
1 parent 0cdc431 commit e846ebb

File tree

10 files changed

+160
-2
lines changed

10 files changed

+160
-2
lines changed

.changeset/cyan-planes-yawn.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'svelte': minor
3+
---
4+
5+
feat: allow `$derived($state())` for deep reactive deriveds

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

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,15 @@ export function CallExpression(node, context) {
121121
is_class_property_definition(parent) ||
122122
is_class_property_assignment_at_constructor_root(parent, context);
123123

124-
if (!valid) {
124+
if (
125+
!valid &&
126+
(rune !== '$state' ||
127+
!(
128+
parent.type === 'CallExpression' &&
129+
parent.callee.type === 'Identifier' &&
130+
parent.callee.name === '$derived'
131+
))
132+
) {
125133
e.state_invalid_placement(node, rune);
126134
}
127135

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

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,15 @@ import * as b from '#compiler/builders';
55
import { get_rune } from '../../../scope.js';
66
import { should_proxy } from '../utils.js';
77
import { get_inspect_args } from '../../utils.js';
8+
import { get_parent } from '../../../../utils/ast.js';
89

910
/**
1011
* @param {CallExpression} node
1112
* @param {Context} context
1213
*/
1314
export function CallExpression(node, context) {
1415
const rune = get_rune(node, context.state.scope);
16+
const parent = get_parent(context.path, -1);
1517

1618
switch (rune) {
1719
case '$host':
@@ -40,7 +42,25 @@ export function CallExpression(node, context) {
4042
}
4143

4244
const callee = b.id('$.state', node.callee.loc);
43-
return b.call(callee, value);
45+
let retval = b.call(callee, value);
46+
47+
// this is not the path you would expect from `$derived($state(...))`, but
48+
// it looks like this because we visit the arguments of the derived in the
49+
// VariableDeclaration visitor
50+
if (
51+
(parent.type === 'VariableDeclaration' &&
52+
parent.declarations.length === 1 &&
53+
parent.declarations[0].init?.type === 'CallExpression' &&
54+
parent.declarations[0].init.callee.type === 'Identifier' &&
55+
parent.declarations[0].init?.callee.name === '$derived') ||
56+
(parent.type === 'CallExpression' &&
57+
parent.callee.type === 'Identifier' &&
58+
parent.callee.name === '$derived')
59+
) {
60+
retval = b.call('$.get', retval);
61+
}
62+
63+
return retval;
4464
}
4565

4666
case '$derived':
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
export class Test {
2+
local_arr;
3+
constructor(arr) {
4+
this.local_arr = $derived($state(arr()));
5+
}
6+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<script>
2+
import { Test } from "./Class.svelte.js";
3+
let { arr } = $props();
4+
5+
let test = new Test(() => arr);
6+
</script>
7+
8+
<button onclick={()=>{
9+
test.local_arr.push(test.local_arr.length + 1);
10+
}}>push</button>
11+
{#each test.local_arr as item}
12+
<p>{item}</p>
13+
{/each}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { flushSync } from 'svelte';
2+
import { test } from '../../test';
3+
4+
export default test({
5+
async test({ target, assert }) {
6+
const [update_parent, push] = target.querySelectorAll('button');
7+
let p_tags = target.querySelectorAll('p');
8+
9+
assert.equal(p_tags.length, 3);
10+
assert.equal(p_tags[0].textContent, '1');
11+
assert.equal(p_tags[1].textContent, '2');
12+
assert.equal(p_tags[2].textContent, '3');
13+
14+
flushSync(() => {
15+
push.click();
16+
});
17+
18+
p_tags = target.querySelectorAll('p');
19+
20+
assert.equal(p_tags.length, 4);
21+
assert.equal(p_tags[0].textContent, '1');
22+
assert.equal(p_tags[1].textContent, '2');
23+
assert.equal(p_tags[2].textContent, '3');
24+
assert.equal(p_tags[3].textContent, '4');
25+
26+
flushSync(() => {
27+
update_parent.click();
28+
});
29+
30+
p_tags = target.querySelectorAll('p');
31+
32+
assert.equal(p_tags.length, 3);
33+
assert.equal(p_tags[0].textContent, '4');
34+
assert.equal(p_tags[1].textContent, '5');
35+
assert.equal(p_tags[2].textContent, '6');
36+
}
37+
});
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<script>
2+
import Component from './Component.svelte';
3+
let arr = $state.raw([1, 2, 3]);
4+
</script>
5+
6+
<button onclick={()=>{
7+
arr = [4,5,6];
8+
}}>update</button>
9+
10+
<Component {arr}/>
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<script>
2+
let { arr } = $props();
3+
4+
let local_arr = $derived($state(arr));
5+
</script>
6+
7+
<button onclick={()=>{
8+
local_arr.push(local_arr.length + 1);
9+
}}>push</button>
10+
{#each local_arr as item}
11+
<p>{item}</p>
12+
{/each}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { flushSync } from 'svelte';
2+
import { test } from '../../test';
3+
4+
export default test({
5+
async test({ target, assert }) {
6+
const [update_parent, push] = target.querySelectorAll('button');
7+
let p_tags = target.querySelectorAll('p');
8+
9+
assert.equal(p_tags.length, 3);
10+
assert.equal(p_tags[0].textContent, '1');
11+
assert.equal(p_tags[1].textContent, '2');
12+
assert.equal(p_tags[2].textContent, '3');
13+
14+
flushSync(() => {
15+
push.click();
16+
});
17+
18+
p_tags = target.querySelectorAll('p');
19+
20+
assert.equal(p_tags.length, 4);
21+
assert.equal(p_tags[0].textContent, '1');
22+
assert.equal(p_tags[1].textContent, '2');
23+
assert.equal(p_tags[2].textContent, '3');
24+
assert.equal(p_tags[3].textContent, '4');
25+
26+
flushSync(() => {
27+
update_parent.click();
28+
});
29+
30+
p_tags = target.querySelectorAll('p');
31+
32+
assert.equal(p_tags.length, 3);
33+
assert.equal(p_tags[0].textContent, '4');
34+
assert.equal(p_tags[1].textContent, '5');
35+
assert.equal(p_tags[2].textContent, '6');
36+
}
37+
});
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<script>
2+
import Component from './Component.svelte';
3+
let arr = $state.raw([1, 2, 3]);
4+
</script>
5+
6+
<button onclick={()=>{
7+
arr = [4,5,6];
8+
}}>update</button>
9+
10+
<Component {arr}/>

0 commit comments

Comments
 (0)