Svelte 5 Is Coming

| Published: January 30, 2024

| Updated: February 23, 2024

The word is out that Svelte 5 will be released early 2024, and we’re starting to see glimpses of what’s to come.

It’s a complete rewrite of the Svelte framework, and the hype around it makes it one of the most anticipated web-dev releases of 2024.

Did you say Svelte 5 is coming?

By following the Svelte repo on Github and the statements made by the founder, Rich Harris, we can expect:

  • many syntax overhauls,
  • a bunch of new features, and
  • massively improved performance

In this article, we’ll walk through some of the main changes we’ve seen so far in the beta, and inform you how to best prepare for when Svelte 5 officially arrives.

Skip to FAQ’s

Is Svelte 5 Ready yet?

Svelte 5 is not ready yet, however, an early 2024 release is expected.

To see some of the expected features coming up, check out the SvelteKit Roadmap, which looks into the milestones on the GitHub Page. You can also check the Test Status page to monitor Svelte 5’s test status across 4,000+ tests.

via GIPHY

How to get the Svelte 5 Beta

If you don’t want to go through the process of scaffolding a new project on your computer, you can use the Svelte 5 playground to build simple components and test out the new features.

However, if you want to do it properly and create a project with the Svelte 5 preview on your computer, you can get started by entering the following command in your terminal:

npm create svelte@latest svelte-5-test-app

Then you’ll be prompted to scaffold the project with the latest Svelte 5 Beta.

Starting a project with Svelte 5

If you select this option and start writing components with the syntax you’re used to from Svelte 4, you’ll be getting errors popping up all over the place.

Luckily, the working changes in Svelte 5 are intuitive for the first part, and open up opportunities to create more reliable apps without the need for ’hacks’ to get around the quirks of earlier Svelte versions.

Current features in the Svelte 5 Beta

When you install the Svelte 5 beta, you’ll now have access to the following features:

  1. Runes,
  2. Snippets,
  3. Event Handler Changes,
  4. The untrack() function, and
  5. The unstate() function,

There are also some other dev-experience improvements with Svelte 5, like Typescript in your markup.

via GIPHY

Let’s get right to it with runes.

Runes

Runes was the first big feature announced for Svelte 5, and for good reason. It’s a big change to how you do basic variable declarations and effect management in your .svelte components.

Initially runes caught some negative feedback, as some of the API bears resemblance to React, however as more developers start playing with the new approach, the overall sentiment feels more positive.

The main proposition runes bring to the table is more fine-grained reactivity. To be more precise, runes have these properties:

  1. a suite of new syntax for creating reactive variables,
  2. runtime reactivity,
  3. aligned behavior between .svelte files and .js/.ts files,
  4. reactive objects and arrays,
  5. making a number of keywords and methods from Svelte 4 obsolete 👀

In this article we’ll touch only touch on a couple of the changes, so I encourage you to read the Guide to Runes, which look at all of the runes and how they’ll change your workflow moving forward.

Basic Syntax Changes with Runes

To write a reactive variable with Svelte 4, you would declare it as this:

// ❌ Old way

let age = 20;

With Runes, you would declare the same variable like this:

// ✅ New way with runes

let age = $state(20);

You can see why there was an immediate recoil from some of the Svelte community. 😅

via GIPHY

I promise there’s a case for this more verbose syntax, but we’ll get to that in a bit.

There are 9 runes in the Svelte 5 beta:

  1. $state,
  2. $state.frozen,
  3. $derived,
  4. $props,
  5. $inspect,
  6. $effect,
  7. $effect.pre,
  8. $effect.active, and
  9. $effect.root,

The first 6 mentioned are the most useful and important to know when using Svelte 5. So now we’ll walk through each of the runes, with a summary of their purpose and a code block to demonstrate their usage.

via GIPHY

Before we start going into each of the runes, make sure you’ve opted in to use them in a Svelte 5 beta project, or use the Svelte 5 playground with runes on by default.

$state rune

The $state rune declares a new reactive variable which can be used beyond the <script> tag in .svelte components.

✔ New Way with $state

let name = $state('Jack')

❌ Old Way:

let name = "Jack"

$state.frozen rune

The $state.frozen rune is the same as the $state rune, however the variable cannot be mutated - it can only be reassigned.

let students = $state.frozen(['Jack', 'Sam', 'Mary']);

// ❌ This won't work as you are attempting to mutate the value
students.push('Max');

// ✅ Reassign the value instead:
students = [...students, studentInput];

If you wish to have a reactive variable which can be mutated, use the $state rune instead:

let students = $state(['Jack', 'Sam', 'Mary']);

// ✅ This will work as $state runes can be mutated
students.push('Max');

via GIPHY

$derived rune

The $derived rune allows you to create a reactive variable who’s value is dependent on some other variable.

✔ New Way with $derived

let area = $derived(width * height)

❌ Old Way:

$: area = width * height;

$props rune

The $props rune allows you to accept parameters into a .svelte component:

✔ New Way with $props

type Props = {
	name: string;
	string;
	number;
}

let {name, email, age} = $props<Props>()

❌ Old Way:

export let name: string; export let email: string; export let age: number;

via GIPHY

$inspect rune

The $inspect rune will console.log a value every time it changes, and will also provide context as to whether it’s the initialized value, or whether it’s being updated:

✔ New Way with $inspect

let points = $state(0) $inspect(points)

❌ Old Way:

let points = 0 $: points, console.log(points)

via GIPHY

It’s worth noting that the $inspect rune will only run in the dev environment.

$effect rune

The $effect rune accepts an anonymous function which will run each time one of the variables within the anonymous function changes, and also when the component is mounted to the DOM.

The $effect rune looks like this:

$effect(() => {
    // Do something
    return {
	    // Do something with cleanup
    }
})

via GIPHY

Yep, this is quite a bit like the useEffect hook we all know and love from React! 🤦‍♂️

This rune will replace a list of different functions and features in Svelte 4, including:

Creating an effect to do an action when a variable changes

✔ Call an effect when a variable changes with $effect

<script>
	let name = $state('Jack');

	$effect(() => {
		alert('New Name is ' + name);
	});
</script>

As name is inside the $effect, it is added to the list of dependencies for the effect, such that when any of them change, the $effect is run.

❌ Old Way:

<script>
	let name = 'Jack';

	$: name, alert('New Name is ' + name);
</script>

A new syntax for onMount

✔ Calling a function when the component is mounted to the DOM with $effect

<script>
	$effect(() => {
		alert('The component is mounted');
	});
</script>

❌ Old Way:

<script>
	import { onMount } from 'svelte';
	onMount(() => {
		alert('The component is mounted');
	});
</script>

via GIPHY

A new syntax for onDestroy

✔ Calling a function when the component is destroyed from the DOM with $effect

<script>
    $effect(() => {
        return {
	        alert("The component is destroyed");
        }
    })
</script>

Notice that when the component is destroyed, the contents inside the return of the $effect rune are executed.

❌ Old Way:

<script>
	import { onDestroy } from 'svelte';
	onDestroy(() => {
		alert('The component is destroyed');
	});
</script>

via GIPHY

A new syntax for afterUpdate

✔ Call an effect when a variable changes with $effect

<script>
	let name = $state('Jack');

	$effect(() => {
		alert('The component has updated');
	});
</script>

As the $effect runs after the DOM has been updated, it’s effectively an afterUpdate() call.

❌ Old Way:

<script>
	import { afterUpdate } from 'svelte';
	afterUpdate(() => {
		alert('The component is mounted');
	});
</script>

The afterUpdate() and beforeUpdate() functions will be deprecated in Svelte 5.

via GIPHY

If you wish to learn about how to perform beforeUpdate with the new rune system, you can do that with the $effect.pre rune.

A short word on runes

Given that the runes we’ve just covered will be the only ones you need to know 95% of the time, we’ll now move onto how you can opt into using runes. However, if you wish to dig a bit deeper, the guide to runes goes into depth for all of the runes.

The runes we didn’t cover in this article are:

Opting in to use Runes with the Svelte 5 Beta

If you’re using the Svelte 5 beta, you’ll have to specifically ’opt-in’ to use runes, which means you will forgo the use of the reactive variables that you had in prior Svelte versions.

The best part is, you can opt-in at the project level, or just at the component level. So if you already have a project, you won’t have to update everything in your project at once. Your old components will still work and the migration process can be gradual - if you even want to move to runes at all!

How to Opt Into Runes

You can scope opting-in to runes at the component level by adding this tag anywhere in your .svelte file:

<svelte:options runes={true} />

Or,

You can do across your whole project level in your svelte.config.ts file, by adding runes: true to your compilerOptions:

export default {
	compilerOptions: {
		runes: true
	}
};

Okay, we’re done with runes now. 🥳

via GIPHY

With runes being based more on the script side of things, it’s time to venture down to our markup. The next discussion point is a markup feature which allows us to write cleaner code and improve developer experience when building large and complex UI’s.

So, let’s move on to Snippets!

Snippets

Snippets are designed to make you stop repeating code, by providing an interface for making reusable markup blocks, or rather - snippets!

Isn’t that exactly what a component is?

Well, almost. But not quite!

It’s more like a <slot /> that you would’ve use in Svelte 4. The difference is that with snippets, you can assign arguments to them, and they are more flexible.

via GIPHY

Let’s say you have a component that looks like this:

+page.svelte

{#each profiles as profile}
	{#if profile.approved && profile.id}
		<a href={`/profile/${profile.id}`}>
			<div class="card">
				<div class="flex">
					<img src={profile.image} />
					<p>Name: {profile.name}</p>
				</div>
				<p>{profile.username}</p>
			</div>
		</a>
	{:else}
		<div class="card">
			<div class="flex">
				<img src={profile.image} />
				<p>Name: {profile.name}</p>
			</div>
			<p>{profile.username}</p>
		</div>
	{/if}
{/each}

We can see a repeatable code block for both conditions:

<div class="card">
	<div class="flex">
		<img src={profile.image} />
		<p>Name: {profile.name}</p>
	</div>
	<p>{profile.username}</p>
</div>

So, we can make that code block a snippet like this:

{#snippet card(profile)}
    <div class="card">
        <div class="flex">
            <img src={profile.image}>
            <p>Name: {profile.name}</p>
        </div>
        <p>{profile.username}</p>
    </div>
{/snippet}

And then use it anywhere in our component like this:

{@render card(profile)}

So the full example would look like this:

{#snippet card(profile)}
    <div class="card">
        <div class="flex">
            <img src={profile.image}>
            <p>Name: {profile.name}</p>
        </div>
        <p>{profile.username}</p>
    </div>
{/snippet}

{#each profiles as profile}
    {#if profile.approved && profile.id}
        <a href={`/profile/${profile.id}`}>
            {@render card(profile)}
        </a>
    {:else}
        {@render card(profile)}
    {/if}
{/each}

via GIPHY

Passing Snippets to Components

You can pass snippets to component in 2 ways:

via GIPHY

But first, lets look at the component that will accept the snippet into it:

List.svelte

<script lang='ts'>
    let { data, listHead, listRow } = $props();
</script>

<div>
    {#if listHead}
        {@render listHead()}
    {/if}
    {#if listRow}
        <ul>
            {#each data as item}
                <li>{@render listRow(item)}</li>
            {/each}
        </ul>
    {/if}
</div>

Now let’s look at how we could pass a snippet into this component.

Passing a snippet to a component as a prop

FriendsList.svelte

<script lang='ts'>
    type Friend = {
        id: string | null;
        name: string;
        username: string;
        image: string;
        approved: boolean;
    };

    const friends: Friend[] = [
        {
            id: '1',
            name: 'Ben Beaker',
            username: 'beakerman',
            image: 'https://fakeimage.com/beakerman',
            approved: true,
        },
        {
            id: null,
            name: 'Mary Fisher',
            username: 'fish',
            image: 'https://fakeimage.com/fish',
            approved: false,
        }
    ];
</script>

{#snippet listHead()}
    <h1>This is my friends list</h1>
{/snippet}

{#snippet listRow(profile)}
    <div class="card">
        <div class="flex">
            <img src={profile.image}>
            <p>Name: {profile.name}</p>
        </div>
        <p>{profile.username}</p>
    </div>  
{/snippet}

<List data={friends} {listHead} {listRow} />

Passing a snippet to a component as a child

As was the case with slot’s, if a component had a <slot /> in the markup somewhere, we could add markup between the components tags.

<List data={friends}>
    {#snippet listHead()}
        <h1>This is my friends list</h1>
    {/snippet}

    {#snippet listRow(profile)}
        <div class="card">
            <div class="flex">
                <img src={profile.image}>
                <p>Name: {profile.name}</p>
            </div>
            <p>{profile.username}</p>
        </div>  
    {/snippet}
</List>

Because snippets have names, they are equivalent to a named slot in Svelte 4. And therefore no changes need to happen for the component accepting the snippet props:

let {(data, listHead, listRow)} = $props();

That’s enough to get you by with snippets. Remember, <slot /> will be deprecated in Svelte 5, so it’s important to understand snippets!

via GIPHY

If you want to learn more, check out the guide to snippets which goes into more detail about how to use them and provides some additional examples, including porting a real-life Aceternity UI component from React to Svelte using snippets.

Now we’ve covered the major breaking changes to Svelte 5, we can move onto the subtle changes and improvements.

Changes to Event Handlers

In Svelte 5, the element event listeners will move from on: directives, to being their own own property.

So on:click becomes onclick, and on:mouseenter becomes onmouseenter… etc.

There’s some benefits, and consequences as a result of this change.

A few benefits include:

  • Guaranteeing typesafety,
  • Improving readability,
  • Ability to spread event handlers (i.e. <button {...props} />)

Another emergent property of removing directives is that if you have generically named functions (e.g. function onclick()) to do something when process these events, you can now use the shorthand prop declaration when adding it to an element.

For example, if you had this function in your component:

<script>
	function onclick() {
		console.log('Clicked');
	}
</script>

❌ This old way of triggering the onclick function above would be:

<button on:click={onclick}>Click Me</button>

✅ The new way:

<button {onclick}>Click Me</button>

Or if your function was called onMouseClick, then you would have:

<button onclick={onMouseClick}>Click Me</button>

Just remember, there’s no on: directives on elements anymore - they’ve become regular properties without the colon (:).

Event Modifiers are now gone

Now that the element directives are gone on the elements, we don’t have access to the event modifiers, which were effectively shortcuts for common event modifications.

via GIPHY

For example, in Svelte 4 you could turn this:

<!-- ✅ You have to move forward like this -->
<button
	on:click={(event) => {
		event.preventDefault(); // Do Something
	}}>Click Me</button
>

Into this:

<!-- ❌ You can't do this anymore -->
<button
	on:click|preventDefault={(event) => {
		// Do Something
	}}>Click Me</button
>

which you can’t do anymore.

This applies to all event modifiers, which include (but are not limited to):

  • stopPropagation(),
  • capture(),
  • once(),
  • self(), etc.

Duplicate event handlers on elements are not possible anymore

Previously in Svelte 4 you could attach multiple duplicate events to an element.

For example, you could have 2 on:click directives on a single element, which would look like this:

<!-- ❌ Don't do this anymore -->

<button on:click={functionOne} on:click={functionTwo}>Click Me</button>

But now in Svelte 5, we have to move this into the 1 property, which you can do by creating a function and then calling functionOne and functionTwo inside that function:

<!-- ✅ Do this instead -->

<button
	onclick={(event) => {
		functionOne(event);
		functionTwo(event);
	}}>Click Me</button
>

No more createEventDispatcher

In Svelte 5, the createEventDispatcher function is being deprecated, in favor of accepting callback props.

via GIPHY

So now in a child component, you can set the $props to take in a callback function.

Then instead of the createEventDispatcher which would fire off a name and detail when something happens in the <Child /> component, it just calls the passed in callback function assigned to it.

For example:

<script lang="ts">
	let { add, subtract } = $props();
</script>

<button onclick={add}>Add</button>
<button onclick={subtract}>Subtract</button>

And in the parent component:

<script lang="ts">
	let number = $state(0);
</script>

<Child add={() => number++} subtract={() => number--} />

This is how you should use use events from a child to its parent.

These are all the major changes done to the event handlers, so now let’s look at some functions that were introduced to Svelte 5.

The untrack function

The untrack function is used to escape a variable from being used as a dependency in our #effect or #derived runes.

via GIPHY

For example, if we have the following #effect rune:

<script>
	import { untrack } from 'svelte';

	let name = $state('Jack');
	let age = $state(30); // ❌ age is now in the $effect dependency list when it shouldn't be

	$effect(() => {
		console.log("We're talking about a new person");
		console.log('Name: ', name);
		console.log('Age: ', age);
	});
</script>

The problem here is that every time age changes, we will log We're talking about a new person, which is untrue if the age changes.

What we want is the ability to change the age value, but also log the name and age when the person is different, i.e. name changes.

So we would untrack the age variable:

<!-- ✅ Remove age from the $effect dependency list like this -->

$effect(() => {
    console.log("We're talking about a new person")
    console.log('Name: ', name)
    console.log('Age: ', untrack(() => age))
})

So that now when name changes, the $effect is run, but if age changes, the $effect doesn’t get run.

via GIPHY

Or if you wanted to use this in a $derived rune, you’d use it like this:

let totalOwing = $derived(latestPurchaseTier * untrack(() => rate))

You’d use this if you wanted to didn’t want the $derived value to respond to changes in one of your dependencies. So, if it changes, it doesn’t effect the $derived value, however if others change (that are not untracked), the $derived value will change.

So in the example above , we only want to apply the rate at the time the latestPurchaseTier changes. So we can move the rate around all we want, but it will only affect totalOwing at the timestamp when the latestPurchaseTier changes, so we use untrack() on the rate.

via GIPHY

The unstate function

For objects and arrays made with $state, the unstate function will return the plain value of the object/array without its reactivity. This means that if you were to log it, you would get the actual value you want, rather than a Proxy(Object).

For example, if you had this:

<script>
	import { unstate } from 'svelte';

	let person = $state({ name: 'Jack', age: 10 });

	$effect(() => {
		// ❌ Will log Proxy(Object)
		console.log(person);
	});
</script>

<input bind:value={person.name} />
<input bind:value={person.age} type="number" />

When you want to log the person, you won’t get the actual value of the object, but rather a Proxy(Object) with the value inside it.

But in the $effect, if we changed:

console.log(person) // Proxy(Object)

to:

console.log(unstate(person)) // {name: 'Jack', age: 10}

It will now log the person object value: { name: 'Jack', age: 10 }.

via GIPHY

You would typically use this for when you can’t pass a ProxyObject into a function or API, such as using a library.

So now you know the 2 additional functions in Svelte 5 which allow us to handle some of the edge cases caused by runes.

Now we need to talk about something near and dear to my heart: It’s a solution to the problem that made me give up on Svelte in the early days before committing.

Svelte now crushes it with typescript.

via GIPHY

You can now use Typescript in your Svelte markup

With the Svelte 5 preview, we can use typescript syntax everywhere, including .svelte components outside of the <script> tags.

In my article about Svelte {#each} blocks, I discussed in depth about Svelte’s inability to have typescript syntax inside of the Svelte markup.

In the article, I cited a common example where the lack of typescript in Svelte markup meant we had to do some crazy and unsafe workarounds to make a code block work.

We can now use the as type assertion keyword, so a code block like this will work:

<script lang="ts">
    let { numberArray } = $props<{numberArray: (number | undefined)[]}>()
</script>

{#each numberArray.filter((element) => Boolean(element)) as item}
    <p>{Math.floor(item as number)}</p> <!-- ✔️ "as number" now compiles -->
{/each}

However, in Svelte 4 and earlier, you have to initialize an entirely new reactive variable and assert it inside the <script> tags:

<script lang="ts">
	export let numberArray: (number | undefined)[]; // Svelte 4: we'd have to create asserted variable here ↓
	$: filteredNumArray = numberArray.filter((element) => Boolean(element)) as number[];
</script>

{#each filteredNumArray as item}
	<p>{Math.floor(item)}</p>
{/each}

As the size of applications grow and you start dealing with complex types, having this new typescript syntax in your .svelte components is a huge win.

via GIPHY

Conclusion

It’s likely we can begin measuring the upcoming release date of Svelte 5 in just days now.

Runes has knocked a lot of the familiar processes for 6, but once you spend 20 minutes developing a quick feature with them, it’s pretty straightforward. Some of the design approaches are still up in the air, so there’s every likelihood some of the runes will be amended, either under the hood or explicitly in the API.

There’s a lot of edge cases which might trip you up with Svelte 5, so if your app isn’t compiling properly despite following the guidance in this article, check this full list of breaking changes to see if any is relevant to you.

If you’re playing around with the Svelte 5 preview and spot a bug, don’t forget to pop it in their Github Issues page. And while you’re there, check out the milstones for version 5.0 so you have a better idea what’s coming up.

Hope you learnt lots from this article! ❤️

FAQ’s

Thanks for reading ❤️

Here are some other articles I think you might like!