sveltejs / svelte

web development for the rest of us
https://svelte.dev
MIT License
80.03k stars 4.26k forks source link

Svelte 5: using `bind:this` in `$derived` #13181

Open hyunbinseo opened 2 months ago

hyunbinseo commented 2 months ago

Describe the bug

<script lang="ts">
  let a: HTMLParagraphElement;
  let b: HTMLParagraphElement | undefined = undefined;
</script>

<p bind:this={a} bind:this={b}></p>
// Variable 'a' is used before being assigned.ts(2454)
const a2 = $derived(a.children);

// Property 'children' does not exist on type 'never'.ts(2339)
const b2 = $derived(b?.children);
// let a2: HTMLCollection
$: a2 = a.children;

// let b2: HTMLCollection | undefined
$: b2 = b?.children;

Reproduction

See above.

Logs

No response

System Info

System:
  OS: macOS 14.6.1
  CPU: (10) arm64 Apple M1 Pro
  Memory: 665.30 MB / 16.00 GB
  Shell: 5.9 - /bin/zsh
Binaries:
  Node: 22.8.0 - ~/.local/state/fnm_multishells/8044_1725946328522/bin/node
  npm: 10.8.2 - ~/.local/state/fnm_multishells/8044_1725946328522/bin/npm
  pnpm: 9.9.0 - ~/.local/state/fnm_multishells/8044_1725946328522/bin/pnpm
Browsers:
  Chrome: 128.0.6613.120
  Edge: 128.0.2739.67
  Safari: 17.6
npmPackages:
  svelte: 5.0.0-next.244 => 5.0.0-next.244

Severity

annoyance

paoloricciuti commented 2 months ago

If you want to use it in a derived it has to be state

<script lang="ts">
    let b: HTMLParagraphElement = $state();
    const b2 = $derived(b?.children);
    $inspect(b2);
</script>

<p bind:this={b}></p>

this works fine (and the error that you are seeing on b is just about the intersection between HTMLParagraphElement and undefined being never. So i would cheat a bit and use only HTMLParagraphElement while still using the nullish to avoid the derived throwing (it also depends on where you are using the derived...if you don't access it immediately you can also avoid the nullish.

hyunbinseo commented 2 months ago

My bad and thank you. Maybe this should be a documentation issue?

// HTMLElement | undefined
let article = $state<HTMLElement>();

// HTMLCollection | undefined
const children = $derived(article?.children);
paoloricciuti commented 2 months ago

My bad and thank you. Maybe this should be a documentation issue?

// HTMLElement | undefined
let article = $state<HTMLElement>();

// HTMLCollection | undefined
const children = $derived(article?.children);

What's exactly the documentation issue?

hyunbinseo commented 2 months ago

Providing an example of bind:this and $state() in the docs will be helpful.

Since the following is valid in Svelte 5 runes, I honestly didn't thought using $state() was viable.

<script lang="ts">
  let div: HTMLDivElement;
</script>

<div bind:this={div}></div>

Going one step further, maybe forcing $state with bind:this in Svelte 6 could be an option?

In the example code, canvasElement is initially undefined, but is typed as HTMLCanvasElement.

<script>
  import { onMount } from 'svelte';

  /** @type {HTMLCanvasElement} */
  let canvasElement;

  onMount(() => {
    const ctx = canvasElement.getContext('2d');
    drawStuff(ctx);
  });
</script>

<canvas bind:this={canvasElement} />

Forcing $state will fix this problem:

<script lang="ts">
  //  HTMLDivElement | undefined
  let div = $state<HTMLDivElement>();
</script>

<div bind:this={div}></div>
paoloricciuti commented 2 months ago

As i've said technically you don't really need $state if the div never changes...you only need it in your case because you are reading the derived immediately. For example

<script lang="ts">
  let a;
    const a2 = $derived(a.children);
</script>

<button onclick={()=>{
    console.log(a2);
}}>
    log child
</button>

<p bind:this={a}></p>

this doesn't cause any problems because you are accessing a2 when a was already set.

However i agree that we need specific documentation around this topic so i'll keep this open with the doc label