Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,13 @@ You can build your entire app with Svelte (for example, using an application fra

> You'll need to have basic familiarity with HTML, CSS and JavaScript to understand Svelte.

This tutorial is split into four main parts:
This tutorial is split into five main parts:

- [Basic Svelte](/tutorial/welcome-to-svelte) (you are here)
- [Advanced Svelte](/tutorial/tweens)
- [Basic SvelteKit](/tutorial/introducing-sveltekit)
- [Advanced SvelteKit](/tutorial/optional-params)
- [Migrating from Svelte 4 to 5](/tutorial/migration-overview)

Each section will present an exercise designed to illustrate a feature. Later exercises build on the knowledge gained in earlier ones, so it's recommended that you go from start to finish. If necessary, you can navigate via the menu above.

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<script>
import { page } from '$app/stores';
</script>

{#if $page.status === 404}
<h1>Not found</h1>
<p><a href="/">Go to /</a></p>
{:else}
<p>
Server-side rendering failed with HTTP status
code
<a
target="_blank"
href="https://http.dog/{$page.status}"
>{$page.status}</a
>
</p>
{/if}

<style>
h1 {
font-weight: 200;
font-size: 2rem;
margin: 0 0 0.5em 0;
}
</style>
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const ssr = false;
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<script>
import App from '$lib/App.svelte';
</script>

<App />
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<script>
let version = $state(4);
</script>

<h1>Welcome to Svelte {version}!</h1>

<button onclick={() => version = 5}>Upgrade</button>
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
title: Migration overview
---

Svelte 5 is the next incarnation of Svelte. It's the culmination of our learnings over the course of five years.

On first look a lot of things have changed between version 3/4 and 5. But on a closer look you will find a lot of similarities, many unchanged parts of the syntax, and hopefully the same familiar feeling that got you into Svelte.

This tutorial is meant for people who already know Svelte and are upgrading to Svelte 5. It will go through each of the syntax changes along with explanations of why we did it. For a complete list of all changes (however tiny they are) head over to _TODO LINK TO LIST OF BREAKING CHANGES_.
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<script>
let count = 0;
</script>

<button on:click={() => count++}>{count}</button>
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<script>
let count = $state(0);
</script>

<button on:click={() => count++}>{count}</button>
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
---
title: State
---

At the heart of Svelte 5 is the new runes API. Runes are basically compiler instructions that inform Svelte about reactivity. Syntactically, runes are functions starting with a dollar-sign.

In Svelte 4, a `let` declaration at the top level of a component was implicitly reactive. In Svelte 5, things are more explicit: a variable is reactive when created using the `$state` rune. Let's migrate the counter to runes mode by wrapping the counter in `$state`:

```svelte
<script>
let count = +++$state(+++0+++)+++;
</script>
```

Nothing else changes. `count` is still the number itself, and you read and write directly to it, without a wrapper like `.value` or `getCount()`.

## Why we did this

`let` being implicitly reactive at the top level worked great, but it meant that reactivity was constrained - a `let` declaration anywhere else was not reactive. This forced you to resort to using stores when refactoring code out of the top level of components for reuse. This meant you had to learn an entirely separate reactivity model, and the result often wasn't as nice to work with. Because reactivity is more explicit in Svelte 5, you can keep using the same API in an outside the top level of components. Head to TODO LINK TO TUTORIAL to learn more.
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<script>
let count = 0;
$: double = count * 2;
</script>

<button on:click={() => count++}>{count} * 2 = {double}</button>
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<script>
let count = $state(0);
const double = $derived(count * 2);
</script>

<button on:click={() => count++}>{count} * 2 = {double}</button>
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
---
title: Derivations
---

In Svelte 4, a `$:` statement at the top level of a component could be used to declare a derivation, i.e. state that is entirely defined through a computation of other state. In Svelte 5, this is achieved using the `$derived` rune:

```svelte
<script>
let count = +++$state(+++0+++)+++;
---$:--- +++const+++ double = +++$derived(+++count * 2+++)+++;
</script>
```

As with `$state`, nothing else changes. `double` is still the number itself, and you read it directly, without a wrapper like `.value` or `getCount()`.

## Why we did this

`$:` was a great shorthand and intuitive to get started with: you could slap a `$:` in front of most code and it would somehow work. This intuitiveness was also its drawback the more complicated your code became, because it wasn't as easy to reason about. Was the intent of the code to create a derivation, or a side effect?

There were also gotchas that were hard to spot:

- `$:` only updated directly before rendering, which meant you could read stale values in-between rerenders
- `$:` only ran once per tick, which meant that statements may run less often than you think
- `$:` dependencies were determined through static analysis of the dependencies. This worked in most cases, but could break in subtle ways during a refactoring where dependencies would be for example moved into a function and no longer be visible as a result
- `$:` statements were also ordered by using static analysis of the dependencies. In some cases there could be ties and the ordering would be wrong as a result, needing manual interventions. Ordering could also break while refactoring code and some dependencies no longer being visible as a result.

Lastly, it wasn't TypeScript-friendly (our editor tooling had to jump through some hoops to make it valid for TypeScript), which was a blocker for making Svelte's reactivity model truly universal.

`$derived` fixes all of these by

- always returning the latest value
- running as often as needed to be stable
- determining the dependencies at runtime, and therefore being immune to refactorings
- executing dependencies as need and therefore being immune to ordering problems
- being TypeScript-friendly
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<script>
let count = 0;
$: {
if (count > 5) {
alert('Count is too high!');
}
}
</script>

<button on:click={() => count++}>{count}</button>
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<script>
let count = $state(0);
$effect(() => {
if (count > 5) {
alert('Count is too high!');
}
});
</script>

<button on:click={() => count++}>{count}</button>
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
---
title: Side effects
---

In Svelte 4, a `$:` statement at the top level of a component could also be used to create side effects. In Svelte 5, this is achieved using the `$effect` rune:

```svelte
<script>
let count = +++$state(+++0+++)+++;
---$:---+++$effect(() =>+++ {
if (count > 5) {
alert('Count is too high!');
}
}+++);+++
</script>
```

As with `$state`, nothing else changes. `double` is still the number itself, and you read it directly, without a wrapper like `.value` or `getCount()`.

## Why we did this

Most of the rationale is explained in the previous section already. Additionally, `$:` could be used for all kinds of things: Derivations, side effects, mix the two - you could even create state with it instead of using `let`. This meant that while it was easy to write code, it became harder to read the code and reason about it. With `$derived` and `$effect`, you have a bit more up-front decision making to do (spoiler alert: 90% of the time you want `$derived`), but future-you and other developers on your team will have an easier time.
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<script>
import Nested from './Nested.svelte';
</script>

<Nested required="filled by parent" />
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<script>
export let optional = 'unset';
export let required;
</script>

<p>Optional: {optional}</p>
<p>Required: {required}</p>
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<script>
let { optional = 'unset', required } = $props();
</script>

<p>Optional: {optional}</p>
<p>Required: {required}</p>
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
---
title: Component props
---

In Svelte 4, properties of a component were declared using `export let`. Each property was one declaration. In Svelte 5, all properties are declared through the `$props` rune, through destructuring:

```svelte
<script>
---export let optional = 'unset';
export let required;---
+++let { optional = 'unset', required } = $props();+++
</script>
```

## Why we did this

`export let` was one of the more controversial API decisions, and there was a lot of debate about whether you should think about a property being `export`ed or `import`ed. `$props` doesn't have this trait. It's also in line with the other runes, and the general thinking reduces to "everything special to reactivity in Svelte is a rune".

There were also a lot of limitations around `export let`, which required additional API. These are discussed in the next section.
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<script>
let string = `this string contains some <strong>HTML!!!</strong>`;
</script>

<p>{string}</p>
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<script>
let string = `this string contains some <strong>HTML!!!</strong>`;
</script>

<p>{@html string}</p>
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
---
title: Component props (advanced)
---

There are multiple cases where declaring properties becomes less straightforward than having a few `export let` declarations:

- you want to rename the property, for example because the name is a reserved identifier (e.g. `class`)
- you don't know which other properties to expect in advance
- you want to forward every property to another component

All these cases need special syntax in Svelte 4:

- renaming: `export { klass as class}`
- other properties: `$$restProps`
- all properties `$$props`

In Svelte 5, the `$props` rune makes this straightforward without any additional Svelte-specific syntax:

- renaming: use property renaming `let { class: klass } = $props();`
- other properties: use spreading `let { foo, bar, ...rest } = $props();`
- all properties: don't destructure `let props = $props();`

Let's transform our component to using the new syntax.

```svelte
<script>
---let klass = '';
export { klass as class};---
+++let { class: klass, ...rest } = $props();+++
</script>

<button {class} {...rest}>click me</button>
```

## Why we did this

It should become clear by now that `export let` had a lot of limitations beyond simple cases, which meant we had to add more APIs. With `$props` we can remove a lot of that stuff in favor of a simpler, more robust model.
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
title: $/let/... -> runes
scope: { 'prefix': '/src/lib/', 'name': 'src' }
focus: /src/lib/App.svelte
---
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<script>
let version = $state(4);
</script>

<h1>Welcome to Svelte {version}!</h1>

<button onclick={() => version = 5}>Upgrade</button>
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
title: Dom events
---

Svelte 5 changes the way we do event handling.

TODO on:click -> onclick

## Why we did this

The change in itself doesn't seem all that relevant or even unnecessary, but it unlocks crucial capabilities like using callback props for consistency, spreading events, and more. See the next sections for more details.
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<script>
let count = 0;
</script>

<button on:click={() => count++}>{count}</button>
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<script>
let count = $state(0);
</script>

<button on:click={() => count++}>{count}</button>
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
---
title: Component events
---

TODO some createEventDispatcher stuff migrated to props

## Why we did this

`createEventDispatcher` was always a bit boilerplate-y:

- import the function
- call the function to get a dispatch function
- call said dispatch function with a string and possibly a payload
- retrieve said payload on the other end through a `.details` property, because the event itself was always a `CustomEvent`

It was always possible to use component callback props, but because you had to listen to dom events using `on:`, it made sense to use `createEventDispatcher` for component events due to syntactical consiscency. Now that we have event attributes (`onclick`), it's the other way around: Callback props are now the more sensible thing to do.

They also unlock event spreading, which we discuss in the next section.
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<script>
let count = 0;
</script>

<button on:click={() => count++}>{count}</button>
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<script>
let count = $state(0);
</script>

<button on:click={() => count++}>{count}</button>
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
title: Event forwarding
---

TODO event forwarding

## Why we did this

This is a huge improvement over the old syntax: We had to manually forward each event separately, not know which of the events the consumer is actually interested in. With event spreading, we turn it around - the consumer is now in charge, and the component can spread events like props onto other components or the Dom.
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
title: Event directives -> event attributes
scope: { 'prefix': '/src/lib/', 'name': 'src' }
focus: /src/lib/App.svelte
---
Loading
Loading