Created
May 10, 2025 09:52
-
-
Save diramazioni/ae3792d37ed0678d1c68de08722ca287 to your computer and use it in GitHub Desktop.
svelte 5 & sveltekit 2 migration guide
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
## docs/svelte/07-misc/07-v5-migration-guide.md | |
--- title: Svelte 5 migration guide --- Version 5 comes with an overhauled syntax and reactivity system. While it may look different at first, you'll soon notice many similarities. This guide goes over the changes in detail and shows you how to upgrade. Along with it, we also provide information on _why_ we did these changes. You don't have to migrate to the new syntax right away - Svelte 5 still supports the old Svelte 4 syntax, and you can mix and match components using the new syntax with components using the old and vice versa. We expect many people to be able to upgrade with only a few lines of code changed initially. There's also a [migration script](#Migration-script) that helps you with many of these steps automatically. ## Reactivity syntax changes 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. ### let β $state 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()`. > `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 outside the top level of components. Head to [the tutorial](/tutorial) to learn more. ### $: β $derived/$effect 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); $:constdouble =$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 `getDouble()`. A `$:` statement 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> ``` Note that [when `$effect` runs is different]($effect#Understanding-dependencies) than when `$:` runs. > `$:` was a great shorthand and easy 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? 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. > > 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` and `$effect` fix 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 needed and therefore being immune to ordering problems > - being TypeScript-friendly ### export let β $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> ``` 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();` ```svelte <script> let klass = ''; export { klass as class}; let { class: klass, ...rest } = $props(); </script> <button class={klass} {...$$restPropsrest}>click me</button> ``` > `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, as shown above. `$props` unite this in one syntactical concept that leans heavily on regular JavaScript destructuring syntax. ## Event changes Event handlers have been given a facelift in Svelte 5. Whereas in Svelte 4 we use the `on:` directive to attach an event listener to an element, in Svelte 5 they are properties like any other (in other words - remove the colon): ```svelte <script> let count = $state(0); </script> <button on:click={() => count++}> clicks: {count} </button> ``` Since they're just properties, you can use the normal shorthand syntax... ```svelte <script> let count = $state(0); function onclick() { count++; } </script> <button {onclick}> clicks: {count} </button> ``` ...though when using a named event handler function it's usually better to use a more descriptive name. ### Component events In Svelte 4, components could emit events by creating a dispatcher with `createEventDispatcher`. This function is deprecated in Svelte 5. Instead, components should accept _callback props_ - which means you then pass functions as properties to these components: ```svelte <!file: App.svelte> <script> import Pump from './Pump.svelte'; let size = $state(15); let burst = $state(false); function reset() { size = 15; burst = false; } </script> <Pump on:inflate={(power) => { size += power.detail; if (size > 75) burst = true; }} on:deflate={(power) => { if (size > 0) size -= power.detail; }} /> {#if burst} <button onclick={reset}>new balloon</button> <span class="boom">π₯</span> {:else} <span class="balloon" style="scale: {0.01 * size}"> π </span> {/if} ``` ```svelte <!file: Pump.svelte> <script> import { createEventDispatcher } from 'svelte'; const dispatch = createEventDispatcher(); let { inflate, deflate } = $props(); let power = $state(5); </script> <button onclick={() =>dispatch('inflate', power)inflate(power)}> inflate </button> <button onclick={() =>dispatch('deflate', power)deflate(power)}> deflate </button> <button onclick={() => power--}>-</button> Pump power: {power} <button onclick={() => power++}>+</button> ``` ### Bubbling events Instead of doing `<button on:click>` to 'forward' the event from the element to the component, the component should accept an `onclick` callback prop: ```svelte <script> let { onclick } = $props(); </script> <buttonon:click{onclick}> click me </button> ``` Note that this also means you can 'spread' event handlers onto the element along with other props instead of tediously forwarding each event separately: ```svelte <script> let props = $props(); </script> <button{...$$props} on:click on:keydown on:all_the_other_stuff{...props}> click me </button> ``` ### Event modifiers In Svelte 4, you can add event modifiers to handlers: ```svelte <button on:click|once|preventDefault={handler}>...</button> ``` Modifiers are specific to `on:` and as such do not work with modern event handlers. Adding things like `event.preventDefault()` inside the handler itself is preferable, since all the logic lives in one place rather than being split between handler and modifiers. Since event handlers are just functions, you can create your own wrappers as necessary: ```svelte <script> function once(fn) { return function (event) { if (fn) fn.call(this, event); fn = null; }; } function preventDefault(fn) { return function (event) { event.preventDefault(); fn.call(this, event); }; } </script> <button onclick={once(preventDefault(handler))}>...</button> ``` There are three modifiers β `capture`, `passive` and `nonpassive` β that can't be expressed as wrapper functions, since they need to be applied when the event handler is bound rather than when it runs. For `capture`, we add the modifier to the event name: ```svelte <button onclickcapture={...}>...</button> ``` Changing the [`passive`](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#using_passive_listeners) option of an event handler, meanwhile, is not something to be done lightly. If you have a use case for it β and you probably don't! β then you will need to use an action to apply the event handler yourself. ### Multiple event handlers In Svelte 4, this is possible: ```svelte <button on:click={one} on:click={two}>...</button> ``` Duplicate attributes/properties on elements β which now includes event handlers β are not allowed. Instead, do this: ```svelte <button onclick={(e) => { one(e); two(e); }} > ... </button> ``` When spreading props, local event handlers must go _after_ the spread, or they risk being overwritten: ```svelte <button {...props} onclick={(e) => { doStuff(e); props.onclick?.(e); }} > ... </button> ``` > `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 `.detail` 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 consistency. Now that we have event attributes (`onclick`), it's the other way around: Callback props are now the more sensible thing to do. > > The removal of event modifiers is arguably one of the changes that seems like a step back for those who've liked the shorthand syntax of event modifiers. Given that they are not used that frequently, we traded a smaller surface area for more explicitness. Modifiers also were inconsistent, because most of them were only useable on DOM elements. > > Multiple listeners for the same event are also no longer possible, but it was something of an anti-pattern anyway, since it impedes readability: if there are many attributes, it becomes harder to spot that there are two handlers unless they are right next to each other. It also implies that the two handlers are independent, when in fact something like `event.stopImmediatePropagation()` inside `one` would prevent `two` from being called. > > By deprecating `createEventDispatcher` and the `on:` directive in favour of callback props and normal element properties, we: > > - reduce Svelte's learning curve > - remove boilerplate, particularly around `createEventDispatcher` > - remove the overhead of creating `CustomEvent` objects for events that may not even have listeners > - add the ability to spread event handlers > - add the ability to know which event handlers were provided to a component > - add the ability to express whether a given event handler is required or optional > - increase type safety (previously, it was effectively impossible for Svelte to guarantee that a component didn't emit a particular event) ## Snippets instead of slots In Svelte 4, content can be passed to components using slots. Svelte 5 replaces them with snippets which are more powerful and flexible, and as such slots are deprecated in Svelte 5. They continue to work, however, and you can pass snippets to a component that uses slots: ```svelte <!file: Child.svelte> <slot /> <hr /> <slot name="foo" message="hello" /> ``` ```svelte <!file: Parent.svelte> <script> import Child from './Child.svelte'; </script> <Child> default child content {#snippet foo({ message })} message from child: {message} {/snippet} </Child> ``` (The reverse is not true β you cannot pass slotted content to a component that uses [`{@render ...}`](/docs/svelte/@render) tags.) When using custom elements, you should still use `<slot />` like before. In a future version, when Svelte removes its internal version of slots, it will leave those slots as-is, i.e. output a regular DOM tag instead of transforming it. ### Default content In Svelte 4, the easiest way to pass a piece of UI to the child was using a `<slot />`. In Svelte 5, this is done using the `children` prop instead, which is then shown with `{@render children()}`: ```svelte <script> let { children } = $props(); </script> <slot /> {@render children?.()} ``` ### Multiple content placeholders If you wanted multiple UI placeholders, you had to use named slots. In Svelte 5, use props instead, name them however you like and `{@render ...}` them: ```svelte <script> let { header, main, footer } = $props(); </script> <header> <slot name="header" /> {@render header()} </header> <main> <slot name="main" /> {@render main()} </main> <footer> <slot name="footer" /> {@render footer()} </footer> ``` ### Passing data back up In Svelte 4, you would pass data to a `<slot />` and then retrieve it with `let:` in the parent component. In Svelte 5, snippets take on that responsibility: ```svelte <!file: App.svelte> <script> import List from './List.svelte'; </script> <List items={['one', 'two', 'three']}let:item> {#snippet item(text)} <span>{text}</span> {/snippet} <span slot="empty">No items yet</span> {#snippet empty()} <span>No items yet</span> {/snippet} </List> ``` ```svelte <!file: List.svelte> <script> let { items,item, empty} = $props(); </script> {#if items.length} <ul> {#each items as entry} <li> <slot item={entry} /> {@render item(entry)} </li> {/each} </ul> {:else} <slot name="empty" /> {@render empty?.()} {/if} ``` > Slots were easy to get started with, but the more advanced the use case became, the more involved and confusing the syntax became: > > - the `let:` syntax was confusing to many people as it _creates_ a variable whereas all other `:` directives _receive_ a variable > - the scope of a variable declared with `let:` wasn't clear. In the example above, it may look like you can use the `item` slot prop in the `empty` slot, but that's not true > - named slots had to be applied to an element using the `slot` attribute. Sometimes you didn't want to create an element, so we had to add the `<svelte:fragment>` API > - named slots could also be applied to a component, which changed the semantics of where `let:` directives are available (even today us maintainers often don't know which way around it works) > > Snippets solve all of these problems by being much more readable and clear. At the same time they're more powerful as they allow you to define sections of UI that you can render _anywhere_, not just passing them as props to a component. ## Migration script By now you should have a pretty good understanding of the before/after and how the old syntax relates to the new syntax. It probably also became clear that a lot of these migrations are rather technical and repetitive - something you don't want to do by hand. We thought the same, which is why we provide a migration script to do most of the migration automatically. You can upgrade your project by using `npx sv migrate svelte-5`. This will do the following things: - bump core dependencies in your `package.json` - migrate to runes (`let` β `$state` etc) - migrate to event attributes for DOM elements (`on:click` β `onclick`) - migrate slot creations to render tags (`<slot />` β `{@render children()}`) - migrate slot usages to snippets (`<div slot="x">...</div>` β `{#snippet x()}<div>...</div>{/snippet}`) - migrate obvious component creations (`new Component(...)` β `mount(Component, ...)`) You can also migrate a single component in VS Code through the `Migrate Component to Svelte 5 Syntax` command, or in our Playground through the `Migrate` button. Not everything can be migrated automatically, and some migrations need manual cleanup afterwards. The following sections describe these in more detail. ### run You may see that the migration script converts some of your `$:` statements to a `run` function which is imported from `svelte/legacy`. This happens if the migration script couldn't reliably migrate the statement to a `$derived` and concluded this is a side effect instead. In some cases this may be wrong and it's best to change this to use a `$derived` instead. In other cases it may be right, but since `$:` statements also ran on the server but `$effect` does not, it isn't safe to transform it as such. Instead, `run` is used as a stopgap solution. `run` mimics most of the characteristics of `$:`, in that it runs on the server once, and runs as `$effect.pre` on the client (`$effect.pre` runs _before_ changes are applied to the DOM; most likely you want to use `$effect` instead). ```svelte <script> import { run } from 'svelte/legacy'; run(() => { $effect(() => { // some side effect code }) </script> ``` ### Event modifiers Event modifiers are not applicable to event attributes (e.g. you can't do `onclick|preventDefault={...}`). Therefore, when migrating event directives to event attributes, we need a function-replacement for these modifiers. These are imported from `svelte/legacy`, and should be migrated away from in favor of e.g. just using `event.preventDefault()`. ```svelte <script> import { preventDefault } from 'svelte/legacy'; </script> <button onclick={preventDefault((event) => { event.preventDefault(); // ... })} > click me </button> ``` ### Things that are not automigrated The migration script does not convert `createEventDispatcher`. You need to adjust those parts manually. It doesn't do it because it's too risky because it could result in breakage for users of the component, which the migration script cannot find out. The migration script does not convert `beforeUpdate/afterUpdate`. It doesn't do it because it's impossible to determine the actual intent of the code. As a rule of thumb you can often go with a combination of `$effect.pre` (runs at the same time as `beforeUpdate` did) and `tick` (imported from `svelte`, allows you to wait until changes are applied to the DOM and then do some work). ## Components are no longer classes In Svelte 3 and 4, components are classes. In Svelte 5 they are functions and should be instantiated differently. If you need to manually instantiate components, you should use `mount` or `hydrate` (imported from `svelte`) instead. If you see this error using SvelteKit, try updating to the latest version of SvelteKit first, which adds support for Svelte 5. If you're using Svelte without SvelteKit, you'll likely have a `main.js` file (or similar) which you need to adjust: ```js import { mount } from 'svelte'; import App from './App.svelte' const app = new App({ target: document.getElementById("app") }); const app = mount(App, { target: document.getElementById("app") }); export default app; ``` `mount` and `hydrate` have the exact same API. The difference is that `hydrate` will pick up the Svelte's server-rendered HTML inside its target and hydrate it. Both return an object with the exports of the component and potentially property accessors (if compiled with `accessors: true`). They do not come with the `$on`, `$set` and `$destroy` methods you may know from the class component API. These are its replacements: For `$on`, instead of listening to events, pass them via the `events` property on the options argument. ```js import { mount } from 'svelte'; import App from './App.svelte' const app = new App({ target: document.getElementById("app") }); app.$on('event', callback); const app = mount(App, { target: document.getElementById("app"), events: { event: callback } }); ``` For `$set`, use `$state` instead to create a reactive property object and manipulate it. If you're doing this inside a `.js` or `.ts` file, adjust the ending to include `.svelte`, i.e. `.svelte.js` or `.svelte.ts`. ```js import { mount } from 'svelte'; import App from './App.svelte' const app = new App({ target: document.getElementById("app"), props: { foo: 'bar' } }); app.$set({ foo: 'baz' }); const props = $state({ foo: 'bar' }); const app = mount(App, { target: document.getElementById("app"), props }); props.foo = 'baz'; ``` For `$destroy`, use `unmount` instead. ```js import { mount, unmount } from 'svelte'; import App from './App.svelte' const app = new App({ target: document.getElementById("app"), props: { foo: 'bar' } }); app.$destroy(); const app = mount(App, { target: document.getElementById("app") }); unmount(app); ``` As a stop-gap-solution, you can also use `createClassComponent` or `asClassComponent` (imported from `svelte/legacy`) instead to keep the same API known from Svelte 4 after instantiating. ```js import { createClassComponent } from 'svelte/legacy'; import App from './App.svelte' const app = new App({ target: document.getElementById("app") }); const app = createClassComponent({ component: App, target: document.getElementById("app") }); export default app; ``` If this component is not under your control, you can use the `compatibility.componentApi` compiler option for auto-applied backwards compatibility, which means code using `new Component(...)` keeps working without adjustments (note that this adds a bit of overhead to each component). This will also add `$set` and `$on` methods for all component instances you get through `bind:this`. ```js /// svelte.config.js export default { compilerOptions: { compatibility: { componentApi: 4 } } }; ``` Note that `mount` and `hydrate` are _not_ synchronous, so things like `onMount` won't have been called by the time the function returns and the pending block of promises will not have been rendered yet (because `#await` waits a microtask to wait for a potentially immediately-resolved promise). If you need that guarantee, call `flushSync` (import from `'svelte'`) after calling `mount/hydrate`. ### Server API changes Similarly, components no longer have a `render` method when compiled for server side rendering. Instead, pass the function to `render` from `svelte/server`: ```js import { render } from 'svelte/server'; import App from './App.svelte'; const { html, head } = App.render({ props: { message: 'hello' }}); const { html, head } = render(App, { props: { message: 'hello' }}); ``` In Svelte 4, rendering a component to a string also returned the CSS of all components. In Svelte 5, this is no longer the case by default because most of the time you're using a tooling chain that takes care of it in other ways (like SvelteKit). If you need CSS to be returned from `render`, you can set the `css` compiler option to `'injected'` and it will add `<style>` elements to the `head`. ### Component typing changes The change from classes towards functions is also reflected in the typings: `SvelteComponent`, the base class from Svelte 4, is deprecated in favour of the new `Component` type which defines the function shape of a Svelte component. To manually define a component shape in a `d.ts` file: ```ts import type { Component } from 'svelte'; export declare const MyComponent: Component<{ foo: string; }>; ``` To declare that a component of a certain type is required: ```js import { ComponentA, ComponentB } from 'component-library'; import type { SvelteComponent } from 'svelte'; import type { Component } from 'svelte'; let C: typeof SvelteComponent<{ foo: string }> = $state( let C: Component<{ foo: string }> = $state( Math.random() ? ComponentA : ComponentB ); ``` The two utility types `ComponentEvents` and `ComponentType` are also deprecated. `ComponentEvents` is obsolete because events are defined as callback props now, and `ComponentType` is obsolete because the new `Component` type is the component type already (i.e. `ComponentType<SvelteComponent<{ prop: string }>>` is equivalent to `Component<{ prop: string }>`). ### bind:this changes Because components are no longer classes, using `bind:this` no longer returns a class instance with `$set`, `$on` and `$destroy` methods on it. It only returns the instance exports (`export function/const`) and, if you're using the `accessors` option, a getter/setter-pair for each property. ## `<svelte:component>` is no longer necessary In Svelte 4, components are _static_ β if you render `<Thing>`, and the value of `Thing` changes, [nothing happens](/REMOVED). To make it dynamic you had to use `<svelte:component>`. This is no longer true in Svelte 5: ```svelte <script> import A from './A.svelte'; import B from './B.svelte'; let Thing = $state(); </script> <select bind:value={Thing}> <option value={A}>A</option> <option value={B}>B</option> </select> <Thing /> <svelte:component this={Thing} /> ``` While migrating, keep in mind that your component's name should be capitalized (`Thing`) to distinguish it from elements, unless using dot notation. ### Dot notation indicates a component In Svelte 4, `<foo.bar>` would create an element with a tag name of `"foo.bar"`. In Svelte 5, `foo.bar` is treated as a component instead. This is particularly useful inside `each` blocks: ```svelte {#each items as item} <item.component {...item.props} /> {/each} ``` ## Whitespace handling changed Previously, Svelte employed a very complicated algorithm to determine if whitespace should be kept or not. Svelte 5 simplifies this which makes it easier to reason about as a developer. The rules are: - Whitespace between nodes is collapsed to one whitespace - Whitespace at the start and end of a tag is removed completely - Certain exceptions apply such as keeping whitespace inside `pre` tags As before, you can disable whitespace trimming by setting the `preserveWhitespace` option in your compiler settings or on a per-component basis in `<svelte:options>`. ## Modern browser required Svelte 5 requires a modern browser (in other words, not Internet Explorer) for various reasons: - it uses [`Proxies`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy) - elements with `clientWidth`/`clientHeight`/`offsetWidth`/`offsetHeight` bindings use a [`ResizeObserver`](https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserver) rather than a convoluted `<iframe>` hack - `<input type="range" bind:value={...} />` only uses an `input` event listener, rather than also listening for `change` events as a fallback The `legacy` compiler option, which generated bulkier but IE-friendly code, no longer exists. ## Changes to compiler options - The `false`/`true` (already deprecated previously) and the `"none"` values were removed as valid values from the `css` option - The `legacy` option was repurposed - The `hydratable` option has been removed. Svelte components are always hydratable now - The `enableSourcemap` option has been removed. Source maps are always generated now, tooling can choose to ignore it - The `tag` option was removed. Use `<svelte:options customElement="tag-name" />` inside the component instead - The `loopGuardTimeout`, `format`, `sveltePath`, `errorMode` and `varsReport` options were removed ## The `children` prop is reserved Content inside component tags becomes a snippet prop called `children`. You cannot have a separate prop by that name. ## Breaking changes in runes mode Some breaking changes only apply once your component is in runes mode. ### Bindings to component exports are not allowed Exports from runes mode components cannot be bound to directly. For example, having `export const foo = ...` in component `A` and then doing `<A bind:foo />` causes an error. Use `bind:this` instead β `<A bind:this={a} />` β and access the export as `a.foo`. This change makes things easier to reason about, as it enforces a clear separation between props and exports. ### Bindings need to be explicitly defined using `$bindable()` In Svelte 4 syntax, every property (declared via `export let`) is bindable, meaning you can `bind:` to it. In runes mode, properties are not bindable by default: you need to denote bindable props with the `$bindable` rune. If a bindable property has a default value (e.g. `let { foo = $bindable('bar') } = $props();`), you need to pass a non-`undefined` value to that property if you're binding to it. This prevents ambiguous behavior β the parent and child must have the same value β and results in better performance (in Svelte 4, the default value was reflected back to the parent, resulting in wasteful additional render cycles). ### `accessors` option is ignored Setting the `accessors` option to `true` makes properties of a component directly accessible on the component instance. ```svelte <svelte:options accessors={true} /> <script> // available via componentInstance.name export let name; </script> ``` In runes mode, properties are never accessible on the component instance. You can use component exports instead if you need to expose them. ```svelte <script> let { name } = $props(); // available via componentInstance.getName() export const getName = () => name; </script> ``` Alternatively, if the place where they are instantiated is under your control, you can also make use of runes inside `.js/.ts` files by adjusting their ending to include `.svelte`, i.e. `.svelte.js` or `.svelte.ts`, and then use `$state`: ```js import { mount } from 'svelte'; import App from './App.svelte' const app = new App({ target: document.getElementById("app"), props: { foo: 'bar' } }); app.foo = 'baz' const props = $state({ foo: 'bar' }); const app = mount(App, { target: document.getElementById("app"), props }); props.foo = 'baz'; ``` ### `immutable` option is ignored Setting the `immutable` option has no effect in runes mode. This concept is replaced by how `$state` and its variations work. ### Classes are no longer "auto-reactive" In Svelte 4, doing the following triggered reactivity: ```svelte <script> let foo = new Foo(); </script> <button on:click={() => (foo.value = 1)}>{foo.value}</button > ``` This is because the Svelte compiler treated the assignment to `foo.value` as an instruction to update anything that referenced `foo`. In Svelte 5, reactivity is determined at runtime rather than compile time, so you should define `value` as a reactive `$state` field on the `Foo` class. Wrapping `new Foo()` with `$state(...)` will have no effect β only vanilla objects and arrays are made deeply reactive. ### Touch and wheel events are passive When using `onwheel`, `onmousewheel`, `ontouchstart` and `ontouchmove` event attributes, the handlers are [passive](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#using_passive_listeners) to align with browser defaults. This greatly improves responsiveness by allowing the browser to scroll the document immediately, rather than waiting to see if the event handler calls `event.preventDefault()`. In the very rare cases that you need to prevent these event defaults, you should use [`on`](/docs/svelte/svelte-events#on) instead (for example inside an action). ### Attribute/prop syntax is stricter In Svelte 4, complex attribute values needn't be quoted: ```svelte <Component prop=this{is}valid /> ``` This is a footgun. In runes mode, if you want to concatenate stuff you must wrap the value in quotes: ```svelte <Component prop="this{is}valid" /> ``` Note that Svelte 5 will also warn if you have a single expression wrapped in quotes, like `answer="{42}"` β in Svelte 6, that will cause the value to be converted to a string, rather than passed as a number. ### HTML structure is stricter In Svelte 4, you were allowed to write HTML code that would be repaired by the browser when server side rendering it. For example you could write this... ```svelte <table> <tr> <td>hi</td> </tr> </table> ``` ... and the browser would auto-insert a `<tbody>` element: ```svelte <table> <tbody> <tr> <td>hi</td> </tr> </tbody> </table> ``` Svelte 5 is more strict about the HTML structure and will throw a compiler error in cases where the browser would repair the DOM. ## Other breaking changes ### Stricter `@const` assignment validation Assignments to destructured parts of a `@const` declaration are no longer allowed. It was an oversight that this was ever allowed. ### :is(...) and :where(...) are scoped Previously, Svelte did not analyse selectors inside `:is(...)` and `:where(...)`, effectively treating them as global. Svelte 5 analyses them in the context of the current component. As such, some selectors may now be treated as unused if they were relying on this treatment. To fix this, use `:global(...)` inside the `:is(...)/:where(...)` selectors. When using Tailwind's `@apply` directive, add a `:global` selector to preserve rules that use Tailwind-generated `:is(...)` selectors: ```css main:global{ @apply bg-blue-100 dark:bg-blue-900; } ``` ### CSS hash position no longer deterministic Previously Svelte would always insert the CSS hash last. This is no longer guaranteed in Svelte 5. This is only breaking if you [have very weird css selectors](https://stackoverflow.com/questions/15670631/does-the-order-of-classes-listed-on-an-item-affect-the-css). ### Scoped CSS uses :where(...) To avoid issues caused by unpredictable specificity changes, scoped CSS selectors now use `:where(.svelte-xyz123)` selector modifiers alongside `.svelte-xyz123` (where `xyz123` is, as previously, a hash of the `<style>` contents). You can read more detail [here](https://github.com/sveltejs/svelte/pull/10443). In the event that you need to support ancient browsers that don't implement `:where`, you can manually alter the emitted CSS, at the cost of unpredictable specificity changes: ```js // @errors: 2552 css = css.replace(/:where\((.+?)\)/, '$1'); ``` ### Error/warning codes have been renamed Error and warning codes have been renamed. Previously they used dashes to separate the words, they now use underscores (e.g. foo-bar becomes foo_bar). Additionally, a handful of codes have been reworded slightly. ### Reduced number of namespaces The number of valid namespaces you can pass to the compiler option `namespace` has been reduced to `html` (the default), `mathml` and `svg`. The `foreign` namespace was only useful for Svelte Native, which we're planning to support differently in a 5.x minor. ### beforeUpdate/afterUpdate changes `beforeUpdate` no longer runs twice on initial render if it modifies a variable referenced in the template. `afterUpdate` callbacks in a parent component will now run after `afterUpdate` callbacks in any child components. `beforeUpdate/afterUpdate` no longer run when the component contains a `<slot>` and its content is updated. Both functions are disallowed in runes mode β use `$effect.pre(...)` and `$effect(...)` instead. ### `contenteditable` behavior change If you have a `contenteditable` node with a corresponding binding _and_ a reactive value inside it (example: `<div contenteditable=true bind:textContent>count is {count}</div>`), then the value inside the contenteditable will not be updated by updates to `count` because the binding takes full control over the content immediately and it should only be updated through it. ### `oneventname` attributes no longer accept string values In Svelte 4, it was possible to specify event attributes on HTML elements as a string: ```svelte <button onclick="alert('hello')">...</button> ``` This is not recommended, and is no longer possible in Svelte 5, where properties like `onclick` replace `on:click` as the mechanism for adding event handlers. ### `null` and `undefined` become the empty string In Svelte 4, `null` and `undefined` were printed as the corresponding string. In 99 out of 100 cases you want this to become the empty string instead, which is also what most other frameworks out there do. Therefore, in Svelte 5, `null` and `undefined` become the empty string. ### `bind:files` values can only be `null`, `undefined` or `FileList` `bind:files` is now a two-way binding. As such, when setting a value, it needs to be either falsy (`null` or `undefined`) or of type `FileList`. ### Bindings now react to form resets Previously, bindings did not take into account `reset` event of forms, and therefore values could get out of sync with the DOM. Svelte 5 fixes this by placing a `reset` listener on the document and invoking bindings where necessary. ### `walk` no longer exported `svelte/compiler` reexported `walk` from `estree-walker` for convenience. This is no longer true in Svelte 5, import it directly from that package instead in case you need it. ### Content inside `svelte:options` is forbidden In Svelte 4 you could have content inside a `<svelte:options />` tag. It was ignored, but you could write something in there. In Svelte 5, content inside that tag is a compiler error. ### `<slot>` elements in declarative shadow roots are preserved Svelte 4 replaced the `<slot />` tag in all places with its own version of slots. Svelte 5 preserves them in the case they are a child of a `<template shadowrootmode="...">` element. ### `<svelte:element>` tag must be an expression In Svelte 4, `<svelte:element this="div">` is valid code. This makes little sense β you should just do `<div>`. In the vanishingly rare case that you _do_ need to use a literal value for some reason, you can do this: ```svelte <svelte:element this={"div"}> ``` Note that whereas Svelte 4 would treat `<svelte:element this="input">` (for example) identically to `<input>` for the purposes of determining which `bind:` directives could be applied, Svelte 5 does not. ### `mount` plays transitions by default The `mount` function used to render a component tree plays transitions by default unless the `intro` option is set to `false`. This is different from legacy class components which, when manually instantiated, didn't play transitions by default. ### `<img src={...}>` and `{@html ...}` hydration mismatches are not repaired In Svelte 4, if the value of a `src` attribute or `{@html ...}` tag differ between server and client (a.k.a. a hydration mismatch), the mismatch is repaired. This is very costly: setting a `src` attribute (even if it evaluates to the same thing) causes images and iframes to be reloaded, and reinserting a large blob of HTML is slow. Since these mismatches are extremely rare, Svelte 5 assumes that the values are unchanged, but in development will warn you if they are not. To force an update you can do something like this: ```svelte <script> let { markup, src } = $props(); if (typeof window !== 'undefined') { // stash the values... const initial = { markup, src }; // unset them... markup = src = undefined; $effect(() => { // ...and reset after we've mounted markup = initial.markup; src = initial.src; }); } </script> {@html markup} <img {src} /> ``` ### Hydration works differently Svelte 5 makes use of comments during server side rendering which are used for more robust and efficient hydration on the client. As such, you shouldn't remove comments from your HTML output if you intend to hydrate it, and if you manually authored HTML to be hydrated by a Svelte component, you need to adjust that HTML to include said comments at the correct positions. ### `onevent` attributes are delegated Event attributes replace event directives: Instead of `on:click={handler}` you write `onclick={handler}`. For backwards compatibility the `on:event` syntax is still supported and behaves the same as in Svelte 4. Some of the `onevent` attributes however are delegated, which means you need to take care to not stop event propagation on those manually, as they then might never reach the listener for this event type at the root. ### `--style-props` uses a different element Svelte 5 uses an extra `<svelte-css-wrapper>` element instead of a `<div>` to wrap the component when using CSS custom properties. | |
## docs/kit/60-appendix/30-migrating-to-sveltekit-2.md | |
--- title: Migrating to SvelteKit v2 --- Upgrading from SvelteKit version 1 to version 2 should be mostly seamless. There are a few breaking changes to note, which are listed here. You can use `npx sv migrate sveltekit-2` to migrate some of these changes automatically. We highly recommend upgrading to the most recent 1.x version before upgrading to 2.0, so that you can take advantage of targeted deprecation warnings. We also recommend [updating to Svelte 4](../svelte/v4-migration-guide) first: Later versions of SvelteKit 1.x support it, and SvelteKit 2.0 requires it. ## `redirect` and `error` are no longer thrown by you Previously, you had to `throw` the values returned from `error(...)` and `redirect(...)` yourself. In SvelteKit 2 this is no longer the case β calling the functions is sufficient. ```js import { error } from '@sveltejs/kit' // ... throw error(500, 'something went wrong'); error(500, 'something went wrong'); ``` `svelte-migrate` will do these changes automatically for you. If the error or redirect is thrown inside a `try {...}` block (hint: don't do this!), you can distinguish them from unexpected errors using [`isHttpError`](@sveltejs-kit#isHttpError) and [`isRedirect`](@sveltejs-kit#isRedirect) imported from `@sveltejs/kit`. ## path is required when setting cookies When receiving a `Set-Cookie` header that doesn't specify a `path`, browsers will [set the cookie path](https://www.rfc-editor.org/rfc/rfc6265#section-5.1.4) to the parent of the resource in question. This behaviour isn't particularly helpful or intuitive, and frequently results in bugs because the developer expected the cookie to apply to the domain as a whole. As of SvelteKit 2.0, you need to set a `path` when calling `cookies.set(...)`, `cookies.delete(...)` or `cookies.serialize(...)` so that there's no ambiguity. Most of the time, you probably want to use `path: '/'`, but you can set it to whatever you like, including relative paths β `''` means 'the current path', `'.'` means 'the current directory'. ```js /** @type {import('./$types').PageServerLoad} */ export function load({ cookies }) { cookies.set(name, value,{ path: '/' }); return { response } } ``` `svelte-migrate` will add comments highlighting the locations that need to be adjusted. ## Top-level promises are no longer awaited In SvelteKit version 1, if the top-level properties of the object returned from a `load` function were promises, they were automatically awaited. With the introduction of [streaming](/blog/streaming-snapshots-sveltekit) this behavior became a bit awkward as it forces you to nest your streamed data one level deep. As of version 2, SvelteKit no longer differentiates between top-level and non-top-level promises. To get back the blocking behavior, use `await` (with `Promise.all` to prevent waterfalls, where appropriate): ```js // @filename: ambient.d.ts declare const url: string; // @filename: index.js //cut // If you have a single promise /** @type {import('./$types').PageServerLoad} */ exportasyncfunction load({ fetch }) { const response =awaitfetch(url).then(r => r.json()); return { response } } ``` ```js // @filename: ambient.d.ts declare const url1: string; declare const url2: string; // @filename: index.js //cut // If you have multiple promises /** @type {import('./$types').PageServerLoad} */ exportasyncfunction load({ fetch }) { const a = fetch(url1).then(r => r.json()); const b = fetch(url2).then(r => r.json()); const [a, b] = await Promise.all([ fetch(url1).then(r => r.json()), fetch(url2).then(r => r.json()), ]); return { a, b }; } ``` ## goto(...) changes `goto(...)` no longer accepts external URLs. To navigate to an external URL, use `window.location.href = url`. The `state` object now determines `$page.state` and must adhere to the `App.PageState` interface, if declared. See [shallow routing](shallow-routing) for more details. ## paths are now relative by default In SvelteKit 1, `%sveltekit.assets%` in your `app.html` was replaced with a relative path by default (i.e. `.` or `..` or `../..` etc, depending on the path being rendered) during server-side rendering unless the [`paths.relative`](configuration#paths) config option was explicitly set to `false`. The same was true for `base` and `assets` imported from `$app/paths`, but only if the `paths.relative` option was explicitly set to `true`. This inconsistency is fixed in version 2. Paths are either always relative or always absolute, depending on the value of [`paths.relative`](configuration#paths). It defaults to `true` as this results in more portable apps: if the `base` is something other than the app expected (as is the case when viewed on the [Internet Archive](https://archive.org/), for example) or unknown at build time (as is the case when deploying to [IPFS](https://ipfs.tech/) and so on), fewer things are likely to break. ## Server fetches are not trackable anymore Previously it was possible to track URLs from `fetch`es on the server in order to rerun load functions. This poses a possible security risk (private URLs leaking), and as such it was behind the `dangerZone.trackServerFetches` setting, which is now removed. ## `preloadCode` arguments must be prefixed with `base` SvelteKit exposes two functions, [`preloadCode`]($app-navigation#preloadCode) and [`preloadData`]($app-navigation#preloadData), for programmatically loading the code and data associated with a particular path. In version 1, there was a subtle inconsistency β the path passed to `preloadCode` did not need to be prefixed with the `base` path (if set), while the path passed to `preloadData` did. This is fixed in SvelteKit 2 β in both cases, the path should be prefixed with `base` if it is set. Additionally, `preloadCode` now takes a single argument rather than _n_ arguments. ## `resolvePath` has been removed SvelteKit 1 included a function called `resolvePath` which allows you to resolve a route ID (like `/blog/[slug]`) and a set of parameters (like `{ slug: 'hello' }`) to a pathname. Unfortunately the return value didn't include the `base` path, limiting its usefulness in cases where `base` was set. As such, SvelteKit 2 replaces `resolvePath` with a (slightly better named) function called `resolveRoute`, which is imported from `$app/paths` and which takes `base` into account. ```js import { resolvePath } from '@sveltejs/kit'; import { base } from '$app/paths'; import { resolveRoute } from '$app/paths'; const path = base + resolvePath('/blog/[slug]', { slug }); const path = resolveRoute('/blog/[slug]', { slug }); ``` `svelte-migrate` will do the method replacement for you, though if you later prepend the result with `base`, you need to remove that yourself. ## Improved error handling Errors are handled inconsistently in SvelteKit 1. Some errors trigger the `handleError` hook but there is no good way to discern their status (for example, the only way to tell a 404 from a 500 is by seeing if `event.route.id` is `null`), while others (such as 405 errors for `POST` requests to pages without actions) don't trigger `handleError` at all, but should. In the latter case, the resulting `$page.error` will deviate from the [`App.Error`](types#Error) type, if it is specified. SvelteKit 2 cleans this up by calling `handleError` hooks with two new properties: `status` and `message`. For errors thrown from your code (or library code called by your code) the status will be `500` and the message will be `Internal Error`. While `error.message` may contain sensitive information that should not be exposed to users, `message` is safe. ## Dynamic environment variables cannot be used during prerendering The `$env/dynamic/public` and `$env/dynamic/private` modules provide access to _run time_ environment variables, as opposed to the _build time_ environment variables exposed by `$env/static/public` and `$env/static/private`. During prerendering in SvelteKit 1, they are one and the same. As such, prerendered pages that make use of 'dynamic' environment variables are really 'baking in' build time values, which is incorrect. Worse, `$env/dynamic/public` is populated in the browser with these stale values if the user happens to land on a prerendered page before navigating to dynamically-rendered pages. Because of this, dynamic environment variables can no longer be read during prerendering in SvelteKit 2 β you should use the `static` modules instead. If the user lands on a prerendered page, SvelteKit will request up-to-date values for `$env/dynamic/public` from the server (by default from a module called `/_app/env.js`) instead of reading them from the server-rendered HTML. ## `form` and `data` have been removed from `use:enhance` callbacks If you provide a callback to [`use:enhance`](form-actions#Progressive-enhancement-use:enhance), it will be called with an object containing various useful properties. In SvelteKit 1, those properties included `form` and `data`. These were deprecated some time ago in favour of `formElement` and `formData`, and have been removed altogether in SvelteKit 2. ## Forms containing file inputs must use `multipart/form-data` If a form contains an `<input type="file">` but does not have an `enctype="multipart/form-data"` attribute, non-JS submissions will omit the file. SvelteKit 2 will throw an error if it encounters a form like this during a `use:enhance` submission to ensure that your forms work correctly when JavaScript is not present. ## Generated `tsconfig.json` is more strict Previously, the generated `tsconfig.json` was trying its best to still produce a somewhat valid config when your `tsconfig.json` included `paths` or `baseUrl`. In SvelteKit 2, the validation is more strict and will warn when you use either `paths` or `baseUrl` in your `tsconfig.json`. These settings are used to generate path aliases and you should use [the `alias` config](configuration#alias) option in your `svelte.config.js` instead, to also create a corresponding alias for the bundler. ## `getRequest` no longer throws errors The `@sveltejs/kit/node` module exports helper functions for use in Node environments, including `getRequest` which turns a Node [`ClientRequest`](https://nodejs.org/api/http.html#class-httpclientrequest) into a standard [`Request`](https://developer.mozilla.org/en-US/docs/Web/API/Request) object. In SvelteKit 1, `getRequest` could throw if the `Content-Length` header exceeded the specified size limit. In SvelteKit 2, the error will not be thrown until later, when the request body (if any) is being read. This enables better diagnostics and simpler code. ## `vitePreprocess` is no longer exported from `@sveltejs/kit/vite` Since `@sveltejs/vite-plugin-svelte` is now a peer dependency, SvelteKit 2 no longer re-exports `vitePreprocess`. You should import it directly from `@sveltejs/vite-plugin-svelte`. ## Updated dependency requirements SvelteKit 2 requires Node `18.13` or higher, and the following minimum dependency versions: - `svelte@4` - `vite@5` - `typescript@5` - `@sveltejs/vite-plugin-svelte@3` (this is now required as a `peerDependency` of SvelteKit β previously it was directly depended upon) - `@sveltejs/adapter-cloudflare@3` (if you're using these adapters) - `@sveltejs/adapter-cloudflare-workers@2` - `@sveltejs/adapter-netlify@3` - `@sveltejs/adapter-node@2` - `@sveltejs/adapter-static@3` - `@sveltejs/adapter-vercel@4` `svelte-migrate` will update your `package.json` for you. As part of the TypeScript upgrade, the generated `tsconfig.json` (the one your `tsconfig.json` extends from) now uses `"moduleResolution": "bundler"` (which is recommended by the TypeScript team, as it properly resolves types from packages with an `exports` map in package.json) and `verbatimModuleSyntax` (which replaces the existing `importsNotUsedAsValues ` and `preserveValueImports` flags β if you have those in your `tsconfig.json`, remove them. `svelte-migrate` will do this for you). ## SvelteKit 2.12: $app/stores deprecated SvelteKit 2.12 introduced `$app/state` based on the [Svelte 5 runes API](/docs/svelte/what-are-runes). `$app/state` provides everything that `$app/stores` provides but with more flexibility as to where and how you use it. Most importantly, the `page` object is now fine-grained, e.g. updates to `page.state` will not invalidate `page.data` and vice-versa. As a consequence, `$app/stores` is deprecated and subject to be removed in SvelteKit 3. We recommend [upgrading to Svelte 5](/docs/svelte/v5-migration-guide), if you haven't already, and then migrate away from `$app/stores`. Most of the replacements should be pretty simple: Replace the `$app/stores` import with `$app/state` and remove the `$` prefixes from the usage sites. ```svelte <script> import { page } from '$app/stores'; import { page } from '$app/state'; </script> {$page.data} {page.data} ``` Use `npx sv migrate app-state` to auto-migrate most of your `$app/stores` usages inside `.svelte` components. | |
Instructions for LLMs: <SYSTEM>Always use Svelte 5 runes and Svelte 5 syntax. Runes do not need to be imported, they are globals. $state() runes are always declared using `let`, never with `const`. When passing a function to $derived, you must always use $derived.by(() => ...). Error boundaries can only catch errors during component rendering and at the top level of an $effect inside the error boundary. Error boundaries do not catch errors in onclick or other event handlers.</SYSTEM> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment