Svelte 5: Features You Should Know
After talking to the Svelte core maintainers, we've compiled a small guide of the changes from Svelte 5. Learn about runes with the new $ syntax, snippets, and see some of the questions I asked!
This year I met Simon Holthausen, one of the core maintainers of Svelte working at Vercel. He came to the building of Der Spiegel and to Zeit Online to explain first hand to use the changes of Svelte 5 and more importantly reasons why they did it. So I could not waste my occasion to ask him some questions! In this article I wanted to share with you some of the new features of Svelte 5 and some of the questions I asked him.
Before we start with Svelte 5
I want to leave you here some links that can be very useful before, while and after reading this post:
The links might change, but I’ll try to keep them updated!
Svelte 5: What’s New?
Runes
With runes, they are going to introduce a new syntax to Svelte, the $
syntax. This new syntax will allow you to use the new features of Svelte 5. Let us start with the first rune, $state
.
$state
Declare a variable as reactive, that is, it will change and its change need to be perceived in every place the variable is used.
// svelte 4
<script>let count = 0;</script>
// svelte 5
<script>let count = $state(0);</script>
If we keep it to this level, we will not see any difference in functionality actually. Already without the $state
the variable was acting pretty reactive, since it would update in all the elements using the variable.
// svelte 4
<script>
let count = 1;
</script>
<button on:click={() => count++}>
Increment - This is the result: {count}
</button>
// svelte 5
<script>
let count = $state(1);
</script>
<button onclick={() => count++}>
Increment - This is the result: {count}
</button>
However, there were some undesired side effects that could happen when that variable was used for something else. Take a look at this example:
// svelte 4
<script>
let count = 1;
$: double = count * 2;
console.log(double)
</script>
<button on:click={() => {
count++
console.log(count, double)
}}>
Increment - This is the result: {count}
</button>
Even though we would expect the console.log(double)
to give us an amazing 2
in the moment of the initialization, it will not. It would give us undefined
. And also when we click the button, the count
and double
will not be in sync as you can see in the next image.
But we can definitely fix. Let us see how.
$derived
The rune $derived
is a way to create a derived value from a reactive variable. This way, we can avoid part of the side effects that we saw in the previous example. With just changing one more line of code, it will work as expected.
// svelte 5
<script>
let count = $state(1);
let double = $derived(count * 2);
console.log(double)
</script>
<button onclick={() => {
count++
console.log(count, double)
}}>
Increment - This is the result: {count}
</button>
We were talking about undesired side effects, but there are also desired side effects. Let us see how we can use them with the next rune.
$effect
This new rune will work pretty much like the previous $:
rune. It will run desired side effects when a variable inside changes. If you come from the useEffect
’s React world, this concept might not be very strange to you.
Here you can see how you can use it in Svelte 5 and how it compares to Svelte 4.
// svelte 4
<script>
let count = 1;
$: if (count > 10) {
console.log('count is greater than 10');
}
</script>
<button on:click={() => {
count++
}}>
Increment - This is the result: {count}
</button>
// svelte 5
<script>
let count = $state(1);
$effect(() => {
if (count > 10) {
console.log('count is greater than 10');
}
})
</script>
<button onclick={() => {
count++
}}>
Increment - This is the result: {count}
</button>
$props
Let us continue to what I think is a very interesting feature of Svelte 5. By using $props
, we can access the props of a component in a simpler way than with the former export let variable
syntax. Let us see how it works.
In Svelte 4:
// svelte 4 - Parent.svelte
<script>
import Child from './Child.svelte';
let count = 1;
let title = 'Your Count';
</script>
<Child {count} {title} />
// svelte 4 - Child.svelte
<script>
export let count;
export let title;
</script>
<div>{title} - {count}</div>
And here is how it looks in Svelte 5.
// svelte 5 - Parent.svelte
<script>
import Child from './Child.svelte';
let count = $state(1);
let title = $state('Your Count');
</script>
<Child {count} {title} />
// svelte 5 - Child.svelte
<script>
let {count, title}: {count: number, title: string} = $props();
</script>
<div>{title} - {count}</div>
Apart from a better support for TypeScript, we can see that the syntax is much more concise and that we can destructure the props in the same line. It also allows us now to pass “all the props that you do know yet you could need” to the child component, which is pretty useful when you are working with a design system and component libraries. Here you have an example of what I mean by that.
// svelte 5 - Parent.svelte
<script>
import Button from './Button.svelte';
</script>
<Button onclick="() => console.log('This is a cool Tutorial')">
Say Hi
</Button>
// svelte 5 - Button.svelte
<script>
let {children, ...rest} = $props();
</script>
<button {...rest}>
{@render children()}
</button>
In this way, we have a very flexible button component that can be used in many different ways. We can pass any prop to it and it will work as expected. Have you seen the @render
syntax? This is another new feature of Svelte 5 that I will explain in the next section.
snippets
Snippets are a new feature of Svelte 5 that will allow you to create reusable pieces of code that can be used in different parts of your application. This is a very powerful feature that will help you to keep your code DRY and to make your components more readable and maintainable.
Let me show you one cool example that I found in the Svelte 5 Documentation.
<script>
let { message = `it's great to see you!` } = $props();
</script>
{#snippet hello(name)}
<p>hello {name}! {message}!</p>
{/snippet}
{@render hello('alice')}
{@render hello('bob')}
It is pretty straightforward, right? You can see that we are creating a snippet called hello
that takes a name
as a parameter and renders a paragraph with a message. We are then rendering this snippet with different names. This is a very simple example, but you can imagine how powerful this feature can be when you are working with more complex components.
For example, we are using in production a Popover.svelte
component that accepts a trigger
and a content
prop. We are using snippets to create different types of popovers that can be used in different parts of our application.
Here we have another simple example where we can pass different snippets for a List.svelte
. We want to pass the items
, but also consider the case when the list is empty
.
// Parent.svelte
<script>
import List from './List.svelte';
let items = $state([])
</script>
{#snippet row(item)}
{item}
{/snippet}
{#snippet empty(item)}
<p>We do now have items, unfortunately.</p>
{/snippet}
<List {items} {row} {empty} />
// List.svelte
<script>
let {items, row, empty} = $props()
</script>
{#if items.length}
<ul>
{#each items as item}
<li>{@render row(item)}</li>
{/each}
</ul>
{:else}
{@render empty?.()}
{/if}
In this case, we will render the content of the empty
snippet for each item in the list since the list is just a sad empty array. However, we can imagine that we are fetching somehow the data and that the list will be populated at some point. Let’s assume that instead of an empty array, we have an array with some items.
// Parent.svelte
<script>
import List from './List.svelte';
let items = $state(["Element 1", "Element 2", "Element 3"]);
</script>
Then we will see the list with the items and not the empty
snippet. This is a very powerful feature that will help you to create more flexible and reusable components.
Deprecated features
Some features of Svelte 4 are deprecated in Svelte 5. For example:
- The
svelte:component
directive is deprecated. But we can now do something like this<svelte:options customElement="my-custom-element" />
to create a custom element. - The beforeUpdate and afterUpdate lifecycle hooks are deprecated.
- The
export let
syntax is deprecated in favor of the new$props
syntax. - The
$:
syntax is deprecated in favor of the new$effect
syntax. - The
$$props
and$$restProps
are deprecated in favor of the new$props
syntax. - The dispatch way of creating custom events with
createEventDispatcher()
. It is recommended that you just pass the fallback through props and let the parent component handle the event.
We can breath in and out, since the Svelte team is not planing to deprecate onMount
and stores
. They are still there and they are still working as expected.
Also there is a way of creating semi custom hooks (like you could do in React) with getters and setters. I would love to go deep in this topic, since this helped us a lot in our latest project, but I will leave it for another post. If you are interested in this topic, let us discuss it in the meantime in my LinkedIn.
Questions to the Svelte Core Maintainer
Having the opportunity to talk to Simon Holthausen, I could not resist to ask him some questions. Here are some of the questions I asked him:
Is it possible to add some logic in the iterated values of an each block?
You know that in other frameworks, like React, we can add some logic in the iterated values of a map function. Let us imagine that we have a Navigation.tsx
component and we want to add a isActive
class to the active link. We could do something like this:
// React
import React from 'react'
const links = [
{ id: 1, text: 'Home', url: '/' },
{ id: 2, text: 'About', url: '/about' },
{ id: 3, text: 'Contact', url: '/contact' },
]
const Navigation = ({ links, activeLink }) => {
return (
<nav>
{links.map((link) => (
<a
key={link.id}
href={link.url}
className={link.id === activeLink ? 'isActive' : ''}
>
{link.text}
</a>
))}
</nav>
)
}
But also we could do the following:
// React
... the rest of the code
const Navigation = ({ links, activeLink }) => {
return (
<nav>
{links.map((link) => {
const isLinkActive = link.id === activeLink
return (
<a
key={link.id}
href={link.url}
className={isLinkActive ? 'isActive' : ''}
>
{link.text}
</a>
)
})}
</nav>
)
}
You can also do this in Svelte 4, but you need to create a new variable for each link. There is already a way to do it by using @const
in the each
block.
{#each items as item}
{@const isActive = item.id === activeLink}
<a href={link.url} class:active={isActive}>
{link.text}
</a>
{/each}
This would work for a very simple case, but let us imagine that I need to do a more complex logic, or even, just do a console.log in the middle of the each block for debugging reasons. This would not be possible with the @const
syntax. Simon told me that this is already the point where you need to create a new component. This is a very interesting point, since it is a very different approach to the one we are used to in React.
// List.svelte - Parent
{#each items as item}
<ListItem {item}>
{/each}
// ListItem.svelte - Child
<script>
let {item} = $props()
console.log(item)
let isActive = item.id === activeLink
console.log(isActive)
</script>
<a href={link.url} class:active={isActive}>
{link.text}
</a>
Will stores be deprecated in the future?
Simon said that they are not planning to deprecate stores. They are still there and they are still working as expected, but that there will be other ways to workaround the use of stores, like this new way of creating semi custom hooks with getters and setters.
Migration guide from Svelte 4 to Svelte 5
As you can see, there were a lot of changes in Svelte 5, but the Svelte team has made it easier for us to migrate from Svelte 4 to Svelte 5. The team alrady encourage you to incrementally migrate your components as soon as you can, so you can take advantage of the new features and improvements. They are planning to add a CLI tool to help with the migration, but for now, you can take a look at how you can do it in the Svelte 5 Playground
FAQ about Svelte 5
Share article