A full list of changes can be found at https://svelte.dev/docs/svelte/v5-migration-guide. This is a list of the most common things I had to deal with.
<script>
// OLD
export let var1 = true;
</script>
<script>
// NEW
let { var1 = true } = $props();
</script>
<script>
// OLD
let var1 = { fu: 'bar' };
</script>
<script>
// NEW
let var1 = $state({ fu: 'bar' });
</script>
When you don't need a deeply reactive Object/Array, or if you're doing a ===
comparison, use $state.raw
.
<script>
// OLD
let var1 = true;
</script>
<script>
// NEW
let var1 = $state.raw(true);
</script>
<script>
// OLD
let var1 = 1;
let var2 = var1 + 4;
</script>
<script>
// NEW
let var1 = $state.raw(1);
let var2 = $derived(var1 + 4);
</script>
-
$derived
for setting one variable value.<script> // OLD $: if ($enabled) { var2 = true; } </script>
<script> // NEW let var2 = $derived($enabled); </script>
<script> // OLD $: if ($enabled) { // a lot of complicated logic that eventually sets `var2` } </script>
<script> // NEW let var2 = $derived.by(() => { return // a lot of complicated logic that eventually sets `var2` }); </script>
-
$effect
for mutating multiple items.<script> // OLD $: if ($enabled) { var2 = true; var3 = 55; } </script>
<script> // NEW $effect(() => { var2 = true; var3 = 55; }); </script>
<script>
// OLD
export let checked = false;
</script>
<input bind:{checked} />
<script>
// NEW
let { checked = $bindable(false) } = $props();
</script>
<input bind:{checked} />
To avoid conflicts of snippet functions and prop names, I prefix the snippet name with s_
.
- Implementing a named slot:
<!-- OLD --> <slot name="opt" {label} {value}>default value</slot>
<!-- NEW --> {#if s_opt}{@render s_opt({ label, value })}{:else}default value{/if}
- Using a named slot:
<!-- OLD --> <Comp let:label let:value> <option slot="opt" {value}>{label}</option> </Comp>
<!-- NEW --> <Comp> {#snippet s_opt({ label, value })} <option {value}>{label}</option> {/snippet} </Comp>
{#snippet children(var)}
children
snippet only has to be defined when parameters are passed to the snippet, otherwise you don't need it for children of a Component.
- Replace all
on:<Event>
props withon<Event>
. Soon:click
becomesonclick
. - Any component waiting for a custom component event that used to use
createEventDispatcher
should alter it's handler argument and ditch thedetail
prop.
Custom attributes are not applied or stripped away if they're not standard for a DOM element. Only way to get around this is by manually setting the attribute or changing the attribute to something like data-<Attribute>
.
<!-- OLD -->
<div class="folder" {open}>
<!-- NEW (switch to data attribute) -->
<div class="folder" data-open={open || null}>
<script>
// NEW (manually set attribute)
let folderRef;
$effect(() => {
(open)
? folderRef.setAttribute('open', '')
: folderRef.removeAttribute('open');
});
</script>
<div class="folder" bind:this={folderRef}>
- Using
console.log
for$state
values seems to cause an infinite loop sometimes. Instead use$inspect(<state_var>);
, usually just place it after the var definition.$inspect
will track all updates so it doesn't have to be placed in specific areas.
-
Component re-rendering everything on one property change (often happens when rendering multple items via
each
).- Passing an entire Object for an
#each
key can cause full re-renders of elements. So try to set a unique key that isn't a prop that gets updated (unless you want it to trigger an update).<!-- could cause issues --> {#each arr of item (item)}
<!-- could cause issues --> {#each arr of item (item.name)}
- Passing an entire Object for an
-
Whitespace not trimmed consistently.
{#if epName}- {#if epNumCombined}({epNumCombined}) {/if}{epName}{/if}
Old:
- (25) Some Name
.
Current:- (25)Some Name
.To get around this, you can force a space character (
{' '}
):{#if epName}- {#if epNumCombined}({epNumCombined}){' '}{/if}{epName}{/if}