Repository: sveltejs/svelte (2025-07-26T12:05:19Z)

Issue:
Default values for function props prevents reactivity when passed to child components
### Describe the bug

If you have some component that takes a function as a prop and passes that on to child components, when the component provides a default value for that prop, changing the value of the prop will trigger reactivity with that component but will not trigger reactivity within any child components. This does not occur if the prop is a string type, for example, or if a default is not specified. This behaviour is the case regardless of whether the default value is ever used.

This manifested as fairly subtle failure in our usecase and took some time to isolate.

### Reproduction

https://svelte.dev/playground/1bdd3298b7574da5bffe6a21ef8b6086?version=5.36.13

### Logs

```shell

```

### System Info

```shell
System:
    OS: macOS 15.5
    CPU: (8) arm64 Apple M1
    Memory: 138.28 MB / 8.00 GB
    Shell: 5.9 - /bin/zsh
  Binaries:
    Node: 24.0.2 - ~/.local/state/fnm_multishells/91717_1753180887181/bin/node
    npm: 11.4.2 - ~/.local/state/fnm_multishells/91717_1753180887181/bin/npm
  Browsers:
    Chrome: 138.0.7204.158
    Safari: 18.5
  npmPackages:
    svelte: ^5.0.0 => 5.36.13
```

### Severity

annoyance


Discussion:
When I edit the input field, I see all four instances of the text in the REPL update. What am I supposed to be seeing?
> When I edit the input field, I see all four instances of the text in the REPL update. What am I supposed to be seeing?

Apologies, I mistakenly left the repro in the state I used to confirm:
> This does not occur if the prop is a string type

I have restored it to the correct state
edit: and fixed the link to it
You should see this:

https://github.com/user-attachments/assets/34c8d0ff-97f9-4383-a4c5-3e17ca0ba750
@Conduitry did this help you reproduce?
Interestingly this works:
```diff
-	let { textFunction = () => "SensibleDefault" } = $props()
+	const fallback = () => "SensibleDefault"
+	let { textFunction = fallback } = $props()
```
---

```js
let { textFunction = () => "SensibleDefault" } = $props()
```
compiles to
```js
	Inner(node, { textFunction: textFunction() });
```
instead of
```js
	Inner(node, {
		get textFunction() {
			return textFunction();
		}
	});
```



Patch:
diff --git a/.changeset/gorgeous-jeans-begin.md b/.changeset/gorgeous-jeans-begin.md
new file mode 100644
index 000000000000..586c4d8a57a3
--- /dev/null
+++ b/.changeset/gorgeous-jeans-begin.md
@@ -0,0 +1,5 @@
+---
+'svelte': patch
+---
+
+fix: always mark props as stateful
diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/Identifier.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/Identifier.js
index cced326f9baa..4dfdfe5af1a1 100644
--- a/packages/svelte/src/compiler/phases/2-analyze/visitors/Identifier.js
+++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/Identifier.js
@@ -93,7 +93,10 @@ export function Identifier(node, context) {
 			context.state.expression.references.add(binding);
 			context.state.expression.has_state ||=
 				binding.kind !== 'static' &&
-				!binding.is_function() &&
+				(binding.kind === 'prop' ||
+					binding.kind === 'bindable_prop' ||
+					binding.kind === 'rest_prop' ||
+					!binding.is_function()) &&
 				!context.state.scope.evaluate(node).is_known;
 		}
 


Test patch:
diff --git a/packages/svelte/tests/runtime-runes/samples/props-default-value-function/_config.js b/packages/svelte/tests/runtime-runes/samples/props-default-value-function/_config.js
new file mode 100644
index 000000000000..6b281f04f0ee
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/props-default-value-function/_config.js
@@ -0,0 +1,15 @@
+import { flushSync } from 'svelte';
+import { test } from '../../test';
+
+export default test({
+	mode: ['client'],
+
+	test({ assert, target }) {
+		const btn = target.querySelector('button');
+
+		assert.htmlEqual(target.innerHTML, `<button>inc</button> Inner: 0 Inner: 0`);
+		btn?.click();
+		flushSync();
+		assert.htmlEqual(target.innerHTML, `<button>inc</button> Inner: 1 Inner: 1`);
+	}
+});
diff --git a/packages/svelte/tests/runtime-runes/samples/props-default-value-function/inner.svelte b/packages/svelte/tests/runtime-runes/samples/props-default-value-function/inner.svelte
new file mode 100644
index 000000000000..6bde0a15a87c
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/props-default-value-function/inner.svelte
@@ -0,0 +1,4 @@
+<script>
+	let { getter } = $props()
+</script>
+Inner: {getter()}
diff --git a/packages/svelte/tests/runtime-runes/samples/props-default-value-function/main.svelte b/packages/svelte/tests/runtime-runes/samples/props-default-value-function/main.svelte
new file mode 100644
index 000000000000..2cb2f67b82f7
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/props-default-value-function/main.svelte
@@ -0,0 +1,14 @@
+<script>
+	import Wrapper1 from "./wrapper.svelte"
+	import Wrapper2 from "./wrapper2.svelte"
+	let count = $state(0);
+
+	let getter = $derived.by(() => {
+		const copy = count;
+		return () => copy;
+	});
+</script>
+
+<button onclick={() => count++}>inc</button>
+<Wrapper1 {getter}/>
+<Wrapper2 {getter}/>
\ No newline at end of file
diff --git a/packages/svelte/tests/runtime-runes/samples/props-default-value-function/wrapper.svelte b/packages/svelte/tests/runtime-runes/samples/props-default-value-function/wrapper.svelte
new file mode 100644
index 000000000000..525494ddfbf1
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/props-default-value-function/wrapper.svelte
@@ -0,0 +1,7 @@
+<script >
+  import Inner from "./inner.svelte";
+
+  const { getter = () => -1} = $props();
+</script>
+
+<Inner {getter} />
diff --git a/packages/svelte/tests/runtime-runes/samples/props-default-value-function/wrapper2.svelte b/packages/svelte/tests/runtime-runes/samples/props-default-value-function/wrapper2.svelte
new file mode 100644
index 000000000000..9498f432d8ce
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/props-default-value-function/wrapper2.svelte
@@ -0,0 +1,7 @@
+<script >
+  import Inner from "./inner.svelte";
+
+  const { getter = $bindable(() => -1)} = $props();
+</script>
+
+<Inner {getter} />