Experiences and Caveats of Svelte 5 Migration #14131
Replies: 6 comments 11 replies
-
By the way: - let notext = $derived.by(() => {
- if (data.completeDoc == 'NoLangVersion') {
- return true;
- }
- if (data.completeDoc !== 'NoLangVersion') {
- return false;
- }
- });
+ let notext = $derived(data.completeDoc == 'NoLangVersion');
|
Beta Was this translation helpful? Give feedback.
-
@brunnerh do you have any advice what is wrong with arrow function in derived rune? <script>
let a = 2
// this works
let smallerThenTen = $derived.by(() => {
if (a < 10)
{return 'valid'}
else
{return 'invalid'}
})
// this does not work
let smallerThanTenWithArrow = $derived.by(() => {
(a < 10)
?
'valid'
:
'invalid'
})
</script>
{a < 10}
{smallerThenTen}
{smallerThanTenWithArrow} Or to have an example from real world what would be in your opinion the most clear way to migrate this code to Svelte 5: <script>
import Icon from '../components/Icon.svelte';
import { enhance } from '$app/forms';
import { tick } from 'svelte';
export let form;
export let searchingLang;
export let l;
let results = [];
let previousSearch = '';
let searchTerm;
let skip;
$: if (!!form && form?.thereIsMore) {
searchTerm = form.searchTerm;
skip = Number(form?.skip) + 20;
}
$: if (!!form?.searchResultFromAction) {
if (previousSearch == form.searchTerm && form.thereWasMore) {
results = [...results, ...form.searchResultFromAction];
} else {
results = [...form.searchResultFromAction];
previousSearch = form.searchTerm;
}
}
async function intoView(el) {
await tick();
if (el.attributes.index.nodeValue == skip - 20 && skip != undefined) {
el.scrollIntoView({ behavior: 'smooth' });
}
}
</script>
{#if results.length}
<ol>
{#each results as item, index}
<li use:intoView {index} aria-posinset={index}>
<!-- users without javascript have calculated order of results within paggination and css disables standard ol ul numbering -->
<!-- users with javascript have standard ol ul numbering and loading more feature -->
<noscript>{Number(index) + 1 + Number(form?.skip)}. </noscript>
<a href="/post/{searchingLang}/{item.id}/content">{item.title}</a>
</li>
{/each}
</ol>
{#if form?.thereIsMore}
<form
method="POST"
action="?/search&skip={skip}&thereWasMore={form?.thereIsMore}"
use:enhance
autocomplete="off"
>
<label>
<!-- Probably we do not need to bind the value as this is hidden input -->
<!-- <input name="searchTerm" type="hidden" bind:value={searchTerm} /> -->
<input name="searchTerm" type="hidden" value={searchTerm} />
</label>
<button aria-label="Button to load more search results" class="outline">
<Icon name="loadMore" />
</button>
</form>
{/if}
{:else if form?.searchResultFromAction.length == 0}
{l.noResultsFound}
{/if}
<style>
@media (scripting: none) {
/* users without javascript have calculated order of results within paggination and css disables standard ol ul numbering
users with javascript have standard ol ul numbering and loading more feature */
ol {
list-style-type: none;
}
}
</style> |
Beta Was this translation helpful? Give feedback.
-
I agree. This is why I'm not a fan of Svelte 5. The migration process is too time-consuming, with little innovation that benefits most of the existing svelte apps. |
Beta Was this translation helpful? Give feedback.
-
If someone is still currious this is how I migrate the code above eventually: Svelte 4 <script>
import Icon from '../components/Icon.svelte';
import { enhance } from '$app/forms';
import { tick } from 'svelte';
export let form;
export let searchingLang;
export let l;
let results = [];
let previousSearch = '';
let searchTerm;
let skip;
$: if (!!form && form?.thereIsMore) {
searchTerm = form.searchTerm;
skip = Number(form?.skip) + 20;
}
$: if (!!form?.searchResultFromAction) {
if (previousSearch == form.searchTerm && form.thereWasMore) {
results = [...results, ...form.searchResultFromAction];
} else {
results = [...form.searchResultFromAction];
previousSearch = form.searchTerm;
}
}
async function intoView(el) {
await tick();
if (el.attributes.index.nodeValue == skip - 20 && skip != undefined) {
el.scrollIntoView({ behavior: 'smooth' });
}
}
</script>
{#if results.length}
<ol>
{#each results as item, index}
<li use:intoView {index} aria-posinset={index}>
<!-- users without javascript have calculated order of results within paggination and css disables standard ol ul numbering -->
<!-- users with javascript have standard ol ul numbering and loading more feature -->
<noscript>{Number(index) + 1 + Number(form?.skip)}. </noscript>
<a href="/post/{searchingLang}/{item.id}/content">{item.title}</a>
</li>
{/each}
</ol>
{#if form?.thereIsMore}
<form
method="POST"
action="?/search&skip={skip}&thereWasMore={form?.thereIsMore}"
use:enhance
autocomplete="off"
>
<label>
<!-- Probably we do not need to bind the value as this is hidden input -->
<!-- <input name="searchTerm" type="hidden" bind:value={searchTerm} /> -->
<input name="searchTerm" type="hidden" value={searchTerm} />
</label>
<button aria-label="Button to load more search results" class="outline">
<Icon name="loadMore" />
</button>
</form>
{/if}
{:else if form?.searchResultFromAction.length == 0}
{l.noResultsFound}
{/if}
<style>
@media (scripting: none) {
/* users without javascript have calculated order of results within paggination and css disables standard ol ul numbering
users with javascript have standard ol ul numbering and loading more feature */
ol {
list-style-type: none;
}
}
</style> Svelte 5 <script>
import Icon from '../components/Icon.svelte';
import { enhance } from '$app/forms';
import { tick } from 'svelte';
let { form, searchingLang, l } = $props();
let previousSearch = '';
let skip = $derived.by(() => {
if (!!form && form?.thereIsMore) {
return Number(form?.skip) + 20;
}
});
let helperResultsArr = [];
let results = $derived.by(() => {
if (!!form?.searchResultFromAction) {
if (previousSearch == form.searchTerm && form.thereWasMore) {
helperResultsArr.push(...form.searchResultFromAction);
return helperResultsArr;
} else {
helperResultsArr = [];
helperResultsArr.push(...form.searchResultFromAction);
previousSearch = form.searchTerm;
return helperResultsArr;
}
} else return [];
});
async function intoView(el) {
await tick();
if (el.attributes.index.nodeValue == skip - 20 && skip != undefined) {
el.scrollIntoView({ behavior: 'smooth' });
}
}
</script>
{#if results.length}
<ol>
{#each results as item, index}
<li use:intoView {index} aria-posinset={index}>
<!-- users without javascript have calculated order of results within paggination and css disables standard ol ul numbering -->
<!-- users with javascript have standard ol ul numbering and loading more feature -->
<noscript>{Number(index) + 1 + Number(form?.skip)}. </noscript>
<a href="/post/{searchingLang}/{item.id}/content">{item.title}</a>
</li>
{/each}
</ol>
{#if form?.thereIsMore}
<form
method="POST"
action="?/search&skip={skip}&thereWasMore={form?.thereIsMore}"
use:enhance
autocomplete="off"
>
<label>
<input name="searchTerm" type="hidden" value={form.searchTerm} />
</label>
<button aria-label="Button to load more search results" class="outline">
<Icon name="loadMore" />
</button>
</form>
{/if}
{:else if form?.searchResultFromAction.length == 0}
{l.noResultsFound}
{/if}
<style>
@media (scripting: none) {
/* users without javascript have calculated order of results within paggination and css disables standard ol ul numbering
users with javascript have standard ol ul numbering and loading more feature */
ol {
list-style-type: none;
}
}
</style> This page works as a posts search with a "Load More" functionality (adding results or pagging if a user does not have JS). |
Beta Was this translation helpful? Give feedback.
-
Another new Svelte 5 mental model example is this: TTake a look at this example of Svelte 4 style of code: <script>
let value;
let derivedArr = []
$: if (value) {
derivedArr = [...derivedArr, value]
}
function random () {
value = Math.floor(1 + Math.random() * 10)
}
</script>
<button on:click={random}>Generate Random Value</button>
<p>value: {value}</p>
<p>derivedArr: {derivedArr}</p> We have two reactive variables and Svelte 4 solves the updates automatically. We only needed to remember that the right way is by reassigning the variable. In Svelte 5 we should think a little how to achieve the same result. The two variables we are using are not enough, we need one more, the helper one. Prefered way is to use a <script>
let value = $state();
let helperArr = [];
let derivedArr = $derived.by(() => {
if (value) {
helperArr.push(value);
return helperArr;
}
});
function random () {
value = Math.floor(1 + Math.random() * 10)
}
</script>
<button onclick={random}>Generate Random Value</button>
<p>value: {value}</p>
<p>derivedArr: {derivedArr}</p> I am trying visually emphasize which parts of the code are relevant and somehow "wrapped" from Svelte 4 into Svelte 5 mental model. In my opinion a developer's user experience is quite different now. Svelte 4 does a lot of stuff for you. In Svelte 5 you have to be much more vigilant. Probably under the hood Svelte 5 code is more robust now. But it is hard to tell whether some new benefits (using Svelte in JavaScript files, distinction of reactive variables) outweigh the new, quite complex interface and mental model. I am still "shaping my opinion" on Svelte 5. |
Beta Was this translation helpful? Give feedback.
-
click on commit messages in systemaccounting/mxfactorial#399 to see examples, e.g. replace exported variables with props, replace slots with snippets |
Beta Was this translation helpful? Give feedback.
-
I have recently updated a rather complex web application. The application has features like auth, Stripe, i18n, dark/light mode, PWA, etc. Overall, it has around 30 pages and components, with almost no third-party npm packages.
I would like to point out what I found quite challenging when migrating the app to Svelte 5.
Auto-Migration Script Hammer
The auto-migration script provided by Svelte can do the job for you with this "one-liner" command in the terminal
npx sv migrate svelte-5
(after you do all the necessary updates and installs: "@sveltejs/vite-plugin-svelte": "^4.0.0" and "svelte": "^5"). But I do not recommend this "hammer" approach.Go file by file, component by component with Ctrl + Shift + P (Windows/Linux) / Shift + Command + P (Mac) and use the
Migrate Component to Svelte 5 Syntax
command in the VS Code command palette instead. You will have more control that way.Deprecated run() Surprise
The script cannot perform miracles. Upgrading reactive variable declarations to
$state()
is usually fine. However, the script may struggle to detect whether$:
should be converted to$derived()/$derived.by(() => {})
or$effect(() => {})
.So, guess what? With the auto-migration script, you might end up with lots of
run(() => {})
.For example, imagine as a simplified example using something like this:
The auto-migration script will give you this:
with a nice little warning that the run function is deprecated.
The better Svelte 5 code would be this I guess:
The reason is that the script cannot transform code to
$derived.by(() => {})
easily, so it would use a more dirty approach with$effect()
. But$effect()
runs only client-side, so the script uses the deprecatedrun
function instead.Avoid $effect If You Can
Now we are getting to the most important takeaway. Which is
$effect()
running only client-side. So no$effect()
on the server, for prerendering pages and SSR.$effect()
DOES NOT RUN ON THE SERVER!This should be really emphasized in the Svelte 5 documentation.
Look at this two examples:
They are not the same. This causes a lot of challenges. The client will need to reevaluate the c variable when mounting the page. The page will look different when sent from the server and when finally DOM-rendered on the client (SSR, SEO, flicker issues, etc.).
So always try to use $derived or
$derived.by(() => {})
over$effect()
. It will save you lots of trouble.It's quite the same story as when we were discouraged from using stores in SvelteKit and SSR.
$effect vs onMount() in SvelteKit
You might be tempted to replace your
onMount()
with$effect()
thanks to the examples that were given during the arrival of Svelte 5. For the reasons already mentioned, I would discourage this for the time being. onMount is still a a core Svelte lifecycle hook.$bindable $props Surprise
The other nice surprise is that Svelte 5 takes care to have consistent variable values. If you pass a variable as a prop to a component and change this variable in the component later on, the script will try to solve this inconsistency using
$bindable $prop
. The parent should be notified, so your app state is consistent.Look at this example:
The auto-migration script will want you to use a component with binded value to ensure the parent may get the updated value back:
We are mutating the
name
varaible in this child component. So we are notifing the parent so. The parent will use this mutated value as well.If you do not need the parent to reflect the mutated
name
value we can use quite simpler way as well, you guessed it, with$derived()
:But in this later case we are not mutating name variable in the component.
:global { } Block
A very nice feature that I found during migration was that we can use CSS
:global
with block now. Styling with:global
is quite necessary if you want to style the HTML elements in@html
, for example.So instead of this:
you can use this:
Style as a Prop in Components
In Svelte 4, if you wanted to provide a CSS class as a prop to a component, you would use
{$$props.class}
:In Svelte 5 you may use
class={className}
:Possible Lighthouse Perfomance Drop
When I used the auto-merging script, I was shocked at how my app's performance dropped. With Svelte 4, I had nearly all 100%s. It was only after I manually migrated and carefully considered how (mainly how to avoid
$effect()
if possible) that my Lighthouse scores were back in the green again.Final Words
It took longer to migrate to Svelte 5 than I had expected. I still have not pushed this new version to production, though. The updates to Svelte 5 are still coming in with quite high frequency.
I hope my experience may be useful to others.
You can read this post also at https://dev.to/kvetoslavnovak/experiences-and-caveats-of-svelte-5-migration-27cp
Beta Was this translation helpful? Give feedback.
All reactions