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?
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.