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

* feat: make `<svelte:component>` unnecessary in runes mode

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.

* we already have this function

* add interactive demos

* changeset

---------

Co-authored-by: Rich Harris <rich.harris@vercel.com>
pull/12667/head
Simon H 1 year ago committed by GitHub
parent 00e8ebde1d
commit 8be7dd558b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,5 @@
---
"svelte": patch
---
feat: make `<svelte:component>` unnecessary in runes mode

@ -1551,7 +1551,10 @@ const common_visitors = {
node.name.includes('.') ? node.name.slice(0, node.name.indexOf('.')) : node.name
);
node.metadata.dynamic = binding !== null && binding.kind !== 'normal';
node.metadata.dynamic =
context.state.analysis.runes && // Svelte 4 required you to use svelte:component to switch components
binding !== null &&
(binding.kind !== 'normal' || node.name.includes('.'));
},
RenderTag(node, context) {
context.next({ ...context.state, render_tag: node });

@ -933,7 +933,16 @@ function serialize_inline_component(node, component_name, context, anchor = cont
/** @param {Expression} node_id */
let fn = (node_id) => {
return b.call(component_name, node_id, props_expression);
return b.call(
// TODO We can remove this ternary once we remove legacy mode, since in runes mode dynamic components
// will be handled separately through the `$.component` function, and then the component name will
// always be referenced through just the identifier here.
node.type === 'SvelteComponent'
? component_name
: /** @type {Expression} */ (context.visit(b.member_id(component_name))),
node_id,
props_expression
);
};
if (bind_this !== null) {

@ -0,0 +1,14 @@
import { test } from '../../test';
import { flushSync } from 'svelte';
export default test({
html: '<button>switch</button> Component1 Component1',
async test({ assert, target }) {
const btn = target.querySelector('button');
btn?.click();
flushSync();
assert.htmlEqual(target.innerHTML, '<button>switch</button> Component2 Component2');
}
});

@ -0,0 +1,16 @@
<script>
import Component1 from './Component1.svelte';
import Component2 from './Component2.svelte';
let Component = $state(Component1);
let Object = {
get component() {
return Component;
}
};
</script>
<button onclick={() => (Component = Component2)}>switch</button>
<Component />
<Object.component />

@ -1,3 +0,0 @@
import { test } from '../../test';
export default test({ skip: true });

@ -1,30 +0,0 @@
<script>
import A from './A.svelte';
import B from './B.svelte';
let Let = A;
function update() {
Let = B;
}
export let ExportLet = B;
$: Reactive = random() ? A : B;
</script>
<Let />
<ExportLet />
<Reactive />
<svelte:component this={Let} />
<!-- Here, we test to see if svelte-ignore works with reactive-component -->
<!-- svelte-ignore reactive_component -->
<Let />
<div>
<!-- svelte-ignore reactive_component -->
<ExportLet />
<div>
<!-- svelte-ignore reactive_component -->
<Reactive />
</div>
</div>

@ -1,38 +0,0 @@
[
{
"code": "reactive_component",
"message": "<Let/> will not be reactive if Let changes. Use <svelte:component this={Let}/> if you want this reactivity.",
"end": {
"column": 7,
"line": 15
},
"start": {
"column": 0,
"line": 15
}
},
{
"message": "<ExportLet/> will not be reactive if ExportLet changes. Use <svelte:component this={ExportLet}/> if you want this reactivity.",
"code": "reactive_component",
"end": {
"column": 13,
"line": 16
},
"start": {
"column": 0,
"line": 16
}
},
{
"message": "<Reactive/> will not be reactive if Reactive changes. Use <svelte:component this={Reactive}/> if you want this reactivity.",
"code": "reactive_component",
"end": {
"column": 12,
"line": 17
},
"start": {
"column": 0,
"line": 17
}
}
]

@ -198,6 +198,30 @@ In Svelte 4, doing the following triggered reactivity:
This is because the Svelte compiler treated the assignment to `foo.value` as an instruction to update anything that referenced `foo`. In Svelte 5, reactivity is determined at runtime rather than compile time, so you should define `value` as a reactive `$state` field on the `Foo` class. Wrapping `new Foo()` with `$state(...)` will have no effect — only vanilla objects and arrays are made deeply reactive.
### `<svelte:component>` is no longer necessary
In Svelte 4, components are _static_ — if you render `<Thing>`, and the value of `Thing` changes, [nothing happens](https://svelte.dev/repl/7f1fa24f0ab44c1089dcbb03568f8dfa?version=4.2.18). To make it dynamic you must use `<svelte:component>`.
This is [no longer true in Svelte 5](/#H4sIAAAAAAAAE4WQwU7DMAyGX8VESANpXe8lq9Q8AzfGobQujZQmWeJOQlXenaQB1sM0bnG-379_e2GDVOhZ9bYw3U7IKtZYy_aMvmwq_AUVYay9mV2XfrjvnLRUn_SJ5GSNI2hgcGaC3aFsDrlh97LB4g-LLY4ChQSvo9SfcIRHTy3h03NEvLzO0Nyjwo7gQ-q-urRqxuOy9oQ1AjeWpNHwQ5pQN7zMf7e4CLXY8Dhpdc-THooCaESP0DoEPM8ydqEmKIqkzUnL9MxrVJ2JG-qkoFH631xREg82mV4OEntWkZsx7K_3vXtdm_LbuwbiHwNx2-A9fANfmchv7QEAAA==):
```svelte
<script>
import A from './A.svelte';
import B from './B.svelte';
let Thing = $state();
</script>
<select bind:value={Thing}>
<option value={A}>A</option>
<option value={B}>B</option>
</select>
<!-- these are equivalent -->
<Thing />
<svelte:component this={Thing} />
```
## Other breaking changes
### Stricter `@const` assignment validation

Loading…
Cancel
Save