Upgrading to Svelte 5

A month ago I was fixing a hairy section of reactive code in our Svelte application, and the thought crossed my mind to refactor the code in tandem with an upgrade to Svelte 5. If I was going to be cleaning up a tangled mess of store dependencies, why not do so with Svelte's new reactivity paradigm? We'll eventually need to upgrade to Svelte 5 anyway, why not do it now as we refactor a substantial part of our reactive code?

Slack Message referencing Svelte 5 Upgrade

With a high probability of trading one set of problems for another, and 25 years of engineering experience reminding me it's a terrible idea to be a forerunner on preview software, I decided to yolo a svelte@next install and see what this new version was all about. What could possibly go wrong? 😉

Svelte 5 Initial Installation

Svelte 5 is intended to be backwards compatible with Svelte 4 code. Theoretically, upgrading to Svelte 5 in a Svelte 4 project should not introduce any breaking changes. Svelte 4 code will be compiled into equivalent code the Svelte 5 runtime understands and the application should continue to work. That's the promise at least. So I fired up my dev server and put this to the test on our large code base...

Surprisingly, the application loaded immediately and appeared largely intact. Positive sign. As I poked around our application, a few issues started to emerge:

  • Numerous SVG based icons failed to render, even though the <svg> element was placed in the DOM appropriately. Turns out this was an SVG namespace bug, and adding an <svelte:options namespace='svg'> tag to the 3rd party Icon component we use resolved it.
  • Upon navigating to some pages, the first attempt would trigger an Uncaught TypeError: Cannot read properties of undefined (reading 'call') error in the console. However, subsequent navigations would complete without issue.
  • Occasionally I would see The Svelte $effect rune can only be used during component initialisation Even after my British co-founder translated "initialisation" for me, I wasn't able to determine why this was happening at first glance. I can only assume of our existing Svelte 4 code was being compiled into a Svelte 5 version that was attempting using $effect incorrectly.

Overall though, the application worked, despite throwing the console errors mentioned above. It was now time to dive in and start playing around with Svelte 5's new reactivity mode in a real world setting.

Svelte 5 Runes Reactivity?

Yes, they rune'd everything, but they didn't ruin everything as some people on the internet would have you believe. Conceptually, Svelte 5's approach to reactivity is a breath of fresh air. Having universally reactive code powered by signals eliminates numerous inconveniences we would often dance around / struggle with in Svelte 4:

  • $: ordering issues (not a big deal... until you have bug that results from this)
  • $: reactivity dependency tracking limitations (how many times have you passed parameters to functions in $: statements just to enable reactivity?)
  • Having to create event dispatchers to trigger (non-stored) state changes further up the DOM (Yes, you can bind state downward to create bidirectional reactivity, but this doesn't solve more complex use cases and can lead to tight coupling of data and UX)
  • Stores.

The last one - stores - are actually my main annoyance with Svelte 4. Stores are great for simple use cases, but create a mess rather quickly when you want to do anything mildly complex. Most of the store data we deal with is retrieved asynchronously from an API, our stores often have multiple inter-store dependencies, and we have many stores of stores (with arbitrary and dynamically changing length).

If you've worked on any sizeable Svelte project, you've probably encountered challenges with stores. Some well known companies have even built libraries to help deal with the challenges. Usually what you're left with is a tangled web of complex store logic, self-managing store subscriptions, and a lot of code to deal with updates between stores. (If you're thinking, "isn't that what derived solves?", you're correct, but it breaks down with complexity. For example, things like this don't work despite the author claiming they do, and I'll leave it to the reader to uncover why and feel the frustration.)

I've generally enjoyed Svelte 4 and find it far superior to other front-end frameworks out there. But the bulk of the complexity in our code base always comes back to stores, and I'm excited to be done with them. Perhaps too excited, which is how I find myself upgrading our application to a preview release of Svelte 5.

Upgrading Svelte 4 Code to Svelte 5

After installing Svelte 5, we began the process of incrementally upgrading our application code, component by component, store by store. Being able to incrementally upgrade a single file at a time was great, and I give the Svelte team a lot of credit for allowing Svelte 4 & 5 code to coexist together.

Upgrading Components

Upgrading components was incredibly straightforward, albeit a little repetitive (I'm sure the official release will include migration tools to reduce the pain). The two biggest wins from the component upgrades were:

  • Using Props to Spread Event Handlers

    We have an internal UI component library, and we often find ourselves bubbling events up our component hierarchy so consumers of the UI library can leverage event handlers. Now that we can pass event handlers as regular props and easily destructure rest props, we no longer need to do this:

    And instead can do...

    It seems minor, but it's a huge win across a large component library. And I'm sure it has performance improvements.

  • Snippets

    Slots always felt a little off, and there were a few issues, such as this one, that we always struggled with. Snippets resolve many of these issues and feel more intuitive to use.

Upgrading Stores

We replaced all of our stores with Javascript classes with runes. This has been a massive win. We started writing code in the same manner we would outside of a front-end framework, only now reactivity is included if you provide a few rune initializers. The foundational business logic of our application became much cleaner and better organized. Reactivity became easier to comprehend. New functionality has been easier to introduce. Unit testing, especially of reactivity, has gotten easier. There have been wins across the board from ditching stores and adopting runes.

Things to Look Out For

Nested Reactivity Improvements

If you haven't taken a look at the nested reactivity improvements that were added in early December, I suggestion you go check it out now. It's the most significant update to Svelte 5 since the initial announcement blog post. The Svelte team has added a mechanism to create nested reactivity on POJO's by cleverly wrapping them in a Javascript Proxy.

Development Tooling Mostly Works

For the most part, Svelte 5 plays nice with our development tooling. Svelte is mostly JavaScript with a few language specific tags, many of which haven't changed between 4 and 5, so this isn't surprising. Since runes are just functions, they're more tooling friendly than $: (even though this is valid JS syntax).

We use eslint and Svelte 5 code lints without errors. It also plays nice with prettier. Autocomplete and syntax highlighting work just as the did in Svelte 4.

That said, we have not yet been able to get full IDE support for a couple new Svelte 5 tags, namely @render and #snippet. So be prepared for some red squiggles. (If anyone has found a solution to this, feel free to reach out. Highly likely we missed something, we haven't bothered to spend time on it)

HMR Doesn't Work in Svelte 5 (yet)

One of the first things you'll notice when working with Svelte 5 is that they don't have HMR working as of yet. There's an draft pull request to add support for it, authored by none other than @rixo, the author of svelte-hmr, which powers HMR in Svelte 4. This work was started in early January ‘24 and I will imagine it will be polished and released soon. If you're itching for that Hot MR action now, you're gonna have to wait.

While HMR doesn't work, if you're running SvelteKit (we're running SvelteKit 2), the page does refresh any time you edit a file. You lose your state, and the update does take a little longer, but your changes are automatically reflected in the browser.

Svelte Inspector Not Operational in Svelte 5 (yet)

Similarly, the Svelte Inspector is not yet operational in Svelte 5. If this is something you rely on heavily, you may want to hold off on upgrading. I am excited to see where a future Svelte 5 inspector when it does launch. I can only imagine the new signals based reactivity model provides all sorts of opportunities to greatly improve the inspector experience and provide insights into the signal dependency graph. There have been numerous times I've been debugging our application and desperately wishing I could track the signal graph.

Be Careful with Children

In Svelte 4, you could put a <slot> tag in your component and not make use of it. If the component was used without content for a slot, everything worked, and the slot didn't render.

In Svelte 5, if you put a {@render children()} in your component but don't provide child content, an error will occur. If you're building a component that may or may not have children, you need a conditional check for the children since it's now a snippet function - {#if children}{@render children()}{/if}. Slightly less ergonomic than Svelte 4, but a small price to pay for the large improvement snippets provide over slots. (Note: this is true of any snippet, but children is the most common)

Fun with $derived

The $derived rune is designed to work with JavaScript expressions, such as

If you start working with more complex derived values, you may find yourself needing more complex expressions, potentially expressions that span multiple lines. Multi line expressions don't play nice with $derived`s expectation of simple JavaScript expressions. You have a few options to tackle this.

You could wrap these expressions in a function....

Or, if that's not your bag, you could use an IIFE (Immediately Invoked Function Expression)...

Or, as of Jan 29th, you can make use of the new $derived.call sub-rune. (Sounds like this may be renamed to $derived.by)

All work equally well and will update when referenced $state variables change.

Issues We Encountered Upgrading to Svelte 5

We wanted to share all the issues we encountered upgrading to Svelte 5, as well as the solutions we implemented, in case others find it helpful. Hopefully this saves someone out there a few minutes of time.

Strict HTML Enforcement

We observed various situations where the Svelte 5 compiler and runtime provide stricter enforcement of HTML standards than Svelte 4. Invalid HTML you could get away with in Svelte 4 often issues an error in Svelte 5. For example, we encountered the following error when attempting to put a <div> inside a <p> tag.

ERR_SVELTE_HYDRATION_MISMATCH Hydration failed because the initial UI does not match what was rendered on the server. TypeError: Cannot read properties of null (reading 'at')

I've seen other issues posted in the Svelte GitHub Issue list for invalid HTML, such as placing a <button> inside another <button>. If you see odd errors, double check the validity of your HTML to make sure you're not attempting to do something invalid.

Strict Enforcement of Rendering Mismatches

We also notice a stronger enforcement of rendering mismatches - when the server generates one thing and the client generates another. We received the following error....

ERR_SVELTE_HYDRATION_MISMATCH Hydration failed because the initial UI does not match what was rendered on the server. Error: this={...} of <svelte:component> should specify a Svelte component.

as the result meta tags that differed on the server vs client. We had a +page.svelte that was attempting to set a meta tag using data available to the page. However, the server did not have this data and the meta tags mismatched. Svelte will override the title & description meta tags, but not others.

Per Svelte's documentation: A common pattern is to return SEO-related data from page load functions, then use it (as $page.data) in a <svelte:head> in your root layout. https://kit.svelte.dev/docs/load#$page-data

Svelte Package w/ next Version Fails

If you plan on using @sveltejs/package to create an installable npm package, and you use next as your svelte version specifier, you'll get an error > Invalid comparator: next. You need to use an explicit version number of the svelte library for packaging to work. So your package.json file will go from something like...

to.....

Portals Stopped Working [Fixed!]

Our custom UI library makes extensive use of portals to move DOM elements to a new parent (eg the body) to avoid stacking context issues. This is common practice for Dropdowns, Popovers, Tooltips, Modals, Comboboxes, etc to function appropriately and avoid clipping. Events (eg on:click) associated with these components stopped working, as it appears Svelte 5 attaches events via delegation to the root.

The issue was identified and outlined here: https://github.com/sveltejs/svelte/issues/9777 Thankfully, the Svelte team addressed this issue in release svelte@5.0.0-next.28.

Failing 3rd Party Libraries

The following libraries aren't yet compatitible with Svelte 5 and don't work:

  • svelte-turnstile

    We make use of a 3rd party package svelte-turnstile, which implements Cloudflare's version of Captcha. This package provides a relatively simple Svelte 4 component that doesn't work when compiled to Svelte 5. There's a bug in Svelte 5 that prevents on:load events for a script tag from firing, which prevents the component from completing it's initialization.

    We submitted a PR to work around this in svelte-turnstile, but I imagine it won't be resolved until the Svelte bug is fixed.

  • svelte-confetti

    The library that powers our celebratory confetti for successfully merging a pull request stopped working. We were able to track this down to a bug in svelte-confetti and submit a PR to fix it.

Conclusions

Overall, we've been very pleased with Svelte 5. Svelte has become even easier to use than it was before and our code base has improved drastically as a result. We haven't officially released the upgrade yet, we're still in a testing phase and waiting for Svelte 5 development to stabilize a bit more, but we love the new direction.

Looking to enhance your code review experience?

Try GitContext, code reviews for modern software development teams. Ship high quality code faster than ever before.

App screenshot