Porting a medium-sized Vue application to Svelte 5
The short version: porting from Vue to Svelte is pretty straightforward and Svelte 5 is nice upgrade to Svelte 4.
Why port?
I’m working on
Edna, a note taking application for developers.
It started as a fork of Heynote. I’ve added a bunch of features, most notably managing multiple notes.
Heynote is written in Vue. Vue is similar enough to Svelte that I was able to add features without really knowing Vue but Svelte is what I use for all my other projects.
At some point I invested enough effort (over 350 commits) into Edna that I decided to port from Vue to Svelte. That way I can write future code faster (I know Svelte much better than Vue) and re-use code from my
other Svelte projects.
Since Svelte 5 is about to be released, I decided to try it out.
There were 10 .vue
components. It took me about 3 days to port everything.
Adding Svelte 5 to build pipeline
In the above commit:
- I’ve installed Svelte 5 and it’s vite plugin by adding it to
package.json
- updated
tailwind.config.cjs
to also scan .svelte
files
- added Svelte plugin to
vite.config.js
to run Svelte compiler on .svelte and .svelte.js files during build
- deleted
Help.vue
, which is not related to porting, I just wasn’t using it anymore
- started converting smallest component
AskFSPermissions.vue
as AskFSPermissions.svelte
- I finished porting
AskFSPermissions.vue
- I tweaked
tsconfig.json
so that VSCode type-checks .svelte
files
- I replaced
AskFSPermissions.vue
with Svelte 5 version
Here replacing was easy because the component was a stand-alone component. All I had to do was to replace Vue’s:
app = createApp(AskFSPermissions);
app.mount("#app");
with Svelte 5:
const args = {
target: document.getElementById("app"),
};
appSvelte = mount(AskFSPermissions, args);
Overall porting strategy
Next part was harder. Edna’s structure is: App.vue
is the main component which shows / hides other components depending on state and desired operations.
My preferred way of porting would be to start with leaf components and port them to Svelte one by one.
However, I haven’t found an easy way of using .svelte components from .vue components. It’s possible: Svelte 5 component can be imported and mounted into arbitrary html element and I could pass props down to it.
If the project was bigger (say weeks of porting) I would try to make it work so that I have a working app at all times.
Given I estimated I can port it quickly, I went with a different strategy: I created mostly empty App.svelte
and started porting components, starting with the simplest leaf components.
I didn’t have a working app but I could see and test the components I’ve ported so far.
This strategy had it’s challenges. Namely: most of the state is not there so I had to fake it for a while.
For example the first component I ported was TopNav.vue
, which displays name of the current note in the top upper part of the screen.
The problem was: I didn’t port the logic to load the file yet. For a while I had to fake the state i.e. I created noteName
variable with a dummy value.
With each ported component I would port App.vue
parts needed by the component
Replacing third-party components
Most of the code in Edna is written by me (or comes from the original Heynote project) and doesn’t use third-party Vue libraries.
There are 2 exceptions: I wanted to show notification messages and have a context menu.
Showing notifications messages isn’t hard: for another project I wrote a Svelte component for that in a few hours. But since I didn’t know Vue well, it would have taken me much longer, possibly days.
For that reason I’ve opted to use a third-party toast notifications Vue library.
The same goes menu component. Even more so: implementing menu component is complicated. At least few days of effort.
When porting to Svelte I replaced third-party
vue-toastification
library with
my own code. At under 100 loc it was trivial to write.
For context menu I re-used context menu I wrote for my
notepad2 project. It’s a more complicated component so it took longer to port it.
Vue => Svelte 5 porting
Vue and Svelte have very similar structure so porting is straightforward and mostly mechanical.
The big picture:
<template>
become Svelte templates. Remove <template>
and replace Vue control flow directives with Svelte equivalent. For example <div v-if="foo">
becomes {#if foo}<div>{/if}
setup()
can be done either at top-level, when component is imported, or in $effect( () => { ... } )
when component is mounted
data()
become variables. Some of them are regular JavaScript variables and some of them become reactive $state()
props
becomes $props()
mounted()
becomes $effect( () => { ... } )
methods()
become regular JavaScript functions
computed()
become $derived.by( () => { ... } )
ref()
becomes $state()
$emit('foo')
becomes onfoo
callback prop. Could also be an event but Svelte 5 recommends callback props over events
@click
becomes onclick
v-model="foo"
becomes bind:value={foo}
{{ foo }}
in HTML template becomes { foo }
ref="foo"
becomes bind:this={foo}
:disabled="!isEnabled"
becomes disabled={!isEnabled}
- CSS was scoped so didn’t need any changes
Svelte 5
At the time of this writing Svelte 5 is Release Candidates and the creators tell you not use it in production.
Guess what, I’m using it in production.
It works and it’s stable. I think Svelte 5 devs operate from the mindset of “abundance of caution”. All software has bugs, including Svelte 4. If Svelte 5 doesn’t work, you’ll know it.
Coming from Svelte 4, Svelte 5 is a nice upgrade.
One small project is too early to have deep thoughts but I like it so far.
It’s easy to learn new ways of doing things. It’s easy to convert Svelte 4 to Svelte 5, even without any tools.
Things are even more compact and more convenient than in Svelte 4.
{#snippet}
adds functionality that I was missing from Svelte 4.