Skip to content

feat: make <svelte:component> unnecessary in runes mode #12646

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Jul 30, 2024

Conversation

dummdidumm
Copy link
Member

@dummdidumm dummdidumm commented Jul 29, 2024

In Svelte 4, writing <Component /> meant that the component instance is static. If you made the variable Component a reactive state variable and updated the component value, the component would not be reinstantiated with the new value - you had to use <svelte:component> for that. One reason was that having a dynamic component was more overhead, which is no longer the case in Svelte 5. We can therefore reduce the potential API surface area (by maybe deprecating <svelte:component> in the future) by allowing Svelte to recognize when a component variable is potentially dynamic. It turned out that this was already mostly the case. This PR fixes one case where it wasn't, and fixes another where this was wrongfully applied in legacy mode.

Also fixes #12646

Before submitting the PR, please make sure you do the following

  • It's really useful if your PR references an issue where it is discussed ahead of time. In many cases, features are absent for a reason. For large changes, please create an RFC: https://github.com/sveltejs/rfcs
  • Prefix your PR title with feat:, fix:, chore:, or docs:.
  • This message body should clearly illustrate what problems it solves.
  • Ideally, include a test that fails without this PR but passes with it.

Tests and linting

  • Run the tests with pnpm test and lint the project with pnpm lint

In Svelte 4, writing `<Component />` meant that the component instance is static. If you made the variable `Component` a reactive state variable and updated the component value, the component would not be reinstantiated with the new value - you had to use `<svelte:component>` for that. One reason was that having a dynamic component was more overhead, which is no longer the case in Svelte 5. We can therefore reduce the potential API surface area (by maybe deprecating `<svelte:component>` in the future) by allowing Svelte to recognize when a component variable is potentially dynamic. It turned out that this was already mostly the case. This PR fixes one case where it wasn't, and fixes another where this was wrongfully applied in legacy mode.
Copy link

changeset-bot bot commented Jul 29, 2024

🦋 Changeset detected

Latest commit: ca2e825

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
svelte Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@Rich-Harris
Copy link
Member

Do we want to go so far as to deprecate <svelte:component> in runes mode? Or do we still think there's enough value in being able to do this sort of thing?

<svelte:component this={condition ? A : B}>

@dummdidumm
Copy link
Member Author

I'm honestly not sure. I wouldn't mind deprecating it - examples like yours are probably rare, and writing let Component = $derived(condition ? A : B) means reusing existing APIs which feels better.

@arxpoetica
Copy link
Member

Deprecating is fine. I just searched our code base and found 3 places and all of them will look more elegant with the new approach.

@Rich-Harris Rich-Harris merged commit 8be7dd5 into main Jul 30, 2024
9 checks passed
@Rich-Harris Rich-Harris deleted the dynamic-component branch July 30, 2024 21:55
@memestageceo
Copy link

memestageceo commented Jul 31, 2024

So does this mean I can now:

<script>
  let DynamicComponent = $derived(value > 10 ? A : B);
</script>

<DynamicComponent />

@Conduitry
Copy link
Member

Yes, <Foo /> now also dynamically updates when Foo changes, just like <svelte:component this={Foo} /> does in v4.

@niemyjski
Copy link

Does this have to be wrapped in state? Because when I try this in one of my components it works perfectly if I define let Icon = $state(icon) but if I don't this has two different errors:

<script lang="ts">
    import type { Component } from 'svelte';
    import type { HTMLAnchorAttributes } from 'svelte/elements';

    type Props = {
        icon: Component;
    } & HTMLAnchorAttributes;

    let { icon }: Props = $props();
</script>

<icon />
  • icon' is declared but its value is never read.ts(6133)
  • Self-closing HTML tags for non-void elements are ambiguous — use <icon ...></icon> rather than <icon ... />svelte(element_invalid_self_closing_tag)

If I change it to not be self closing I still have it never being used. might be a tooling issue but calling it out.

@niemyjski
Copy link

I can open a second issue but I'm not sure we are covering the following scenario where we iterate over an array to display a component:

{#each facets as facet (facet.filter.key)}
    {#if isVisible(facet)}
        <svelte:component this={facet.component} filter={facet.filter} {filterChanged} {filterRemoved}  />
    {/if}
{/each}

With this change, what's the best way to handle this?

@nickolasgregory
Copy link

How is this supposed to work in an {#each} loop?

I have somethin similar to the below. REPL

<script>
	import IconA from './IconA.svelte'
	import IconB from './IconB.svelte'
	
	let icons = {
		a: IconA, 
		b: IconB,
	}
	let things = [
		{text: "A", icon: "a"},
		{text: "B", icon: "b"},
	]
	
	let MyIcon = $derived(icons['a'])
</script>

<div><i>$derived</i> component <MyIcon /> - OK</div>

{#each things as thing}
	<div><i>svelte:component</i> <svelte:component this={icons[thing.icon]} /></div>
	<!-- <icons[thing.icon] /> NOPE -->
{/each}

(I seem to have typed this at the same time as @niemyjski)

@paoloricciuti
Copy link
Member

Take a look at my comment in the related issue

#12668 (comment)

You can make it work just by uppercasing thing or facet

@nickolasgregory
Copy link

Thanks @paoloricciuti

In my example things only contains a key to reference the icon component. (things is actually from a json file)
So Thing didn't work in my case (unless I missed something.)

I tried with these changes;
but get the error "Icons[thing.icon] is not a function"

	let Icons = $state({
		a: IconA, 
		b: IconB,
	})
	// etc...
</script>

{#each things as thing}
	<Icons[thing.icon] />
{/each}

@paoloricciuti
Copy link
Member

Thanks @paoloricciuti

In my example things only contains a key to reference the icon component. (things is actually from a json file) So Thing didn't work in my case (unless I missed something.)

I tried with these changes; but get the error "Icons[thing.icon] is not a function"

	let Icons = $state({
		a: IconA, 
		b: IconB,
	})
	// etc...
</script>

{#each things as thing}
	<Icons[thing.icon] />
{/each}

Oh missed that...then you can do this

{#each things as thing}
	{@const Icon = icons[thing.icon]}
	<div><i>svelte:component</i> <Icon /></div>
	<!-- <icons[thing.icon] /> NOPE -->
{/each}

@nickolasgregory
Copy link

Thanks again @paoloricciuti

I was too invested in "svelte 5 shiny" to get back to the basics!
All good now :)

@MrBns
Copy link

MrBns commented Aug 17, 2024

Thanks @paoloricciuti

	let Icons = $state({
		a: IconA, 
		b: IconB,
	})
	// etc...
</script>

{#each things as thing}
	<Icons[thing.icon] />
{/each}

Iocns is a state here .. How is this can be callable as a Component ?

@MrBns
Copy link

MrBns commented Aug 17, 2024

Thanks @paoloricciuti
Oh missed that...then you can do this

{#each things as thing}
	{@const Icon = icons[thing.icon]}
	<div><i>svelte:component</i> <Icon /></div>
	<!-- <icons[thing.icon] /> NOPE -->
{/each}

Wow, I don't know why the hell Svelte is becoming more verbose.
in svelte 4 .. it was just <svelte:component/>
But now ?
declare a const inside each loop and Call it as Component. thats really crazy.

@paoloricciuti
Copy link
Member

Thanks @paoloricciuti
Oh missed that...then you can do this

{#each things as thing}
	{@const Icon = icons[thing.icon]}
	<div><i>svelte:component</i> <Icon /></div>
	<!-- <icons[thing.icon] /> NOPE -->
{/each}

Wow, I don't know why the hell Svelte is becoming more verbose. in svelte 4 .. it was just <svelte:component/> But now ? declare a const inside each loop and Call it as Component. thats really crazy.

This is the single edge case we're svelte:component would be less verbose. For all the rest it becomes less verbose and if you want you can still use it with the ignore. Sometimes you need to loose some to gain some.

@sheecegardezi
Copy link

Just an example:

import { CircleArrowDown, CircleArrowUp, Spline } from 'lucide-svelte';
const icon = CircleArrowUp

following code:

<svelte:component this={icon}  />

gets replace with:

<icon/>


<!-- these are equivalent -->
<Thing />
<svelte:component this={Thing} />
Copy link

@connectkushal connectkushal Nov 3, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I got confused by this initially and suggest that this should be a comment as to highlight that it is no longer needed. Adds more clarity.

@coryvirok
Copy link
Contributor

How do we handle typing with components that are from packages that have not been updated to Svelte 5 yet?

E.g. The following works but TypeScript complains about Sun and Moon not being Components

<script lang="ts">
  import Moon from 'lucide-svelte/icons/moon'
  import Sun from 'lucide-svelte/icons/sun'

  import type { Component } from 'svelte'
</script>

{#snippet foo(Icon: Component)}
  Here's a <Icon />
{/snippet}

<ul>
  <li>{@render foo(Sun)}</li>
  <li>{@render foo(Moon)}</li>
</ul>
Argument of type 'typeof Sun' is not assignable to parameter of type 'Component<{}, {}, string>'.
  Type 'typeof Sun' provides no match for the signature '(this: void, internals: Brand<"ComponentInternals">, props: {}): { $on?(type: string, callback: (e: any) => void): () => void; $set?(props: Partial<{}>): void; }'.ts(2345)

@dummdidumm
Copy link
Member Author

Also accept the old SvelteComponent type for the snippet param

@coryvirok
Copy link
Contributor

Unfortunately that still doesn't work.

TypeScript errors on both the usage of <Icon /> and on the parameters passed to the snippet.

<script lang="ts">
  import Moon from 'lucide-svelte/icons/moon'
  import Sun from 'lucide-svelte/icons/sun'

  import type { Component, SvelteComponent } from 'svelte'
</script>

{#snippet foo(Icon: Component | SvelteComponent)}
  Here's a <Icon />
{/snippet}

<ul>
  <li>{@render foo(Sun)}</li>
  <li>{@render foo(Moon)}</li>
</ul>
Argument of type 'Component<{}, {}, string> | SvelteComponent<Record<string, any>, any, any>' is not assignable to parameter of type 'ConstructorOfATypedSvelteComponent | Component<any, any, any> | null | undefined'.
  Type 'SvelteComponent<Record<string, any>, any, any>' is not assignable to type 'ConstructorOfATypedSvelteComponent | Component<any, any, any> | null | undefined'.
    Type 'SvelteComponent<Record<string, any>, any, any>' is not assignable to type 'Component<any, any, any>'.
      Type 'SvelteComponent<Record<string, any>, any, any>' provides no match for the signature '(this: void, internals: Brand<"ComponentInternals">, props: any): any'.

Possible causes:
- You use the instance type of a component where you should use the constructor type
- Type definitions are missing for this Svelte Component. ts(2345)

@dummdidumm
Copy link
Member Author

Sorry, I meant typeof SvelteComponent<any>

@coryvirok
Copy link
Contributor

Sorry, I meant typeof SvelteComponent<any>

That works. Thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.