Repository: sveltejs/svelte (JavaScript) — Bug Fix

Issue:
Slotted content less reactive when referencing external slot props in event handler
### Describe the bug

This is a very specific issue which only seems to happen when passing slot props into content slotted into another component, only using them in an event handler.

E.g.

```svelte
<ItemDisplay content={item}>
  <svelte:fragment slot="default" let:content>
    <!-- ... -->

    <Wrapper>
      <!-- Passed value of "content" will be outdated if original source (item) changes -->
      <button type=button on:click={() => onEdit(content)}>Edit (Wrapped)</button>
    </Wrapper>

    <!-- Passed value of "content" will be up to date if original source (item) changes  -->
    <button type=button on:click={() => onEdit(content)}>Edit</button>
  </svelte:fragment>
</Table>
```

If the the `content` is additionally used in any other way, e.g. JSON stringified in the button content, the problem disappears.

### Reproduction

[REPL](https://svelte.dev/repl/6e71c6198e044697b577f36a29a0c507?version=3.47.0)

The edit buttons are supposed to initialize an edit dialog with the current state of the item, that being whether it is checked.
So if an item is initially unchecked, the edit dialog should also have an unchecked checkbox. Is this changed and the dialog is opened again, the box should now be checked.

Concrete steps:
- Click "Edit (Wrapped)" button
- Check box in dialog, confirm with OK
- Click the same edit button again, observe checked state in dialog
- (Clicking the non-wrapped edit button should show the dialog in the correct state)

### Logs

_No response_

### System Info

```shell
System:
    OS: Windows 10 10.0.19044
    Memory: 13.37 GB / 31.92 GB
  Binaries:
    Node: 16.7.0 - C:\Program Files\nodejs\node.EXE
    Yarn: 1.22.17 - ~\AppData\Roaming\npm\yarn.CMD
    npm: 6.14.13 - C:\Program Files\nodejs\npm.CMD
  Browsers:
    Chrome: 100.0.4896.75
    Edge: Spartan (44.19041.1266.0), Chromium (100.0.1185.36)
    Internet Explorer: 11.0.19041.1566
  npmPackages:
    svelte: ^3.46.4 => 3.47.0
    webpack: ^5.70.0 => 5.70.0
```


### Severity

annoyance


Discussion:
You can workaround this by

1. Subscribing to `content` inside the nested slot including reactive event handlers i.e `on:click={(content, () => {})}`.
https://svelte.dev/repl/d4666b62f77640d1ad3c1a6a54bc647a?version=3.47.0

2. Spreading `$$props` or `$$restProps` in parent slot (these are not recommended for perf reasons)
https://svelte.dev/repl/b41c38532d2c46d9a8d81c732c4e551b?version=3.47.0

Edit: I think svelte should dirty check by default if a slot's let bindings are referenced by inner slots' function scopes, e.g. event handlers. This would lead to more correct code(IINW) and better compiler output than above approaches.
Hi @brunnerh. I'm noticing a couple of things here (and I'm rather new to Svelte, so forgive me if some of my comments are in error).
* Shouldn't your `svelte-fragment` be `svelte:fragment`. Otherwise, as you can see by inspecting your REPL, the `svelte-fragment` custom element is actually rendered to the DOM.
* The `content` slot-prop you created is on `ItemDisplay`, not the fragment, yet you are using the `let:` directive on your fragment instead of the `ItemDisplay` component instance. Instead, I believe this should be added to the `ItemDisplay` allowing you to forego your use of a fragment altogether.
  
  Here is how that would look:
  
  **BEFORE** ([REPL](https://svelte.dev/repl/dd1351787bbd42e0b51945f6e2655f92?version=3.47.0))
  
  ```svelte
  <ItemDisplay content={item}>
      <svelte-fragment slot="default" let:content>
          <input type=checkbox disabled checked={content.isChecked} />
          <Wrapper>
              <button type=button on:click={() => onEdit(content)}>Edit (Wrapped)</button>
          </Wrapper>
          <button type=button on:click={() => onEdit(content)}>Edit</button>
      </svelte-fragment>
  </ItemDisplay>
  ```
  
  **AFTER** ([REPL](https://svelte.dev/repl/5f25609efa92454bbc46bc89d0b28b5e?version=3.47.0))
  
  ```svelte
  <ItemDisplay content={item} let:content>
      <input type=checkbox disabled checked={content.isChecked} />
      <Wrapper>
          <button type=button on:click={() => onEdit(content)}>Edit (Wrapped)</button>
      </Wrapper>
      <button type=button on:click={() => onEdit(content)}>Edit</button>
  </ItemDisplay>
  ```
@brandonmcconnell The `svelte-fragment` was an error. It does not affect the bug, but it might be confusing, so I fixed it accordingly.

I know the slot props can be declared on the component, but slot props may not necessarily be available on every slot and the component already has a property called `content`. Separating it like this makes the scope a bit more explicit, but this is up to personal preference.

Resolving patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,7 @@
 * Fix `{@const}` tag not working inside Component when there's no `let:` [#7189](https://github.com/sveltejs/svelte/issues/7189)
 * Ignore comments in `{#each}` blocks when containing elements with `animate:` ([#3999](https://github.com/sveltejs/svelte/issues/3999))
 * Add a third parameter to the returned function of `createEventDispatcher` that allows passing an object of `{ cancelable: true }` to create a cancelable custom event. The returned function when called will also return a boolean depending on whether the event is cancelled ([#7064](https://github.com/sveltejs/svelte/pull/7064))
+* Fix value of `let:` bindings not updating in certain cases ([#7440](https://github.com/sveltejs/svelte/issues/7440)) 
 
 ## 3.47.0
 
diff --git a/src/compiler/compile/nodes/shared/Expression.ts b/src/compiler/compile/nodes/shared/Expression.ts
--- a/src/compiler/compile/nodes/shared/Expression.ts
+++ b/src/compiler/compile/nodes/shared/Expression.ts
@@ -90,7 +90,7 @@ export default class Expression {
 					}
 
 					if (template_scope.is_let(name)) {
-						if (!function_expression) { // TODO should this be `!lazy` ?
+						if (!lazy) {
 							contextual_dependencies.add(name);
 							dependencies.add(name);
 						}


Test patch:
diff --git a/test/runtime/samples/component-slot-let-in-slot-2/Inner.svelte b/test/runtime/samples/component-slot-let-in-slot-2/Inner.svelte
new file mode 100644
--- /dev/null
+++ b/test/runtime/samples/component-slot-let-in-slot-2/Inner.svelte
@@ -0,0 +1 @@
+<slot/>
diff --git a/test/runtime/samples/component-slot-let-in-slot-2/Outer.svelte b/test/runtime/samples/component-slot-let-in-slot-2/Outer.svelte
new file mode 100644
--- /dev/null
+++ b/test/runtime/samples/component-slot-let-in-slot-2/Outer.svelte
@@ -0,0 +1,5 @@
+<script>
+	export let prop
+</script>
+
+<slot value={prop} />
diff --git a/test/runtime/samples/component-slot-let-in-slot-2/_config.js b/test/runtime/samples/component-slot-let-in-slot-2/_config.js
new file mode 100644
--- /dev/null
+++ b/test/runtime/samples/component-slot-let-in-slot-2/_config.js
@@ -0,0 +1,25 @@
+let logs;
+function log(value) {
+	logs.push(value);
+}
+
+export default {
+	props: {
+		prop: 'a',
+		log
+	},
+	html: '<button></button>',
+	before_test() {
+		logs = [];
+	},
+	async test({ assert, component, target, window }) {
+		const button = target.querySelector('button');
+		await button.dispatchEvent(new window.MouseEvent('click'));
+
+		assert.deepEqual(logs, ['a']);
+
+		component.prop = 'b';
+		await button.dispatchEvent(new window.MouseEvent('click'));
+		assert.deepEqual(logs, ['a', 'b']);
+	}
+};
diff --git a/test/runtime/samples/component-slot-let-in-slot-2/main.svelte b/test/runtime/samples/component-slot-let-in-slot-2/main.svelte
new file mode 100644
--- /dev/null
+++ b/test/runtime/samples/component-slot-let-in-slot-2/main.svelte
@@ -0,0 +1,11 @@
+<script>
+	import Outer from './Outer.svelte'
+	import Inner from './Inner.svelte'
+
+	export let prop
+	export let log;
+</script>
+
+<Outer {prop} let:value>
+	<Inner><button on:click={() => { log(value); }} /></Inner>
+</Outer>