Skip to content

Svelte 5 is Here!

Svelte 5 is Here!

Svelte 5 was finally released after a long time in development. Fortunately, we've been able to test it for some time, and now it has a stable release. Let's dig into its features and why this is such a significant change, even though Svelte 4 code is almost 100% compatible.

Svelte syntax everywhere

This is one of my favorite additions. Previously, Svelte syntax was limited to a component <script> element. Refactoring code from a component and moving it to a JavaScript file worked differently. Now you can use the .svelte.js or .svelte.ts extension, which allows you to use the same syntax everywhere. It's important to note that it's a way to express that this is not just JS, and what you write may be compiled to something else, just like .svelte files are not just html even though they look very similar.

Runes

The introduction of runes is one of the most significant changes in Svelte. Many users felt attracted to variables being instantly reactive in previous versions.

let foo = 0; // foo is a reactive variable

There was, however, a lot of magic underneath and a considerable amount of work from the compiler to make it behave reactively.

$state

In Svelte 5, a reactive variable has to be explicitly declared using the $state rune, which brings a clear distinction between what is reactive and what isn’t.

let foo = $state(0); // foo is a reactive variable
let bar = 0; // bar is a not reactive

In the previous code, $state is not an actual function being called; it's a hint for the compiler to do something special with this declaration.

Rune names always start with a dollar sign ($) and do not need to be imported.

However, there’s much more to the changes than just the way we declare reactive variables. Runes bring a lot of new features that make Svelte 5 a great improvement.

In Svelte 5, objects or arrays use Proxies to allow for granular reactivity, meaning that individual properties in an object are reactive, even if they are nested objects or arrays. If you modify a property, it will only update that property and will not trigger an update on the whole object. It also supports triggering updates when methods like array.push are called. In previous versions, an assignment was required to trigger an update:

// Svelte 4
let myArray = [];
myArray.push(0);
myArray = myArray; // assignment triggers reactive update

// Svelte 5
let myArray = $state([]);
myArray.push(0); // no assignment needed

To have a similar behavior of svelte 4 (no deep reactivity), use the $state.raw() rune. This syntax of <rune>.* is common for related features of a rune:

$state.raw() will require an assignment to trigger reactivity.

// Svelte 5
let myArray = $state.raw([]);
myArray.push(0);             // not reactive
myArray = [...myArray, 0];   // reactive 

Because proxies are used, you may need to extract the underlying state instead of the proxy itself. In that case, $state.snapshot() should be used:

let foo = $state({bar:0});

handleFoo(foo); // foo is a Proxy

let foo2 = $state.snapshot(foo);
handleFoo(foo2); // foo2 is an object

$derived

We will use $derived and $derived.by to declare a derived state. The two runes are essentially the same, except one allows us to use a function instead of an expression, allowing for more complex operations to calculate the derived values.

let myState = $state(1);
let derivedState = $derived($state +1);
let derivedState = $derived.by(() => { return $state+1; }); // same thing as above, but we can use the function's body for more complex operations.

$effect

Effects are useful for running something triggered by a change in state. One of the things you'll note from the $effect rune is that dependencies don't need to be explicit. Those will be picked from reactive values read synchronously in the body of the effect function.

let foo = $state(0);
let bar = $state(0);

$effect(() => {
    doSomethingWith(foo); // foo is a dependency
	setTimeout(() => {
		doSomethingWith(bar);
		// bar is not a dependency because it is not read synchronously
	},0); 
})

Something to bear in mind is that these dependencies are picked each time the effect runs. The values read during the last run become the dependencies of the effect.

$effect(() => {
	let coin = Math.random();
	 if(coin > 0.5) {
		 doSomethingWith(foo); // foo is a dependency if condition is true
	 } else {
	     doSomethingWith(bar); // bar is a not dependency if condition is true
	 }
})

Depending on the result of the random method, foo or bar will stop being a dependency of the effect. You should place them outside the condition so they can trigger reruns of the effect.

Variants of the effect rune are $effect.pre, which runs before a DOM update, and $effect.tracking(), which checks for the context of the effect (true for an effect inside an effect or in the template).

$props

This rune replaces the export let keywords used in previous versions to define a component's props. To use the $props syntax, we can consider it to return an object with all the properties a component receives. We can use JavaScript syntax to destructure it or use the rest property if we want to retrieve every property not explicitly destructured..

// all valid syntax
let props = $props();
let { foo, bar } = $props();
let { foo, bar, ...otherProps } = $props();
let { foo = 1 } = $props();

$bindable

If you want to mutate a prop so that the change flows back to the parent, you can use the $bindable prop to make it work in both directions. A parent component can pass a value to a child component, and the child component is able to modify it, returning the data back to the parent component.

let { twoWay = $bindable() } = $props();

$inspect

The $inspect rune only works in dev mode and will track dependencies deeply. By default, it will call console.log with the provided argument whenever it detects a change. To change the underlying function, use $inspect.with():

let foo = $state(0);

$inspect(foo);           // console.log(foo) everytime it changes
$inspect(foo).with(myFn) // myFn(foo)

$host

The host rune gives access to the host element when compiling as a custom element:

<!-- set component as a custom element -->
<svelte:options customElement="my-element" />

<script>
	function onButtonClicked() {
		$host().dispatchEvent(new CustomEvent(“boom”));
	}
</script>

<button onclick={() => onButtonClicked()}>Click me! </button>
<my-element onboom={() => {console.log(“boom”);}}>

Other changes

Another important change is that component events are just props in Svelte 5, so you can destructure them using the $props() rune:

// Child component
let { doSomething } = $props();

// Parent component
<Child doSomething={() => {}} />

A special children property can be used to project content into a component instead of using slots. Inside the components, use the [@render[(https://svelte.dev/docs/svelte/@render) tag to place them.

// MyComponent
<script>
    let {children} = props();
</script>
<div>
{@render children?.()}
</div>

Optional chaining (?.) prevents from attempting to render it if no children are passed in.

If you need to render different components in different places (named slots before Svelte 5, you can pass them as any other prop, and use @render with them.

Snippets

Snippets allow us to declare markup slices and render them conveniently using the render tag (@render). They can take any number of arguments.

{ #snippet boldNumber(number)}
<b>{number}</b>
{/snippet }

<div>
{@render boldNumber(1)}
</div>

<div>
{@render boldNumber(2)}
</div>

Snippets can also be passed as props to other components.

<!-- Parent.svelte -->
<script>
	import Thing from './Thing.svelte';
</script>

	{#snippet simpleSpan()}
		<span>I'm a snippet</span>
	{/snippet}

<Thing additionalContent={simpleSpan}></Thing>

<!-- Thing-svelte -->
<script>
	let { additionalContent } = $props();
</script>

<div>
	<p>not a span</p>
	{#if additionalContent}
		{@render additionalContent()}
	{/if}
</div>

Conclusion

Some exciting changes to the Svelte syntax were introduced while, at the same time, maximum compatibility efforts were made. This was a massive rewrite with numerous improvements in terms of performance, bundle size, and DX. Besides that, a new CLI has been released, making the whole experience of starting a project or adding features delightful. If you haven't tried Svelte before, it's a great time to try it now.

This Dot is a consultancy dedicated to guiding companies through their modernization and digital transformation journeys. Specializing in replatforming, modernizing, and launching new initiatives, we stand out by taking true ownership of your engineering projects.

We love helping teams with projects that have missed their deadlines or helping keep your strategic digital initiatives on course. Check out our case studies and our clients that trust us with their engineering.

Let's innovate together!

We're ready to be your trusted technical partners in your digital innovation journey.

Whether it's modernization or custom software solutions, our team of experts can guide you through best practices and how to build scalable, performant software that lasts.

Prefer email? hi@thisdot.co