Repository: sveltejs/svelte (JavaScript)

Issue:
Svelte 5: `$state` setter `this` is not the proxy
### Describe the bug

Setting properties via `this` in a `$state` object does not trigger updates since the proxy gets bypassed. `this` is the raw object.

Should the setter function be called with the proxy as `this`?
(That is already the case for getters, but maybe there are some reasons not to do this, e.g. performance.)

---

Relevant code:
https://github.com/sveltejs/svelte/blob/919e7adaa81f8347d7b2d7da427b9b8258647489/packages/svelte/src/internal/client/proxy.js#L246-L248

If the descriptor is checked for a set function, that could be invoked with the receiver. Something along the lines of:
```js
var descriptor = Reflect.getOwnPropertyDescriptor(target, prop);
if (descriptor.set)
  descriptor.set.call(receiver, value);
else
  target[prop] = value;
```

### Reproduction

```svelte
<script>
  let text = $state({
    value: 'svelte',
    get uppercase() {
      return this.value.toUpperCase()
    },
    set uppercase(v) {
      this.value = v.toLowerCase()
    }
  })
</script>

<input bind:value={text.uppercase} />
<p>{text.value}</p>
```
[REPL](https://svelte-5-preview.vercel.app/#H4sIAAAAAAAACl2PwWrDMAyGX0WIQVsIyd04gbHrrjs1PaSJuhlSW9hy2hL87sVJabedhH6-_xOa8WRGCqj2M9ruTKjwnRkLlBvnJUw0CmGBwUXf50SH3huWprUAIwkIXQVqeAvSCW3nHANM3RhJwWatb4o1_SaByEy-7wJtd_CAATxJ9Bbkx4RyqZbivjL4sYArlh6W8Mcy_dK8-lDDVIr7dJd_jjzSrrW6er7RWm0sR4GjsYNa6vWcvyqfVxJUTWs1N2u-MElX3GCBZzeYk6EBlfhI6ZDundTiw1MBAAA=)

### Logs

_No response_

### System Info

```shell
REPL
```


### Severity

annoyance


Discussion:
I benchmarked it and this does indeed add a bit of overhead for writing a new property to an array of 1000 elements with 6x CPU slowdown:

Before:
7.2ms

After:
10.3ms

I don't think it's that bad though, and correctness here is arguably more important.



Patch:
diff --git a/.changeset/shiny-starfishes-cross.md b/.changeset/shiny-starfishes-cross.md
new file mode 100644
index 000000000000..2eead25214fe
--- /dev/null
+++ b/.changeset/shiny-starfishes-cross.md
@@ -0,0 +1,5 @@
+---
+'svelte': patch
+---
+
+fix: ensure $state proxy invokes set accessor if present
diff --git a/packages/svelte/src/internal/client/proxy.js b/packages/svelte/src/internal/client/proxy.js
index 206c97fbc7e2..a0c5ca23e07a 100644
--- a/packages/svelte/src/internal/client/proxy.js
+++ b/packages/svelte/src/internal/client/proxy.js
@@ -243,9 +243,14 @@ const state_proxy_handler = {
 			}
 		}
 
+		var descriptor = Reflect.getOwnPropertyDescriptor(target, prop);
+
 		// Set the new value before updating any signals so that any listeners get the new value
-		// @ts-ignore
-		target[prop] = value;
+		if (descriptor?.set) {
+			descriptor.set.call(receiver, value);
+		} else {
+			target[prop] = value;
+		}
 
 		if (not_has) {
 			// If we have mutated an array directly, we might need to


Test patch:
diff --git a/packages/svelte/tests/runtime-runes/samples/proxy-set-accessor/_config.js b/packages/svelte/tests/runtime-runes/samples/proxy-set-accessor/_config.js
new file mode 100644
index 000000000000..1ea2ee622a5b
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/proxy-set-accessor/_config.js
@@ -0,0 +1,22 @@
+import { flushSync } from 'svelte';
+import { test, ok } from '../../test';
+
+export default test({
+	html: `<input><p>svelte</p>`,
+	ssrHtml: `<input value="SVELTE"><p>svelte</p>`,
+
+	test({ assert, target }) {
+		const input = target.querySelector('input');
+		ok(input);
+
+		const event = new window.Event('input');
+
+		input.value = 'SVELTEy';
+		input.dispatchEvent(event);
+
+		flushSync();
+
+		assert.htmlEqual(target.innerHTML, `<input><p>sveltey</p>`);
+		assert.equal(input.value, 'SVELTEY');
+	}
+});
diff --git a/packages/svelte/tests/runtime-runes/samples/proxy-set-accessor/main.svelte b/packages/svelte/tests/runtime-runes/samples/proxy-set-accessor/main.svelte
new file mode 100644
index 000000000000..13a4acfea8e2
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/proxy-set-accessor/main.svelte
@@ -0,0 +1,14 @@
+<script>
+  let text = $state({
+    value: 'svelte',
+    get uppercase() {
+      return this.value.toUpperCase()
+    },
+    set uppercase(v) {
+      this.value = v.toLowerCase()
+    }
+  })
+</script>
+
+<input bind:value={text.uppercase} />
+<p>{text.value}</p>